cmux NEXT: Retry Now and Restart cmux-tui on the not-connected screens (cx-jniyz.1)

Merged
TE
20d ago
+3919-1933

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 ensure has failed: a restore timeout, the ensure call itself timing out, or server.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-unresponsive added, and fires retryWake whether 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.

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 by DaemonService.
  • The buttons are in DaemonRecoveryControls.
  • The App Store banner uses a small AppsRecovery hook. Its accessibility changed from .combine to .contain so 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-in cmux-tui script 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-unresponsive and then retries. It also covers a refusal.
  • Red (test commit 6db7e849, fleet run-swift-testing-suites.sh Packages/macOS/CmuxNext, job af884d75c1e513231215d6df): only DaemonRestartTests failed. 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): DaemonRestartTests passed 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.
  • 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.py and check-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):

basehead
lightbase lighthead light
darkbase darkhead dark

App Store not-connected banner:

basehead
lightbase lighthead light
darkbase darkhead dark

Restart flow (head): Restart cmux-tui, confirm, "Restarting cmux-tui…", reconnected about 11 s after the press (owner 68203 → 67513).

restart flow

Full quality:

lightdark
dialog lightdialog dark

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
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 ensure failed the way a stuck owner makes it fail (restore timeout, no answer, or server.unavailable). It asks for confirmation first, then runs the app's ensure command with --replace-unresponsive and 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 CmuxNextDaemonRecovery target; DaemonRestartTests covers 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.py verifies strip recordings frame by frame.