cmux NEXT: pages show nothing while their link starts; a lasting failure is said once
Leo's ask, verbatim: "HOW THE FUCK IS THIS STILL NOT FUCKING CHANGED IT'S LITERALLY "STOP SHOWING FUCKING BULLSHIT ON LOAD" + "JUST FUCKING REMOVE THE FUCKING OLD DAMEMONS WHAT THE…""
This PR covers the webview surfaces that showed noise on every normal launch or daemon restart. It follows the rule #27541 uses for the agent pane's host error: while the link is still recovering, nothing shows. A failure that lasts past the grace gets the page's existing state, or one muted line. It does not touch the files #27541 changes.
What changes
- Shared helper
webviews/src/ui/lasting.ts(useLasting(active)): returns true once a state has held for 25 s without a break. That matches #27541'sHOST_ERROR_DEADLINE_MS. No grace or delay hook existed in webviews before this. Gallery stage frames set the grace to 0, so a disconnected fixture shows its state right away. - Restored agent chats (
ChatUnavailableState.tsx,chatUnavailable.ts): inside the existing 30 s grace, a chat whose machine is still coming up now draws an empty area: no spinner, no "Reconnecting…", no buttons. The chat opens once acpmux lists it. After the grace the existing offline state shows, unchanged. A peer whose daemon announced a restart (restarting; acpmux reconnects at once) now counts as still coming up. Before, it went straight to the offline spinner. A machine with failed attempts is still offline at once, and deleted and not-found chats still show at once. - Channels connect screen (
DaemonGate.tsx): while connecting it shows nothing. That includes "Starting this build's Chief…", "the local cmux daemon is restarting" and "Restarting the cmux daemon…". Once a connect has lasted past the grace, the screen shows one muted line (.hc-gate-line,--page-text-2) with the last attempt's reason. That line is never red, because the retries keep going. A final refusal (retryable: false) and the credential form still show their red line at once, as before. The Channels header's "Home is offline." / "Connecting…" waits for the same grace. - Cloud, Apps, History: the disconnected state (and Cloud's Retry) shows only once it lasts. Before that the body is empty. A link error stays hidden while the page is disconnected, as before. Search fields and Clear stay disabled while disconnected (unchanged).
- Cloud store: Apps and History already re-read when
cmux.page.connectioncomes back up. Cloud did not: a firstcmux.cloud.auth.statusrefused withcmux.protocol.transportleft it "disconnected" until someone pressed Retry. The store now reads auth again by itself.- Timer: one
setTimeoutat a time, starting at 1 s and doubling up to 10 s. - The connection stays "disconnected" while it retries, so the page never flashes "connecting".
- Stop conditions: a read that succeeds, a failure that is not a link failure, or
stop()when the last listener leaves.stop()and Retry clear the timer.
- Timer: one
Why the Swift bridge is unchanged: PageConnectionWatch and PageRouter report the owner link as it is (.connecting means not connected). Changing that on the Swift side would delay the signal for every consumer, including the stores' own reload logic. Keeping it in the webview means each page applies one shared rule to what it shows, while its stores still react to the real link state right away.
Tests
New and updated tests (bun, fake timers, no fixed waits):
src/ui/lasting.test.tsx: hidden through the grace, shown once it ends; a link that comes back inside the grace never shows; a drop after that waits the full grace again; a grace of 0 shows at once.src/agent-session/acpmux/chatUnavailable.test.ts: arestartingpeer is loading inside the grace and offline after it. Red on base: offline at once.src/agent-session/acpmux/ChatUnavailableState.test.tsx,test/chat-unavailable.test.ts,test/agent-pane-chat-unavailable.test.ts(Playwright): a connecting chat renders an empty area, with no text, no role and no buttons; offline keeps its spinner. Updated from the old "Reconnecting…" assertions.src/pages/home-channels/DaemonGate.test.tsx: a starting daemon shows nothing for 25 s, then one muted line and no red. A final refusal is red at once.test/history-page-error.test.tsx: a lost host shows nothing within the grace and its disconnected line once it lasts.src/pages/cloud/store.restart.test.ts: a refused first auth read is retried on the backoff and comes up connected; retries cap at 10 s and stop on unsubscribe.
Test receipt: all the tests above, plus test/apps-flow, test/history-page-parity, src/pages/{cloud,apps,history} and hostRetry, and the full bun run check, green at dde4a546df7b0f822b88cf4a1b3989be404eed6f: https://github.com/manaflow-ai/cmux/actions/runs/40996154926. (Earlier runs failed on my own new tests, formatting and one lint warning; all fixed.)
Observed / Not run
- Not run on this host: the gallery and a live launch.
Screenshots
Before / after: the 2 s frame, right after the page's link drops. Base shows the disconnected state at once; head shows the toolbar with an empty body. (A 27 s pair, base and head, both show the disconnected state, so a lasting failure still shows.)
BASE (2 s):
Capture steps-173df2df20 at c0331557f543 (fleet build, capture mini, text checked for private data)
| Light | Dark |
|---|---|
![]() | ![]() |
HEAD (2 s):
Capture steps-173df2df20 at dde4a546df7b (fleet build, capture mini, text checked for private data)
| Light | Dark |
|---|---|
![]() | ![]() |
Changelog
- Fixed: cmux NEXT no longer shows "Reconnecting…", "Connecting…", red "Could not connect" lines or "disconnected" while the daemon starts or restarts; a failure that lasts is shown once.
Agent session: agent session
🤖 Generated with Claude Code



