cmux NEXT: Retry Now and Restart cmux-tui on the not-connected screens (cx-jniyz.1)
Summary
cx-jniyz.1, the Swift half of cx-jniyz item (b). When cmux-tui is stuck, the app's launch window and the App Store's not-connected banner showed the failure but offered no way out.
Both now have two buttons:
- Retry Now fires
DaemonService.retryWake. - Restart cmux-tui appears only after a plain
server ensurehas failed: a restore timeout, the ensure call itself timing out, orserver.unavailable. The verb can't tell a stuck owner from one still restoring, so the restore bound has to pass first. A later successful ensure hides the button again.- It asks first, in the app's window-scoped dialog (
cmux.dialog.restartDaemon): "Restart cmux-tui? cmux-tui is not responding. Restarting it stops the stuck process and starts a new one. Terminals running in it may be lost." Buttons: Cancel / Restart. - It then runs the app's ensure command with
--replace-unresponsiveadded, and firesretryWakewhether that worked or not. - The replace prints nothing for up to 20 s, so the call allows 30 s of silence before restore progress applies, with a hard cap of 180 s.
- A
server.replace_*refusal (exit 3) shows "cmux-tui could not be restarted. Try again in a moment." The refusal code goes to Copy Details.
- It asks first, in the app's window-scoped dialog (
Remote machines get neither button: there is no launcher for them.
Where the code is:
- How each ensure ended is recorded in
DaemonLauncher+Replace.swift. - The state and actions are in
DaemonRecovery, owned byDaemonService. - The buttons are in
DaemonRecoveryControls. - The App Store banner uses a small
AppsRecoveryhook. Its accessibility changed from.combineto.containso the buttons can be pressed.
The 7 new strings are in all 21 languages. The translations are machine-made and need a human review.
Testing
- New test:
DaemonRestartTests. A stand-incmux-tuiscript runs on the app's real startup path; no real daemon is started and no process is signalled. It checks that Restart is hidden before a failed ensure and offered after it, and that pressing it runs ensure with--replace-unresponsiveand then retries. It also covers a refusal. - Red (test commit 6db7e849, fleet
run-swift-testing-suites.sh Packages/macOS/CmuxNext, job af884d75c1e513231215d6df): onlyDaemonRestartTestsfailed. The app reached the failure screen, but the buttons were missing:(retry → nil) != nil,(shown("daemon.restart") → nil) != nil. - Green (758e3eff, jobs 599df8b981aa8790145610b8 and 558a20f6d216e1cde74136d8):
DaemonRestartTestspassed in both runs (336 suites).- Both runs had one failure in
SidebarInsertWhileDisplayAsleepTests. This PR doesn't touch that suite. It came in with #20663, only runs when the host's display is awake, and passed on the headless red host. Not yet proven to be a base failure.
- Both runs had one failure in
- Release compile of 758e3eff (fleet, isolated, job f4ef136ef8f5f67e4ce6f78b): BUILD SUCCEEDED on Xcode 26.6. There is no Xcode 27 compile receipt yet; the bead asks for one.
- Local checks:
crash_ratchet.py,check-no-godfiles.sh(no file grew past its limit),check-concurrency.sh,check-system-blue.pyandcheck-l10n.sh(21 languages, 0 errors) all passed. - Not yet done:
- the GUI proof from the bead (fleet app, SIGSTOP its own daemon, press Restart through the debug socket, see a new pid);
- light and dark captures of both surfaces;
- the App Store banner path has no test of its own;
- restart behaviour is not yet agreed with the PTY-holder lead.
Changelog
Added: Retry Now and Restart cmux-tui on the not-connected screens when cmux-tui stops responding
Target
The launch window's connecting and failure screen (DaemonConnectingView), the recovery controls, the App Store banner's recovery actions and their 12 strings live in a new CmuxNextDaemonRecovery target (SW14). CmuxNextApp keeps only the wiring in WindowController and AppsService. CmuxNextApp drops to 113,559 lines: 170 under base (113,729), 133 under the cap (113,692). That also clears the base's +37 over the cap from #20709, which #20883 fixes separately. ci-target-graph.json is updated by hand to match; the cmux-next generated files job checks it.
Proof
Captures (fleet DEV builds of 758e3eff and its base; method and PID evidence in this comment)
Launch window failure screen (owner SIGSTOPped, no saved layout):
| base | head | |
|---|---|---|
| light | ![]() | ![]() |
| dark | ![]() | ![]() |
App Store not-connected banner:
| base | head | |
|---|---|---|
| light | ![]() | ![]() |
| dark | ![]() | ![]() |
Restart flow (head): Restart cmux-tui, confirm, "Restarting cmux-tui…", reconnected about 11 s after the press (owner 68203 → 67513).

Full quality:
| light | dark |
|---|---|
![]() | ![]() |
🤖 Generated with Claude Code
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Adds Retry Now and Restart cmux-tui to the not-connected screens so a stuck cmux-tui no longer leaves the launch window and App Store banner at a dead end. Remote machines get neither button, since they have no launcher.
- Retry Now fires the service's retry wakeup immediately.
- Restart cmux-tui appears only after a plain
server ensurefailed the way a stuck owner makes it fail (restore timeout, no answer, orserver.unavailable). It asks for confirmation first, then runs the app's ensure command with--replace-unresponsiveand retries the connection; a refusal shows an error and leaves Retry Now. - A cancelled ensure no longer hides Restart, and a successful ensure clears an earlier Restart failure so it does not resurface on a later disconnect.
- The launch screen and recovery controls moved into a new
CmuxNextDaemonRecoverytarget;DaemonRestartTestscovers the flow against a stand-in cmux-tui script on the app's real startup path.
Also in this branch
- A strip's tab bar lays out for where its pane rests, so its tabs move rigidly with the column instead of being pinned to the traffic lights while the strip camera scrolls; a moving column keeps the larger traffic-light inset until it rests.
- A top-item selection repaints only the old and new sidebar item, with no band update or list reload; the perf gate budgets 0 row configures and band updates and names list reloads by caller, and
strip-scroll-check.pyverifies strip recordings frame by frame.









