cmux NEXT: terminals stay quiet while a handoff adopts their shell
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) waitreconnectingRevealDelay= 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
adoptingpush holds the label for about 5 s; runningwithin the grace hides it and leaves no animation;failedand "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)
| Light | Dark |
|---|---|
![]() | ![]() |
HEAD, run 1s (96030b74, 1 s into the handoff)
| Light | Dark |
|---|---|
![]() | ![]() |
BEFORE, run 6s (91f00ab1, 6 s into the handoff)
| Light | Dark |
|---|---|
![]() | ![]() |
HEAD, run 6s (96030b74, 6 s into the handoff)
| Light | Dark |
|---|---|
![]() | ![]() |
Captured at 96030b74; the later commit (cbadece, a base merge) changes no terminal UI.
Agent session: agent session
🤖 Generated with Claude Code







