fix(app): keys typed right after Cmd-W go to the pane that gets the focus, never into the closing terminal (P0 close-then-type)

Merged
LA
5d ago
+554-37

#29301 (P0 1193 lane code, which this fix builds on) merged at 373f34634dc0; this branch now holds feat with it, and the diff is only the close-then-type change.

Summary

P0 close-then-type (STATUS 1333, 1353): after Cmd-W closed the focused pane, the first typed key went into the closing terminal. Journey r3-after-switch round s3 (cmux19, build a0f707a2) ran cho r3fc-… in the remaining shell.

Cause: a person's Cmd-W on a live terminal first asks the daemon what the terminal runs. This is the close warning's read: TabCloseGuard.close, and CloseUndoToasts.decideFirst when that function asks the terminals itself. The closing terminal kept the focus and the keyboard until that read answered. Keys typed during the read went into the closing terminal.

Fix:

  • The read opens a close lane in the window's creation hold (CreationInputBuffer.openClose). Keys typed while the focus names the closing tab wait as that close's keys, in typing order. Nothing waits on the main thread.
  • When the close runs, the keys go to the terminal that has the focus after the close, through #29301's lane delivery. The target is bound when the close ends, so the code does not predict the focus reducer's choice.
  • When the warning asks the person, the keys wait for the answer outside the hold, so the dialog gets its own keys.
    • Close: the keys go to the pane that got the focus.
    • Cancel: the keys go to the terminal that stays, by its terminal id (its view's encoder, else the daemon), wherever the focus is. No timeout can drop them.
  • Only a live terminal arms a close hold (TabModel.isLiveTerminal). A cancelled creation's stand-in has no read to wait for. This is a guard; the P0 1584 fix (branch fix/p0-close-pending-split-focus) also keeps stand-ins out of the close list.
  • Test seam, DEBUG and bench builds only: debug.close.hold / debug.close.release (DebugCloseReadHold, one per DaemonService). They hold the close read, so a test or journey types inside it every time.

Moves out of the App, no behavior change, in their own commits:

  • ShortcutHintModifierPolicy and PaneNavigationHintPlan move to CmuxNextFocusUI.
  • TypeAheadQueue moves to CmuxNextFocus, also in its own commit with no behavior change.
  • They pay for the fix's App lines: app_lines −88 against #29301's head.

Testing

Red (feat + tests + hook), ref 1609450295aa9a11789eea4c765a875ef6b5a4de, fleet step 7af5d31a3e1122be227bacae:

  • aLineTypedRightAfterCmdWRunsWholeInThePaneThatGetsTheFocus FAILS: "the line ran in the closing pane (typed inside the close warning's read)".
  • aSecondCmdWDuringTheCloseReadKeepsTheKeysOutOfTheClosingTerminal FAILS: "the line ran in the closing pane (a second Cmd-W during the close read let the held keys go)".
  • keysTypedBeforeTheCloseWarningGoToThePaneThatGetsTheFocusOnClose FAILS: "the keys typed before the warning did not run whole in the pane that got the focus on Close".
  • Both cancel tests pass (guards).

Green at head c1e6dc478886d0535f4eda6244cf6d34a60c4c74 (after the merge of feat with #29301):

  • Compile 675243e05d2ad437b3d5dc9c (CmuxNext, app, cmux-tui): ok.
  • DISPLAY fleet step d7d6b94fbf6954cfb25a0d29 (gui-session-v1, console session, "display step: this session has 1 screen(s)", CMUX_TEST_REQUIRE_GUI=1, no skipped test, no FLAKY notice):
    • CloseThenTypeKeysTests 6/6
    • SecondCreationKeysTests 11/11
    • HeldKeyInputTests 3/3
    • PaneNavigationHintTests 6/6
    • TypeAheadQueueTests 2/2
    • TypeAheadDeleteTests 6/6
  • Before the merge, the same suites passed at 32fb96b8aab (step 08a46649bc2ca8634acfe635).

On-screen journey close-then-type (branch cp/journey-close-then-type 4d43e46cf26f): whole-host capture lease, app launched active, 8 tries per round. Each try holds the close read, presses Cmd-W in the focused right pane, types a line, then releases the read. The try fails unless the release resumed 1 held read.

  • Red ref (build a6922a26): 2/2 rounds FAIL (cmux19). The typed keys were handled_by "responder", so they went into the closing terminal.
  • Fix, build 0b5ca6ab of b9fbd360604: 5/5 rounds PASS (cmux19), 40/40 tries. Every key was handled_by "app" (the close hold), and every line ran whole in the left pane. That build has the same product code as this head: the last commit changes only a DEBUG seam and a test.

Review: the first review was FIX at 3da8089ff0a (a second quick Cmd-W during the read let the held keys go, plus 4 smaller findings). All five are fixed in 62a0685bc4b..32fb96b8aab. The re-review was SHIP at 32fb96b8aab (https://github.com/manaflow-ai/cmux/pull/29409#issuecomment-6546771062); the merge of feat had conflicts, so a new review covers the merged head.

Non-blocking review notes, for a follow-up:

  • While a second close still reads, the dialog's own keys can wait up to one read timeout.
  • With two questions queued, the first answer clears the parked keys of both.
  • A chained close inside one pane has no test of its own.

Observed / Not run

  • Observed: the fleet steps and journey rounds above. I read every log myself.
  • Not run:
    • The full CmuxNext suite.
    • macOS 14/15 VMs.
    • A real-person Cmd-W with the close warning dialog on screen (the dialog path is covered by the two cancel tests).

Changelog

Fixed: keys typed right after closing a terminal pane with Cmd-W now go to the pane that gets the focus, instead of the closing terminal.

No capture: keyboard routing only, no visual change. The evidence is the on-screen journey rounds above.

perf claim: none