fix(quit): Cmd-Q with local terminals asks how to quit (Keep Sessions default, End Sessions Keep Layout, End Everything); Don't ask again writes app.quitBehavior (QUIT P0)
What
QUIT P0 (Lawrence 2026-11-01 01:05Z: "after quitting and reopening, i lose all the workspaces and terminals ... cmd q needs to prompt for what type of quit"). A person's Cmd-Q (menu, Dock) with local terminals now asks how to quit, in one standard cmux dialog (CmuxDialogSpec, key hints):
- Keep Sessions Running (default, Return): terminals and agents keep running in their daemons and come back at the next launch.
- End Sessions, Keep Layout: ends the local terminals, keeps the tabs (they restart in place).
- End Everything: ends them and deletes the local workspaces.
- Cancel (Escape).
- Don't ask again writes the choice to
app.quitBehavior(Settings > General > When Quitting), which then applies with no question. A remembered End Everything deletes the local workspaces at every later Cmd-Q with no question: this is the same as setting "End Everything" in Settings today, and the choice is shown on the destructive (red, no key) button; Settings > When Quitting takes it back.
The four buttons do not fit on one line of the 380 pt dialog card (review finding), so CmuxDialogView now stacks a button row that does not fit: each button the card's width, the default (Keep) on top, as NSAlert does. Dialogs whose buttons fit are unchanged.
Scripts (cmux app quit, the socket), SIGTERM/SIGINT/SIGHUP and power off never ask. With no local terminal there is nothing to choose: it quits with Keep. The unsaved-changes question stays the quit's only question when it shows (#17501). End Sessions and End Everything are destructive buttons with no key.
History (why this reverses a decision)
Takes back aba72a3fbae (cx-7vu, 2026-10-11, from Lawrence's 2026-10-07 "no confirm dialogs"): it made QuitPolicy.decide quit with Keep and no question unless incognito programs ran, so app.quitBehavior: "ask" (the default) never asked. Lawrence's newest words (2026-11-01 01:05Z) ask for the prompt; the newest words win. That commit is older than 7 days, so no take-back hold applies.
Cause (STATUS 2718)
- The missing ask: proven from code above, the same on feat and on the dogfood line.
- The ended daemon on Lawrence's Mac: the version handoff at the reopen (a new dogfood build over an older daemon). The journey
quit-reopen-handoffreproduces that handoff on the fleet (log linedaemon <pid> runs build b3b8b44...; handing off to be22bd3b...), and there every workspace, terminal, the holder and the scrollback came back. The holder-mode startup pass that can end a shell is fixed separately (fix/p0-quit-reopen-holder, risk:high).
Tests
-
QuitChoiceTests(CmuxNextAppTests): red commit 8316ec323b0, fix 6bfec69a871. Because the feat base does not compile (NativePopupHost, #28704; fix #29856), the proof ran on scratch refs = the #29856 head 87682a642ac plus these commits (never merged):- red: 8e4b6a0fda6 (test only) — fleet step 1930abc2b7aacbe63c95d704 on aws-m4pro-6:
cmdQWithLocalTerminalsAsksWithKeepAsTheDefault()recorded an issue at QuitChoiceTests.swift:15 ("quit with no question"); the 5 guard tests pass on the base too. - green: 808b04d12a0 — fleet step 6b5e5badc14beec232d50cdf on aws-m4pro-1:
Test run with 6 tests in 1 suite passed. - compile:
cmux-ci compile-check --app --package CmuxNext --recipe-version 2of 808b04d: b67588d83ea2b504d8f83090 passed (CmuxNext, app, cmux-tui; the log compiles QuitPolicy.swift and QuitChoiceTests.swift).
- red: 8e4b6a0fda6 (test only) — fleet step 1930abc2b7aacbe63c95d704 on aws-m4pro-6:
-
Journeys (kit cp/journey-quit-reopen-restore@7d7593f260e, independent review SHIP; a REAL Cmd-Q by CuaDriver to the key window, whole-host lease) on the dogfood build 9276e456:
quit-reopen-restoreRED 3/3 (cmux22, cmux23) andquit-reopen-handoffRED 2/2 (cmux19), each ONLY at the dialog checkquit_asked(logquit origin=interactive behavior=askthenquit choice=keep); every restore check passed (same daemon or a logged handoff, holder kept, workspaces, terminals, focus and scrollback 1..400 back). Green rounds on this fix's build (job b18f6c6e6283198241db0eb2) follow. -
CmuxDialogButtonFitTests(CmuxNextDesignTests): red 325b2a3b2a4, fix 1ac555cf68c; on the scratch ref: red 4297aca13bf step 2df0dc59f812dd14e73a0a32 (aws-m4pro-1): FAILED at CmuxDialogButtonFitTests.swift:25/28 (the base squeezes the buttons to 0 and 99 pt and the default is not on top); green 3eac1f0be7d step a87cf09a4e9d66c2ea92b510 (aws-m4pro-1): CmuxDialogButtonFitTests 2/2 and QuitChoiceTests 6/6 passed. Compile of 3eac1f0be7d: 9b7f249e686afab9d998cf93 passed. -
Scripts that assumed Cmd-Q never asks now answer the dialog's Keep:
scripts/fleet-quit-persistence.sh(checkquit-asks),scripts/cmux-next/dialogs-undo-live.py(quit-asks),scripts/cmux-next/quit-launch-bound.py(interactive and wedged modes).plans/cmux-next/quit-persistence.mdrecords the change. -
Journey rounds on feat DEV builds (kit a1f077f5641 line, whole-host, REAL Cmd-Q): base 66cbc3a8 (feat 8922fc2 + #29856): no dialog (red). Fix 8faff1dd (this PR on that base): the dialog shows with Keep default and is answered in 5/5 rounds (cmux21); every restore check passes in 5/5 (same daemon pid, holder, 4 workspaces, 5 terminals, focus on the split, scrollback 1..400).
-
Known open fault, NOT this PR's, tracked separately (lead 03:09Z): in 4 of those 5 rounds the first line typed into the restored, focused terminal right after the reopen was lost (keys handled by the window's responder, nothing reached the PTY). The base build shows it too (1 of 3), the dogfood line does not (0 of 5). Its own red-first fix PR follows; the journey keeps the step (not weakened).
Risk
risk:normal by the classifier (Swift UI logic, copy, tests; no new delete: End Everything's delete is existing code). Treated with high-tier evidence anyway, because a destructive quit choice is now one click from Cmd-Q: an independent review receipt at the exact head and a green compile. Ships behind the existing app.quitBehavior setting ("keep" restores the old no-question quit).
Localization
One key changed: quit.button.quitEverything ("Quit Everything") is now quit.button.endKeepLayout ("End Sessions, Keep Layout"), with the translations of the existing Settings choice settings.choice.quitEndKeepLayout (all 20 locales; the 8 required ones marked translated). scripts/localization_catalog.py check: no new parity error.
Changelog
- Changed: Cmd-Q with running terminals asks whether to keep them running, end them and keep the layout, or end everything; "Don't ask again" remembers the choice in Settings.
Observed / Not run
- Observed: the green QuitChoiceTests step above; the compile above; the prepush gate (no new violations); journey rounds on the dogfood build (red at the dialog check; every restore check passed).
- Not run yet: the journey on this fix's build, the speed numbers. Capture: follows with the green journey round on the fix build (the journey's check frames show the dialog).