cmux NEXT: terminals stay quiet while a handoff adopts their shell

TE
6d ago
+164-7

Leo: "HOW THE F*** IS THIS STILL NOT FING CHANGED IT'S LITERALLY "STOP SHOWING FING NONSENSE ON LOAD" + "JUST FING REMOVE THE FING OLD DAMEMONS WHAT THE…""

What was wrong

After every update handoff the new cmux-tui marks each tab's terminal_state adopting until it takes the shell's host back. TerminalLinkWatch.shellStall turns adopting/reconnecting into TerminalShellStall.reconnecting, and TerminalStatusBanner showed its text, "Reconnecting to the shell…", at once, so every visible terminal flashed it on each update. A view whose own attach dropped (TerminalAttachMachine.reconnect, .disconnected(_, reconnecting: true)) showed "Reconnecting…" at once too. Only "Restoring…" waited (0.5 s).

The fix

TerminalStatusBanner.revealDelay(for:stall:) is the one rule for when a label may show:

  • "Reconnecting to the shell…" (stall .reconnecting) and "Reconnecting…" (link reconnecting) wait reconnectingRevealDelay = 5 s, then fade in, and never show if the state clears within it.
  • "Restoring…" keeps its 0.5 s.
  • Every lasting state shows at once, as before: "Disconnected: … · Click or type to reconnect" after the quiet retries run out, "Terminal stopped: …", "Process exited"/"Terminal lost", "Turned off by your organization".

Why 5 s: a normal adopt finishes in well under a second for a live host. The worst normal case is a slow host, which costs up to 1 s of connect retries plus 2 s of handshake (HOST_CONNECT_RETRY_WINDOW, HOST_HANDSHAKE_TIMEOUT in cmux-tui-core/src/terminal_host_runtime.rs), after up to 2 s for the owner to say ready (STARTUP_READY_WAIT, mux/terminal_adoption.rs). 5 s covers that.

While a hidden banner is still waiting, a change keeps the longer wait from the same start. So "Restoring…" turning into a reconnecting stall does not show at 0.5 s. A banner already on screen stays on screen: no flicker back to hidden.

Nothing is removed. The banner, its strings and the lasting "Disconnected" state are unchanged, and no strings are added. It reuses the existing hold/scheduled-fade animation, which is removed whenever the banner hides or shows a lasting state.

Tests

TerminalShellStallTests.aHandoffAdoptionStaysQuietUnlessItLasts covers:

  • the delay rule for each state;
  • an adopting push holds the label for about 5 s;
  • running within the grace hides it and leaves no animation;
  • failed and "Disconnected" show at once;
  • "Restoring…" turning into a stall keeps the 5 s wait.

On base it fails: there is no hold for the stall, and revealDelay doesn't exist. Swift compiles in this PR's CI.

Risk: low. One view's reveal timing, with a lasting failure still shown at once.

Screenshots

Before / after: a terminal during an update handoff. At 1 s into the handoff, BEFORE shows 'Reconnecting to the shell…' at the bottom of the terminal and HEAD shows the shell output with no banner. At 6 s, both show 'Reconnecting to the shell…', so a handoff that lasts still says so. Captured on the fleet, each still checked by eye. BEFORE is the merge-base plus only the Debug-only debug.terminal_state route (no fix).

BEFORE, run 1s (91f00ab1, merge-base + debug.terminal_state only, 1 s into the handoff)

LightDark
BEFORE 1s lightBEFORE 1s dark

HEAD, run 1s (96030b74, 1 s into the handoff)

LightDark
HEAD 1s lightHEAD 1s dark

BEFORE, run 6s (91f00ab1, 6 s into the handoff)

LightDark
BEFORE 6s lightBEFORE 6s dark

HEAD, run 6s (96030b74, 6 s into the handoff)

LightDark
HEAD 6s lightHEAD 6s dark

Captured at 96030b74; the later commit (cbadece, a base merge) changes no terminal UI.

Agent session: agent session

🤖 Generated with Claude Code