fix(app): Ctrl-` right after Cmd-N opens its terminal; a create on a pane still opening waits for it (P0 STATUS 2080)
Summary
P0 (STATUS 2080, 2105): a person presses Cmd-N and then Ctrl- at once. No terminal opened: the window showed only the New Tab page. Journey rounds: 4 of 4 quick rounds on two builds, 1 of 13 normal rounds; the desync owner's probe (Cmd-N, then Ctrl- once the workspace shows): 20 of 30 presses on cmux22. Every lost press showed a refusal toast: "This pane is still opening; try again in a moment." or "New Terminal Tab did not apply: new-tab: the daemon refused the new tab".
Cause
- Cmd-N shows its workspace at once with a stand-in pane (
ProvisionalWorkspace), which has no public pane id. The Ctrl-` targets that stand-in. DaemonStore.daemonTarget(the target every create resolves at send time) threwPaneNotReadywhile the stand-in was still shown: the key was refused, never queued. The code named this as a known follow-up ("one seam where every verb waits for its pane").- Once the daemon's workspace had taken the stand-in over, the stand-in was gone from the store, and nothing mapped it to the daemon's pane (a split's stand-in follows its public pane id; Cmd-N's has none). The target resolved to
PendingPaneNotCreated, which New Terminal Tab reported as "the daemon refused the new tab" although no request was sent. So a press shortly after the hand-over (a window that still shows the stand-in, or a send that runs after it) was lost too: this is the 16 of 20 "already on the daemon pane" presses of the probe.
Fix
The one seam: DaemonStore.daemonTarget is async (DaemonStore+StandInTarget.swift).
- A stand-in whose own create is still in the intent log (Cmd-N's workspace, a split, a column, a row) is a valid target. The create waits there, off the key's turn, inside its action work (
.waiting-style work, never on the main thread's turn). The store resumes it from the reconcile that applies the hand-over: every event, snapshot, settlement and rejection passes there, so no timer and no retry. The wait is bounded by the stand-in's own create (its reply, its failure, its connection's end). - Cmd-N's stand-in resolves by its workspace key to the daemon's pane that holds Cmd-N's first tab (the New Tab page the stand-in showed;
ProvisionalWorkspaceOverlay.adoptedPane), so the terminal lands after that page, in the pane where it was pressed, also when a split made meanwhile is the active pane. When Cmd-N's intent leaves the log, the workspace model records the stand-in pane and that first tab (WorkspaceModel.tookOverStandIn), for a window that still shows the stand-in for a frame. The record lives exactly as long as the workspace: no constant cap (rule R9). - A failed create resumes the wait with
PendingPaneNotCreated: a visible refusal then, never silence. - Every create goes through it: New Terminal Tab (optimistic and the typed path), Cmd-D (also Split Left/Up with its swap), New Column, New Row. New Terminal Tab and Cmd-D now report
PendingPaneNotCreatedas itself, not as "the daemon refused". - The Ctrl-
's stand-in tab shows in Cmd-N's stand-in pane in the key's turn and moves to the daemon's pane at the hand-over. Keys typed meanwhile wait in the existing creation hold (CreationInputCoordinator`, unchanged), which ends when the create resolves. - Action contract and budget: the handlers do not change shape.
newSurface(and Cmd-D, New Column, New Row) does in the key's turn only what it did before: the stand-in tab or pane goes into the store as an intent and lays out in that frame; it awaits nothing. The wait is inside the create's tracked work (registry.trackRollingBack, the same task that already waited for the workspace's create chain and the daemon's reply), never in the handler and never on the key's turn. The key path adds no work:daemonTargetruns only in that task. Each reconcile adds oneisEmptycheck (resolveStandInWaits), and a walk of the waiting creates only while one waits. perf claim: none. - Not changed: non-create pane verbs (zoom, rename, swap) on a stand-in still refuse with the notice (lead decision on rule b).
- Keys typed while the Ctrl-
waits: the window's creation hold (CreationInputCoordinator.begin, unchanged) opens in the press's turn, inPaneController.newTerminalTab, before the create's work starts; its ticket resolves only when that work ends (after the wait and the reply). So every key typed during the wait is held for the new terminal and replayed when its focus lands. Checked bykeysTypedWhileCtrlBacktickWaitsForCmdNAreHeldForTheNewTerminal` and on screen by the quick journey's step 6. - Known limit, not changed here (it is
CreationInputCoordinator, owned by #29301/#29409): the hold's 2 s bound (holdLimit) counts from its start. A Cmd-N slower than 2 s releases the held keys to the focused view (the stand-in tab) before the terminal exists.
Reviewer, please check: two existing tests now expect the opposite of before. In PaneVerbOnPendingPaneTests:
splitLeftOnAPendingSplitsPaneShowsOneNoticeAndSendsNothingis renamedsplitLeftOnAPendingSplitsPaneSplitsItsDaemonPaneWithNoNotice. Before: Split Left on a pending split's pane was refused with the notice and made no pane. Now: it makes the pane beside the daemon's pane, with no notice.aCommandTypedIntoANewTabOnAPendingPaneShowsOneNoticeis renamedaCommandTypedIntoANewTabOnAPendingPaneOpensInItsDaemonPane. Before: refused with the notice, no tab. Now: the tab opens in the daemon's pane, with no notice.- Both are creates, and the rule (a key belongs to the target the person meant) makes the old refusal the bug. The zoom test and the every-funnel test keep the refusal: those are not creates.
Testing
Red first (test-only commits on the base 3531e040b66):
CmdNStandInTargetTests(CmuxNextDaemonTests, store only): a create on Cmd-N's stand-in waits, then names the daemon's pane, and the Ctrl-tab moves after the page; a failed Cmd-N givesPendingPaneNotCreated`.CmdNStandInTargetTests.aCreateOnCmdNsStandInReadAfterTheHandOverNamesTheDaemonsPane(second refusal kind, "the daemon refused the new tab"): the target read after the daemon's workspace took the stand-in over, with Cmd-N's intent still in the log and after it left, must name the daemon's pane.CmdNThenNewTerminalTabTests(CmuxNextAppTests, same-tree daemon):newTerminalTabPressedOnCmdNsStandInOpensItsTerminalInTheNewWorkspace(first kind, "still opening"): Cmd-N, then newSurface on its stand-in pane (the daemon is stopped across both presses, so the race is reached in every run), must yield a selected terminal tab in the new workspace, with no failure and no refusal.newTerminalTabOnCmdNsStandInAfterTheHandOverOpensInTheDaemonsPane(second kind): the Ctrl-` reaches the pane controller that still shows the stand-in after the hand-over; it must open its terminal in the daemon's pane, with no refusal.keysTypedWhileCtrlBacktickWaitsForCmdNAreHeldForTheNewTerminal: Cmd-N, Ctrl-, a line typed at once (the daemon stopped across all three): the creation hold is open after the Ctrl-, every key is held, the Ctrl-makes its terminal, and the hold ends byfocusLanded` on it. (A replayed key reaches only the key window, and a test window is never key, so the line's run in the shell is the journey's step 6.)
- Red ref: branch
p0/cmdn-ctrlbt-pending-verb-redat eb8eb59d0dd (base 3531e040b66 + the three test commits only).
Fleet evidence: filled in as the jobs end.
- Review round 1 (receipt https://github.com/manaflow-ai/cmux/pull/29530#issuecomment-6547143059, at bcb6e44f6d3) noted that the target was the active pane, not the first tab's pane, and a list capped at 32 (an R9 cap that its receipt line did not catch: the author's prompt gave it the "R9: pass" line). Both are fixed at 65923eb1679, red first:
CmdNStandInTargetTests.aCreateOnCmdNsStandInGoesToThePaneOfItsFirstTabNotTheActivePane(commit 58eec98d2f6 = the old head + this test). A new receipt is needed at the new head. - At head 65923eb1679: compile c316ec33b93e44241323f732 PASSED (CmuxNext 189 s, app 58 s, cmux-tui 30 s). RED of the new test at 58eec98d2f6, step 391ae7ebf125b906e67d5947:
aCreateOnCmdNsStandInGoesToThePaneOfItsFirstTabNotTheActivePaneFAILS in both states (named the active pane 13, not the first-tab pane 11); the other 4 pass. GREEN display step adf5aa80d1c952a48960ac45 (console session,CMUX_TEST_REQUIRE_GUI=1):CmdNStandInTargetTests5/5,CmdNThenNewTerminalTabTests3/3,PaneVerbOnPendingPaneTests4/4,NewTerminalOnPendingSplitTests1/1, no FLAKY notice. - Earlier: head bcb6e44f6d3 job 999325bd456ae30fc808797e PASSED (CmuxNext 208 s, app 60 s, cmux-tui 41 s); red ref eb8eb59d0dd job 9e23957284904e5b93699d67 PASSED.
- RED, store (step 7bea4b3752debb546acf14f5, red ref eb8eb59d0dd):
CmdNStandInTargetTests3 of 4 FAIL, each on the bug's own outcome. Both refusal kinds are reproduced:- kind 1, "still opening":
aNewTerminalTabOnCmdNsStandInWaitsForTheDaemonsPaneAndNamesItgets "create: the pane is still opening (pane=6917529027641081866); not sent". - kind 2, after the hand-over:
aCreateOnCmdNsStandInReadAfterTheHandOverNamesTheDaemonsPanegets "the pane it was for was not created" (PendingPaneNotCreated) and names no pane, both with Cmd-N's intent in the log and after it left. aCreateOnCmdNsStandInIsRefusedWhenCmdNFailsfails because the create ends at once with "still opening" before Cmd-N fails.- The guard
aDaemonPaneIsItsOwnTargetpasses.
- kind 1, "still opening":
- RED, display (step baf99ec60bc657eca6224bf5, red ref eb8eb59d0dd, gui-session-v1 console session,
CMUX_TEST_REQUIRE_GUI=1):CmdNThenNewTerminalTabTests3 of 3 FAIL, on the bug's own outcome:newTerminalTabPressedOnCmdNsStandInOpensItsTerminalInTheNewWorkspace: failures ["new-tab: the daemon refused the new tab"], no terminal.newTerminalTabOnCmdNsStandInAfterTheHandOverOpensInTheDaemonsPane: failures ["new-tab: the daemon refused the new tab"], no terminal.keysTypedWhileCtrlBacktickWaitsForCmdNAreHeldForTheNewTerminal: failures ["This pane is still opening; try again in a moment."], hold endedcreationFailed, the focused tab is not a terminal.
- GREEN, display (step 876055321c56fb1f26fbdcea, head bcb6e44f6d3, console session,
CMUX_TEST_REQUIRE_GUI=1): all pass, no FLAKY notice:CmdNStandInTargetTests4/4,CmdNThenNewTerminalTabTests3/3,PaneVerbOnPendingPaneTests4/4,NewTerminalOnPendingSplitTests1/1. - Quick journey, BASE build dd6f7c59db5b8740c037603a (feat 3531e040b66), cmux24, whole host, mini scheduler: 5 of 5 FAIL at step 3 (the Ctrl-
made no terminal in 20 s), 0 not run, no New Tab hold marker. HEAD build 544d2d06298c13ba6cee2020 (bcb6e44f6d3), cmux21: 5 of 5 PASS at step 3, and step 6 (the typed line runs in the new terminal within 1 s) passed in all 5, keyshandled_by: responder`, no New Tab hold marker (6th round pending).
| Journey round | Build | Host | Step 3 (Ctrl-` made a terminal) | Step 6 (typed line within 1 s) |
|---|---|---|---|---|
| p0cbv-qbase s1-s5 | base dd6f7c59 | cmux24 | FAIL 5/5 | not reached |
| p0cbv-qhead s1-s5 | head 544d2d06 | cmux21 | PASS 5/5 | PASS 5/5 |
- Rate probe by the desync owner (https://github.com/manaflow-ai/cmux/pull/29530#issuecomment-6546983701), cmux22, whole host, interleaved base/head/base/head, 30 Cmd-N + Ctrl-` cycles per launch: base 51c66359 lost 13 and 4 of 30 (no terminal by 5 s); head 544d2d06 lost 0 and 0 of 30, all terminals within 1 s, the closing typed line ran in both head rounds.
- Open, not caused by this PR (same rate on base and head): about 2.5 "This pane is still opening" toasts per cycle in the rate probe, also in cycles whose terminal came. The in-app tests above assert no refusal toast for Cmd-N + Ctrl-
(they pass), so the source is outside that path; a split probe (toast reads after Cmd-N, after Ctrl-, +1 s, +3 s; every 4th cycle Cmd-N only) is running on both builds. - Journey (judged as the desync owner and the coordinator advise): the probe keywin-rate-notoast (30 Cmd-N + Ctrl-
cycles per launch, "no terminal 1 s after the Ctrl-" and new refusal toasts per press), 2 or more leases on the head interleaved with base leases on the same mini; and the quick journey cmdn-ctrlbt-type-quick (kit cp/journey-cmdn-ctrlbt@37613bbc42d), 5 or more rounds per build through the mini scheduler, judged at its step 3 (the Ctrl-made a terminal); its step 6 (typed line within 1 s) is reported for the head too, apart: a step-6 failure withdebug.focus new_tab_key_log` "hold key #N" is Cmd-N's New Tab hold (#29407, not merged); a step-6 failure without it is this PR's. Base build dd6f7c59db5b8740c037603a (3531e040b66). - Note: keys typed while the create waits stay in the existing creation hold, whose 2 s bound (
CreationInputCoordinator.holdLimit) is not changed. A Cmd-N slower than that lets them go on to the focused view before the terminal exists (existing behavior, not new).
Observed / Not run
- Observed (I read every log myself): compiles of each head (latest c316ec33b93e44241323f732); the red steps 7bea4b3752debb546acf14f5, baf99ec60bc657eca6224bf5, 391ae7ebf125b906e67d5947; the green display steps 876055321c56fb1f26fbdcea and adf5aa80d1c952a48960ac45 (no FLAKY); the quick journey rounds (base 5 fail, head 5 pass); the desync owner's rate-probe result files; the prepush gate (no new violations). Independent review: https://github.com/manaflow-ai/cmux/pull/29530#issuecomment-6547822672 (SHIP at 65923eb1679).
- Not run: the full CmuxNext suite; macOS 14/15 VMs; journey rounds on the head 65923eb1679 build itself (the rounds ran on the bcb6e44f6d3 build: the later commits change only which daemon pane a stand-in resolves to when Cmd-N's workspace has more than one pane, covered by the store test).
Changelog
Fixed: Pressing Ctrl-` right after Cmd-N now opens the terminal in the new workspace instead of showing "This pane is still opening".
Performance
No perf numbers: a correctness fix. The key's turn does the same work as before (the stand-in shows as an intent); the wait is in the create's tracked work, off the key's turn.
Proof
No capture: no new UI; the existing terminal tab now appears. The evidence is the journey rounds above.
perf claim: none