Skip to content

fix(focus): a click on the focused pane ends the new workspace's lane; keys typed after it reach that pane - #32148

Merged
cmux-bot[bot] merged 8 commits into
feat-cmux-nextfrom
fix/creation-claim-ended-by-click
Nov 2, 2026
Merged

cmux-bot[bot] merged 8 commits into
feat-cmux-nextfrom
fix/creation-claim-ended-by-click

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

#32098 has landed; this branch is merged with feat (da9e2e6).

Lost keys (review of #32098, finding 1, #32098 (comment)). The steps:

  1. The person asks for a new workspace W. A creation hold and the person's keyboard claim open.
  2. The person clicks the pane that already has the focus.

The click ends the claim (personActed from the app's one mouse-down subscription), but it moves no focus generation: the responder does not change, and cmux-focus-core's is_noop_responder returns before the generation bump. So the keys typed after the click still took W's lane through buffer.newestGeneration == generation, and they were dropped when W never showed.

After this change, when the person's action ends their claim, the window's hold records that they moved on from its new workspaces (CreationInputBuffer.personMovedOn()). The generation that kept the lane is cleared. A key typed now passes on to the focused view at once when all three of these hold:

  • every creation of the hold is such a workspace;
  • the key is for no creation's or surface's terminal;
  • no key that runs as typed waits before it, so the order is kept.

Keys typed before the click still wait for W. The rule is in CmuxNextFocusUI. cmux-focus-core is not changed (it is an FFI input). CmuxNextApp lines: +0.

REOPEN-F4 owner (a02ba201): please look; this is the creation hold next to your #31707 surface hold.

Testing

Focused command (fleet, Mac): package-test-lane.sh suite Packages/macOS/CmuxNext PersonTerminalKeyboardClaimTests.

  • Red at 36fc468 (test only): step 983083ace1014a807c8de4b6 (aws-m4pro-7) FAILED as expected. keysTypedAfterAClickOnTheFocusedPaneReachThatPane: the keys typed after the click were lost (delivered == []), and the window still kept the keyboard for the creation.
  • Green at dbc308d: step f96929d4fe44ed3eb9985185 (aws-m4pro-8) PASSED all 22 suites. PersonTerminalKeyboardClaimTests, HomePendingTerminalFocusTests, CreationInputHoldTests and SecondCreationKeysTests passed 5 of 5 each; ColdFocusedTerminalTypeAheadTests and CloseThenTypeKeysTests passed once.
  • Compile-check of the head and of the red ref: passed.

Review FIX repair (comment 6575210178)

Two lost-key blockers, each with a red test first (red ref c800f7d):

  1. passesOn skipped the fix(focus): a reopened window's focused terminal is presented first, and keys typed before its surface wait for it (REOPEN-F4) #31707 surface hold. A cold terminal that the person clicked to, with the left workspace's hold still open, got no surface lane, so a line typed at once reached no terminal view. Fix: a hold whose every creation the person left (personLeftEveryCreation) takes the focused terminal's surface lane as no hold would. A key passes on only while responderTakesKeys holds, and no fix(focus): the reopen key hold also waits for a restored browser tab's page; Cmd-L reaches its address bar (REOPEN-F4 browser case) #32030 page is awaited (!awaitsPage; fix(focus): the reopen key hold also waits for a restored browser tab's page; Cmd-L reaches its address bar (REOPEN-F4 browser case) #32030 landed while this was open and is merged in).
  2. A creation asked for after the click (Cmd-T's non-optimistic path) got the left workspace's lane (workspaceTickets.max()), and its keys were dropped. Fix: workspaceTickets.subtracting(movedOnFrom).max().
  • Red: step 8d96ff3b52dd1be6c1e75375 (aws-m4pro-1) FAILED as expected:
    • ColdFocusedTerminalTypeAheadTests.aLineTypedAsAColdWorkspaceShowsAfterTheClickThatLeftANewWorkspaceReachesItsTerminal: the line never ran in the cold terminal.
    • PersonTerminalKeyboardClaimTests.keysTypedForACreationAfterTheClickNeverGoToTheLeftWorkspace: replayed == [].
  • Green before the feat merge: step 5b569ed981016270a1a67fc5 passed 27 suites.
  • Green at the merged head da9e2e6: step 9fc08cbe3665e6a74faef995 (aws-m4pro-9) passed all 27 suites. PersonTerminalKeyboardClaimTests, ColdFocusedTerminalTypeAheadTests, HomePendingTerminalFocusTests, CreationInputHoldTests and SecondCreationKeysTests passed 5 of 5 each, plus CloseThenTypeKeysTests and FocusedPanePresentedFirstTests. The compile passed.

Observed / Not run

Observed: the fleet steps above. Not run: a live click in a tagged build; a click that moves the focus (that path already bumps the generation and is covered by aPersonsFocusMoveEndsTheClaim).

Changelog

Fixed: Keys typed after a click on the focused pane, while a new workspace opens, reach that pane and are no longer lost.

Proof

No capture: key routing only.

…ld meets the #31707 surface hold (red)

Review of #31865 (its merge with #31707, comment 6572422551):
1. CreationInputHoldTests.keysForAPersonsNewWorkspaceTypedDuringASurfaceHoldGoToTheNewTerminal:
   a key typed into a cold terminal opens its surface hold (an adopted
   lane); the person then asks for a new workspace. CreationInputBuffer.capture
   prefers the focused (adopted) lane, so the keys typed for the new
   workspace go to the cold terminal.
2. SecondCreationKeysTests.keysHeldForTheFocusedTerminalReachItWhileAPersonsNewWorkspaceClaimHolds:
   keys held for the terminal that has the keyboard run again as typed
   when the hold ends at its limit; while the person's claim holds (the
   new workspace never showed) the claim keeps every such key that runs
   no binding, so the line is lost.

Both fail on this commit and pass with the fix.
…ring a surface hold; a replayed key held for a terminal reaches it

Review of #31865 (its merge with #31707):
1. CreationInputBuffer.capture: an adopted lane (a cold terminal's
   surface hold, keys carried from another hold) gives way to the person's
   new-workspace ticket. Keys typed after they asked for the workspace are
   its terminal's; keys typed before stay the cold terminal's.
2. CreationInputCoordinator: a replayed key that was held for a terminal
   (it runs again as typed because that terminal has the keyboard) goes to
   it; the person's keyboard claim keeps only replayed keys typed for no
   terminal. Before, a hold that ended at its limit while the new workspace
   was hidden lost the focused terminal's held line.

CmuxNextApp lines: +0 (the replay flag carries the lane in the same
lines). Regression tests: the two tests of the parent commit (red there).
…pane, not the new workspace (red)

Review of #32098 (finding 1): the person asks for a new workspace, then
clicks the pane that already has the focus. The click ends their keyboard
claim but moves no focus generation (no responder change), so the keys
typed after it still take the new workspace's lane through
`buffer.newestGeneration == generation`, and they are dropped when the
workspace never shows. Fails on this commit: b and c are held and lost.
…; the keys typed after it reach that pane

Review of #32098 (finding 1): the person asks for a new workspace, then
clicks the pane that already has the focus. The click ended their keyboard
claim, but it moves no focus generation (the responder does not change),
so the keys typed after it still took the new workspace's lane through
`buffer.newestGeneration == generation`, and were dropped when the
workspace never showed.

When the person's action ends their claim (personActed: the app's one
mouse-down subscription, or one of their actions), the window's hold
records that they moved on from its new workspaces
(CreationInputBuffer.personMovedOn): the generation that kept the lane is
cleared, and a key typed now passes on to the focused view at once when
every creation of the hold is such a workspace, the key is for no
creation's or surface's terminal, and no key that runs as typed waits
before it. Keys typed before the click still wait for the new workspace.
The rule is in CmuxNextFocusUI; CmuxNextApp lines do not rise.

Regression test: PersonTerminalKeyboardClaimTests.keysTypedAfterAClickOnTheFocusedPaneReachThatPane
(red on the parent commit).
@lawrencecchen lawrencecchen added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review da9e2e6: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr32148 -f ref=fix/creation-claim-ended-by-click -f platform=macos -f backend_mode=local

web and size checks: green

Shipped in:

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (subagent, correctness first): FIX at dbc308d

I read the diff of 36fc468 and dbc308d and the full files at the head. I ran no build or test. The fix is right for the case it tests: the person clicks the focused pane, the claim ends, personMovedOn() clears newestGeneration, and passesOn lets the keys after the click go to that pane while the keys typed before it keep the workspace lane. I found two paths where a key typed after the click is still lost.

Blockers

  1. passesOn skips the fix(focus): a reopened window's focused terminal is presented first, and keys typed before its surface wait for it (REOPEN-F4) #31707 surface hold, so a key can go to a pane that has no surface yet (REOPEN-F4 comes back).
    CreationInputCoordinator.swift:216 adds a surface lane only when no buffer exists, or when the buffer already waits for a surface (buffers[n].map { $0.awaitedTab != nil } ?? true). When the moved-on workspace buffer is still there, no surface lane is added. lane(focusedTab:) is then nil, and passesOn (CreationInputBuffer.swift:201-204, called at CreationInputCoordinator.swift:225) returns true. The key goes on to the window with no check of awaitedSurface or responderTakesKeys.
    Repro: ask for a new workspace. Within its reply plus holdLimit (after the reply the buffer stays while awaitedWorkspace != shown, up to 2 s), click another workspace in the sidebar (ActionRouting+SidebarUserIntent → personActed → personMovedOn). It shows cold, and you type at once. The first keys reach no terminal view.
    Before this PR, those keys were held (lane nil once the claim ended), and the flush waited for responderTakesKeys, which is false while the selected surface is still to come. So this is a regression.
    Fix: pass on only when nothing waits for content, for example if kind == .content, let controller, Self.awaitedSurface(controller) == nil, Self.responderTakesKeys(controller, window: shell), buffer.passesOn(...). Or let the line-216 surface check also run for a buffer whose creations are all moved-on. awaitedSurface is already the per-key check when no buffer exists, so the cost is the same. fix(focus): the reopen key hold also waits for a restored browser tab's page; Cmd-L reaches its address bar (REOPEN-F4 browser case) #32030's page hold (awaitsPage, no lane) needs the same guard once it lands: as it is, passesOn would also let the first key reach a page that does not exist yet.

  2. A creation started after the click takes the moved-on workspace's lane, and its keys are dropped.
    personMovedOn() records movedOnFrom and clears newestGeneration, but capture still picks workspaceTickets.max() (CreationInputBuffer.swift:246), and that set still holds the moved-on ticket.
    Repro: after the click, press Cmd-T. It now passes, which is new in this PR. The non-optimistic path is PaneController+Intents.swift:147, begin(in:generation:) with no tab:. That open() sets newestGeneration to the new intent again, so forNewWorkspace is true at CreationInputCoordinator.swift:224. passesOn is false (movedOnFrom.count is 1, nextID is 2), and lane(focusedTab:) is nil (no laneTabs entry for the new tab). The next keys are captured with creation = 1, the moved-on workspace.
    The new tab's intent replaces the workspace's expectation, so the workspace never shows. The forced flush's dropKeys(ofWorkspacesOtherThan:) (CreationInputCoordinator.swift:382) then drops the keys typed for the new tab.
    Fix: let workspace = forNewWorkspace ? workspaceTickets.subtracting(movedOnFrom).max() : nil. A regression test should do click, then a second creation with no tab lane, then keys, then the workspace never shows, and expect the keys in the new tab.

Checked, no finding

  • Order before and after the click: the keys typed before the click keep lane 1 (dropped if the workspace never shows, as designed, or bound to its terminal by bindNewWorkspaceKeys). A key that runs as typed and still waits (creation == nil) makes passesOn false, so later keys queue behind it in order. A hold that also has a split, a close or an adopted lane (movedOnFrom.count != nextID, or lane != nil) keeps holding as before.
  • A pending terminal's own keys: positive lanes still win in capture, and earlyLane is not affected.
  • Hold limit: passesOn is checked before holds, so a pending creation no longer keeps the keys typed after the click. The expire and forced-flush paths do not change. If the create fails before the click, personMovedOn returns early (workspaceTickets is empty), and the buffer flushes on the failed reply, so nothing is lost.
  • fix(focus): a reopened window's focused terminal is presented first, and keys typed before its surface wait for it (REOPEN-F4) #31707 surface hold with a lane: when an adopted lane names the focused tab, lane(focusedTab:) is non-nil, so passesOn is false and the key keeps its surface lane (forNewWorkspace is now false, so capture no longer gives it to the workspace). Blocker 1 is the case where no lane was added.
  • Test determinism: the assertions on b and c come from synchronous interceptKeyDown returns. until is a polled condition with a 20 s deadline, and either flush path (on the failed reply, or the 100 ms limit) gives the same delivered. The result does not depend on timing. (The harness's Task.sleep polling was there before this PR.) The test covers only a failed workspace with a no-op click. It covers neither a workspace that lands nor a second creation.
  • Main-thread cost per key: passesOn is O(lanes in the hold + held keys) and runs only while a buffer exists. It does no O(session) walk, no I/O and no wait, so it is well inside the 10 ms budget. The guard from Blocker 1 adds the same awaitedSurface check the no-buffer path already runs on each key.

@blacksmith-sh

This comment has been minimized.

…a cold terminal and go to a later creation (red)

Review of #32148 (comment 6575210178), two lost-key blockers:
(a) ColdFocusedTerminalTypeAheadTests.aLineTypedAsAColdWorkspaceShowsAfterTheClickThatLeftANewWorkspaceReachesItsTerminal:
    the person's new workspace's hold is open and their click ended its
    claim as another workspace shows cold; a line typed at once must wait
    for the focused terminal's surface and run there. With passesOn the
    keys go on to the window, where no terminal view has them.
(b) PersonTerminalKeyboardClaimTests.keysTypedForACreationAfterTheClickNeverGoToTheLeftWorkspace:
    after the click, a creation with no tab before its reply (Cmd-T's
    non-optimistic path) takes the next keys; capture gave them the left
    workspace's lane (workspaceTickets.max()), and they were dropped when
    it never showed.
…l keeps its keys and a later creation gets its own

Review of #32148 (comment 6575210178), two lost-key blockers:
1. passesOn skipped the REOPEN-F4 surface hold (#31707): with the left
   workspace's hold still open, a cold terminal that the person clicked
   to got no surface lane, and the keys typed at once went on to a window
   where no terminal view had them. Now a hold whose every creation the
   person left (CreationInputBuffer.personLeftEveryCreation) takes the
   focused terminal's surface lane as no hold would, and a key passes on
   only while the focused view takes keys (responderTakesKeys; a remount
   waits).
2. A creation asked for after the click (Cmd-T's non-optimistic path,
   no tab before its reply) set the hold's generation again, so capture
   gave its keys the left workspace's lane (workspaceTickets.max()), and
   they were dropped when that workspace never showed. capture now never
   picks a workspace the person left
   (workspaceTickets.subtracting(movedOnFrom)).

#32030's browser page hold, once it lands, needs the same rule (a page
still to show keeps the key); its owner is told.

Regression tests (red on the parent commit):
ColdFocusedTerminalTypeAheadTests.aLineTypedAsAColdWorkspaceShowsAfterTheClickThatLeftANewWorkspaceReachesItsTerminal,
PersonTerminalKeyboardClaimTests.keysTypedForACreationAfterTheClickNeverGoToTheLeftWorkspace.
…n-claim-ended-by-click

# Conflicts:
#	Packages/macOS/CmuxNext/Sources/CmuxNextApp/Focus/CreationInputCoordinator.swift
#	Packages/macOS/CmuxNext/Sources/CmuxNextFocusUI/CreationInputBuffer.swift
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (review-32148-correctness): SHIP at da9e2e6

Correctness review of the full head (CreationInputBuffer, CreationInputCoordinator, CreationKeyboardClaims, both test files), read in a worktree at da9e2e6. No compile or test run by me; I read the fleet logs.

Summary:

  • Both prior blockers are fixed. (1) With every creation left, capture now opens the fix(focus): a reopened window's focused terminal is presented first, and keys typed before its surface wait for it (REOPEN-F4) #31707 surface hold for a cold focused terminal (line 219), so the key gets the adopted lane and passesOn returns false; responderTakesKeys is a second guard. (2) workspaceTickets.subtracting(movedOnFrom).max() keeps a later creation's keys out of the left workspace's lane.
  • Lost keys: none found. A key passes on only with no lane for the focused tab, no fix(focus): the reopen key hold also waits for a restored browser tab's page; Cmd-L reaches its address bar (REOPEN-F4 browser case) #32030 page awaited, no nil-lane key waiting before it (order), and a responder that takes keys. A close, split or new tab after the click opens a ticket, so personLeftEveryCreation goes false and the old rules apply.
  • Held forever: no. The PR adds no new hold lifetime. A surface await opened inside an existing hold ends with that hold's expire task (armed at creation and at each reply), or at the request timeout while a reply is pending, as before. When the window closes, flush drops the buffer as before.
  • movedOnFrom is bounded: it is a subset of the ticket ids 1...nextID of one buffer, formUnion is idempotent, and the buffer is released when the hold ends.
  • Main thread: the new checks are O(held keys + lanes) per key, with no blocking.
  • Evidence matches the description. Red step 8d96ff3b52dd1be6c1e75375 at c800f7d: the cold-terminal test failed (line never ran), and keysTypedForACreationAfterTheClickNeverGoToTheLeftWorkspace failed (replayed == []). Green step 9fc08cbe3665e6a74faef995 at the head: 27/27 suites passed, and each of the 3 new tests passed 5/5.

Non-blocking notes (not regressions, base behavior):

  1. awaitedWorkspace still names a workspace the person left. When it replied and never shows, the hold (cold-terminal lane keys, nil-lane keys, and a later Cmd-T's keys) ends only at holdLimit (2 s after the last reply). These keys are delayed, not lost. If you skip left tickets there, you must decide what happens to the keys typed for W before the click.
  2. If one workspace ticket of the hold failed before the click, it is not in workspaceTickets, so movedOnFrom.count != nextID and passesOn never holds for that buffer. The keys typed after the click then wait as nil-lane keys and run as typed at the end of the hold. If a second workspace then shows, they go into it. This needs a narrow sequence (two new workspaces in one hold, one fails, then a click).
  3. personMovedOn runs only when personActed ends the claim. A claim that ends in holds() (the person's focus-move count changed) does not mark the tickets as left. I found no person path that does not call personActed first (mouse-down observer, willRun for .user), so this note is only about robustness.

@lawrencecchen lawrencecchen added the ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds label Nov 2, 2026
lawrencecchen added a commit that referenced this pull request Nov 2, 2026
…e keys after the click go on to the clicked pane

Review of #32148 (comment 6576859579), note 2: a new-workspace creation
whose create failed left its id in the hold's count but not in its
workspace tickets, so after the person's click the hold never counted as
left (personLeftEveryCreation), and the keys typed after the click waited
without a lane, to run as typed when the hold ended (into a second new
workspace if that one showed). A failed creation is now recorded as over
(failedWorkspaces) and counts as released; the person's action is still
needed (their claim keeps the keys until then).

Regression test: PersonTerminalKeyboardClaimTests.keysTypedAfterTheClickPassOnWhenAnEarlierNewWorkspaceFailed
(red on the parent commit).
lawrencecchen added a commit that referenced this pull request Nov 2, 2026
… creation's keys (red)

Review of #32148 (comment 6576859579), note 1: the person's new workspace
replied but the window never shows it; the person clicks, asks for
another creation and types. awaitedWorkspace still names the left
workspace, so the flush waits until the hold's limit (2 s by default; 8 s
in the test) before the creation's keys run. Fails on this commit: the
keys do not run within 2 s.
lawrencecchen added a commit that referenced this pull request Nov 2, 2026
…t never shows

Review of #32148 (comment 6576859579), note 1: the hold's flush waited for
the window to show the newest new workspace (awaitedWorkspace) even after
the person left it with a click. For a workspace that replied and never
shows, the keys of a later creation (Cmd-T), and a cold terminal's lane
keys, waited until the hold's limit (2 s). The flush now waits only for a
workspace the person did not leave (shownWorkspaceToWaitFor). The keys
typed for the left workspace before the click go with it when the hold
ends: they run there if it shows by then, else they are dropped, as at
the limit before.

Regression test: PersonTerminalKeyboardClaimTests.aLeftWorkspaceThatNeverShowsDoesNotDelayALaterCreationsKeys
(red on the parent commit).
@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 2, 2026 12:27 Inactive
@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 2, 2026 12:27 Inactive
@blacksmith-sh

blacksmith-sh Bot commented Nov 2, 2026

Copy link
Copy Markdown

Found 1 test failure on Blacksmith runners:

Failure

Test View Logs
test_the_nightly_is_covered (main.NightlyStepCoverageTests.test_the_nightly_is_cove
red)/
test_the_nightly_is_covered (main.NightlyStepCoverageTests.test_the_nightly_is_cove
red)
View Logs

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@cmux-bot
cmux-bot Bot merged commit b78ba25 into feat-cmux-next Nov 2, 2026
148 of 152 checks passed
@cmux-bot
cmux-bot Bot deleted the fix/creation-claim-ended-by-click branch November 2, 2026 12:39
cmux-bot Bot added a commit that referenced this pull request Nov 2, 2026
… releases the wait for a left workspace (#32352)

* test(focus): two key-routing gaps where the person's new-workspace hold meets the #31707 surface hold (red)

Review of #31865 (its merge with #31707, comment 6572422551):
1. CreationInputHoldTests.keysForAPersonsNewWorkspaceTypedDuringASurfaceHoldGoToTheNewTerminal:
   a key typed into a cold terminal opens its surface hold (an adopted
   lane); the person then asks for a new workspace. CreationInputBuffer.capture
   prefers the focused (adopted) lane, so the keys typed for the new
   workspace go to the cold terminal.
2. SecondCreationKeysTests.keysHeldForTheFocusedTerminalReachItWhileAPersonsNewWorkspaceClaimHolds:
   keys held for the terminal that has the keyboard run again as typed
   when the hold ends at its limit; while the person's claim holds (the
   new workspace never showed) the claim keeps every such key that runs
   no binding, so the line is lost.

Both fail on this commit and pass with the fix.

* fix(focus): the person's new workspace takes the keys typed for it during a surface hold; a replayed key held for a terminal reaches it

Review of #31865 (its merge with #31707):
1. CreationInputBuffer.capture: an adopted lane (a cold terminal's
   surface hold, keys carried from another hold) gives way to the person's
   new-workspace ticket. Keys typed after they asked for the workspace are
   its terminal's; keys typed before stay the cold terminal's.
2. CreationInputCoordinator: a replayed key that was held for a terminal
   (it runs again as typed because that terminal has the keyboard) goes to
   it; the person's keyboard claim keeps only replayed keys typed for no
   terminal. Before, a hold that ended at its limit while the new workspace
   was hidden lost the focused terminal's held line.

CmuxNextApp lines: +0 (the replay flag carries the lane in the same
lines). Regression tests: the two tests of the parent commit (red there).

* test(focus): keys typed after a click on the focused pane reach that pane, not the new workspace (red)

Review of #32098 (finding 1): the person asks for a new workspace, then
clicks the pane that already has the focus. The click ends their keyboard
claim but moves no focus generation (no responder change), so the keys
typed after it still take the new workspace's lane through
`buffer.newestGeneration == generation`, and they are dropped when the
workspace never shows. Fails on this commit: b and c are held and lost.

* fix(focus): a click on the focused pane ends the new workspace's lane; the keys typed after it reach that pane

Review of #32098 (finding 1): the person asks for a new workspace, then
clicks the pane that already has the focus. The click ended their keyboard
claim, but it moves no focus generation (the responder does not change),
so the keys typed after it still took the new workspace's lane through
`buffer.newestGeneration == generation`, and were dropped when the
workspace never showed.

When the person's action ends their claim (personActed: the app's one
mouse-down subscription, or one of their actions), the window's hold
records that they moved on from its new workspaces
(CreationInputBuffer.personMovedOn): the generation that kept the lane is
cleared, and a key typed now passes on to the focused view at once when
every creation of the hold is such a workspace, the key is for no
creation's or surface's terminal, and no key that runs as typed waits
before it. Keys typed before the click still wait for the new workspace.
The rule is in CmuxNextFocusUI; CmuxNextApp lines do not rise.

Regression test: PersonTerminalKeyboardClaimTests.keysTypedAfterAClickOnTheFocusedPaneReachThatPane
(red on the parent commit).

* test(focus): keys after the click that left a new workspace wait for a cold terminal and go to a later creation (red)

Review of #32148 (comment 6575210178), two lost-key blockers:
(a) ColdFocusedTerminalTypeAheadTests.aLineTypedAsAColdWorkspaceShowsAfterTheClickThatLeftANewWorkspaceReachesItsTerminal:
    the person's new workspace's hold is open and their click ended its
    claim as another workspace shows cold; a line typed at once must wait
    for the focused terminal's surface and run there. With passesOn the
    keys go on to the window, where no terminal view has them.
(b) PersonTerminalKeyboardClaimTests.keysTypedForACreationAfterTheClickNeverGoToTheLeftWorkspace:
    after the click, a creation with no tab before its reply (Cmd-T's
    non-optimistic path) takes the next keys; capture gave them the left
    workspace's lane (workspaceTickets.max()), and they were dropped when
    it never showed.

* fix(focus): after the click that left a new workspace, a cold terminal keeps its keys and a later creation gets its own

Review of #32148 (comment 6575210178), two lost-key blockers:
1. passesOn skipped the REOPEN-F4 surface hold (#31707): with the left
   workspace's hold still open, a cold terminal that the person clicked
   to got no surface lane, and the keys typed at once went on to a window
   where no terminal view had them. Now a hold whose every creation the
   person left (CreationInputBuffer.personLeftEveryCreation) takes the
   focused terminal's surface lane as no hold would, and a key passes on
   only while the focused view takes keys (responderTakesKeys; a remount
   waits).
2. A creation asked for after the click (Cmd-T's non-optimistic path,
   no tab before its reply) set the hold's generation again, so capture
   gave its keys the left workspace's lane (workspaceTickets.max()), and
   they were dropped when that workspace never showed. capture now never
   picks a workspace the person left
   (workspaceTickets.subtracting(movedOnFrom)).

#32030's browser page hold, once it lands, needs the same rule (a page
still to show keeps the key); its owner is told.

Regression tests (red on the parent commit):
ColdFocusedTerminalTypeAheadTests.aLineTypedAsAColdWorkspaceShowsAfterTheClickThatLeftANewWorkspaceReachesItsTerminal,
PersonTerminalKeyboardClaimTests.keysTypedForACreationAfterTheClickNeverGoToTheLeftWorkspace.

* test(focus): after a failed new workspace and a click, keys go on to the clicked pane (red)

Review of #32148 (comment 6576859579), note 2: two new workspaces in one
hold, the first one's create fails, then the person clicks the focused
pane. The failed ticket is no longer a workspace ticket but still counts
in nextID, so personLeftEveryCreation stays false: the keys typed after
the click wait without a lane and run as typed when the hold ends, into
the second workspace if it shows. Fails on this commit: b and c are held.

* fix(focus): a failed new workspace releases its ticket at once, so the keys after the click go on to the clicked pane

Review of #32148 (comment 6576859579), note 2: a new-workspace creation
whose create failed left its id in the hold's count but not in its
workspace tickets, so after the person's click the hold never counted as
left (personLeftEveryCreation), and the keys typed after the click waited
without a lane, to run as typed when the hold ended (into a second new
workspace if that one showed). A failed creation is now recorded as over
(failedWorkspaces) and counts as released; the person's action is still
needed (their claim keeps the keys until then).

Regression test: PersonTerminalKeyboardClaimTests.keysTypedAfterTheClickPassOnWhenAnEarlierNewWorkspaceFailed
(red on the parent commit).

* test(focus): a left workspace that never shows does not delay a later creation's keys (red)

Review of #32148 (comment 6576859579), note 1: the person's new workspace
replied but the window never shows it; the person clicks, asks for
another creation and types. awaitedWorkspace still names the left
workspace, so the flush waits until the hold's limit (2 s by default; 8 s
in the test) before the creation's keys run. Fails on this commit: the
keys do not run within 2 s.

* fix(focus): a click releases the hold's wait for a left workspace that never shows

Review of #32148 (comment 6576859579), note 1: the hold's flush waited for
the window to show the newest new workspace (awaitedWorkspace) even after
the person left it with a click. For a workspace that replied and never
shows, the keys of a later creation (Cmd-T), and a cold terminal's lane
keys, waited until the hold's limit (2 s). The flush now waits only for a
workspace the person did not leave (shownWorkspaceToWaitFor). The keys
typed for the left workspace before the click go with it when the hold
ends: they run there if it shows by then, else they are dropped, as at
the limit before.

Regression test: PersonTerminalKeyboardClaimTests.aLeftWorkspaceThatNeverShowsDoesNotDelayALaterCreationsKeys
(red on the parent commit).

* test(focus): the failed-workspace rule (note 2) is tested where it lives, in CreationInputBuffer

The App-level test could not tell the fix from the bug: the test window's
focused terminal has no surface, so after the click its keys take that
terminal's surface lane (REOPEN-F4) and replay at the hold's end, which is
the right target, and before the fix they also replayed (as typed). The
rule itself is CreationInputBuffer's: CreationInputHoldTests.aFailedNewWorkspaceCountsAsLeftOnceThePersonActs
checks personLeftEveryCreation and passesOn. Red on the base before the
fix (red ref branch fix/creation-hold-left-workspace-release-red, never
merge).

* test(focus): a line typed for a new workspace before the click runs there when it shows a frame after the reply (red)

Review of #32352 (comment 6580371831), blocker: Cmd-N, type, click, then
the create's reply while the window does not show the workspace yet; the
store applies daemon events once per frame, so it shows one frame later.
The flush no longer waits for a workspace the person left
(shownWorkspaceToWaitFor), so in the reply's turn it dropped the line
(dropKeys(ofWorkspacesOtherThan:)). Fails on this commit: the line never
runs in the new workspace's terminal.

* fix(focus): a left new workspace's own keys wait for it to show with its terminal; the other lanes' keys go now

Review of #32352 (comment 6580371831), blocker: the flush no longer waited
for a new workspace the person left (note 1), so the keys typed for it
before the click were dropped in the create reply's turn. The store
applies daemon events once per frame, so the window usually shows the
new workspace a frame after the reply.

Now a left workspace that replied and still holds keys typed for it
(CreationInputBuffer.leftWorkspaceWithKeys) keeps only those keys
waiting: until the window shows it with its terminal (then they are bound
to its first terminal and run there), or until the hold's limit (then
they are dropped, as before). The keys of every other lane run at once
(take(focused:keeping:)), so note 1's gain stays: a left workspace with
no keys of its own keeps nothing waiting. Docs of open(...) and
personMovedOn() and shownWorkspaceToWaitFor say so.

Regression test: ColdFocusedTerminalTypeAheadTests.aLineTypedForANewWorkspaceBeforeTheClickRunsThereWhenItShowsAFrameAfterTheReply
(red on the parent commit).

---------

Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
cmux-bot Bot pushed a commit that referenced this pull request Nov 2, 2026
…'s keys (#32478)

* test(focus): two key-routing gaps where the person's new-workspace hold meets the #31707 surface hold (red)

Review of #31865 (its merge with #31707, comment 6572422551):
1. CreationInputHoldTests.keysForAPersonsNewWorkspaceTypedDuringASurfaceHoldGoToTheNewTerminal:
   a key typed into a cold terminal opens its surface hold (an adopted
   lane); the person then asks for a new workspace. CreationInputBuffer.capture
   prefers the focused (adopted) lane, so the keys typed for the new
   workspace go to the cold terminal.
2. SecondCreationKeysTests.keysHeldForTheFocusedTerminalReachItWhileAPersonsNewWorkspaceClaimHolds:
   keys held for the terminal that has the keyboard run again as typed
   when the hold ends at its limit; while the person's claim holds (the
   new workspace never showed) the claim keeps every such key that runs
   no binding, so the line is lost.

Both fail on this commit and pass with the fix.

* fix(focus): the person's new workspace takes the keys typed for it during a surface hold; a replayed key held for a terminal reaches it

Review of #31865 (its merge with #31707):
1. CreationInputBuffer.capture: an adopted lane (a cold terminal's
   surface hold, keys carried from another hold) gives way to the person's
   new-workspace ticket. Keys typed after they asked for the workspace are
   its terminal's; keys typed before stay the cold terminal's.
2. CreationInputCoordinator: a replayed key that was held for a terminal
   (it runs again as typed because that terminal has the keyboard) goes to
   it; the person's keyboard claim keeps only replayed keys typed for no
   terminal. Before, a hold that ended at its limit while the new workspace
   was hidden lost the focused terminal's held line.

CmuxNextApp lines: +0 (the replay flag carries the lane in the same
lines). Regression tests: the two tests of the parent commit (red there).

* test(focus): keys typed after a click on the focused pane reach that pane, not the new workspace (red)

Review of #32098 (finding 1): the person asks for a new workspace, then
clicks the pane that already has the focus. The click ends their keyboard
claim but moves no focus generation (no responder change), so the keys
typed after it still take the new workspace's lane through
`buffer.newestGeneration == generation`, and they are dropped when the
workspace never shows. Fails on this commit: b and c are held and lost.

* fix(focus): a click on the focused pane ends the new workspace's lane; the keys typed after it reach that pane

Review of #32098 (finding 1): the person asks for a new workspace, then
clicks the pane that already has the focus. The click ended their keyboard
claim, but it moves no focus generation (the responder does not change),
so the keys typed after it still took the new workspace's lane through
`buffer.newestGeneration == generation`, and were dropped when the
workspace never showed.

When the person's action ends their claim (personActed: the app's one
mouse-down subscription, or one of their actions), the window's hold
records that they moved on from its new workspaces
(CreationInputBuffer.personMovedOn): the generation that kept the lane is
cleared, and a key typed now passes on to the focused view at once when
every creation of the hold is such a workspace, the key is for no
creation's or surface's terminal, and no key that runs as typed waits
before it. Keys typed before the click still wait for the new workspace.
The rule is in CmuxNextFocusUI; CmuxNextApp lines do not rise.

Regression test: PersonTerminalKeyboardClaimTests.keysTypedAfterAClickOnTheFocusedPaneReachThatPane
(red on the parent commit).

* test(focus): keys after the click that left a new workspace wait for a cold terminal and go to a later creation (red)

Review of #32148 (comment 6575210178), two lost-key blockers:
(a) ColdFocusedTerminalTypeAheadTests.aLineTypedAsAColdWorkspaceShowsAfterTheClickThatLeftANewWorkspaceReachesItsTerminal:
    the person's new workspace's hold is open and their click ended its
    claim as another workspace shows cold; a line typed at once must wait
    for the focused terminal's surface and run there. With passesOn the
    keys go on to the window, where no terminal view has them.
(b) PersonTerminalKeyboardClaimTests.keysTypedForACreationAfterTheClickNeverGoToTheLeftWorkspace:
    after the click, a creation with no tab before its reply (Cmd-T's
    non-optimistic path) takes the next keys; capture gave them the left
    workspace's lane (workspaceTickets.max()), and they were dropped when
    it never showed.

* fix(focus): after the click that left a new workspace, a cold terminal keeps its keys and a later creation gets its own

Review of #32148 (comment 6575210178), two lost-key blockers:
1. passesOn skipped the REOPEN-F4 surface hold (#31707): with the left
   workspace's hold still open, a cold terminal that the person clicked
   to got no surface lane, and the keys typed at once went on to a window
   where no terminal view had them. Now a hold whose every creation the
   person left (CreationInputBuffer.personLeftEveryCreation) takes the
   focused terminal's surface lane as no hold would, and a key passes on
   only while the focused view takes keys (responderTakesKeys; a remount
   waits).
2. A creation asked for after the click (Cmd-T's non-optimistic path,
   no tab before its reply) set the hold's generation again, so capture
   gave its keys the left workspace's lane (workspaceTickets.max()), and
   they were dropped when that workspace never showed. capture now never
   picks a workspace the person left
   (workspaceTickets.subtracting(movedOnFrom)).

#32030's browser page hold, once it lands, needs the same rule (a page
still to show keeps the key); its owner is told.

Regression tests (red on the parent commit):
ColdFocusedTerminalTypeAheadTests.aLineTypedAsAColdWorkspaceShowsAfterTheClickThatLeftANewWorkspaceReachesItsTerminal,
PersonTerminalKeyboardClaimTests.keysTypedForACreationAfterTheClickNeverGoToTheLeftWorkspace.

* test(focus): after a failed new workspace and a click, keys go on to the clicked pane (red)

Review of #32148 (comment 6576859579), note 2: two new workspaces in one
hold, the first one's create fails, then the person clicks the focused
pane. The failed ticket is no longer a workspace ticket but still counts
in nextID, so personLeftEveryCreation stays false: the keys typed after
the click wait without a lane and run as typed when the hold ends, into
the second workspace if it shows. Fails on this commit: b and c are held.

* fix(focus): a failed new workspace releases its ticket at once, so the keys after the click go on to the clicked pane

Review of #32148 (comment 6576859579), note 2: a new-workspace creation
whose create failed left its id in the hold's count but not in its
workspace tickets, so after the person's click the hold never counted as
left (personLeftEveryCreation), and the keys typed after the click waited
without a lane, to run as typed when the hold ended (into a second new
workspace if that one showed). A failed creation is now recorded as over
(failedWorkspaces) and counts as released; the person's action is still
needed (their claim keeps the keys until then).

Regression test: PersonTerminalKeyboardClaimTests.keysTypedAfterTheClickPassOnWhenAnEarlierNewWorkspaceFailed
(red on the parent commit).

* test(focus): a left workspace that never shows does not delay a later creation's keys (red)

Review of #32148 (comment 6576859579), note 1: the person's new workspace
replied but the window never shows it; the person clicks, asks for
another creation and types. awaitedWorkspace still names the left
workspace, so the flush waits until the hold's limit (2 s by default; 8 s
in the test) before the creation's keys run. Fails on this commit: the
keys do not run within 2 s.

* fix(focus): a click releases the hold's wait for a left workspace that never shows

Review of #32148 (comment 6576859579), note 1: the hold's flush waited for
the window to show the newest new workspace (awaitedWorkspace) even after
the person left it with a click. For a workspace that replied and never
shows, the keys of a later creation (Cmd-T), and a cold terminal's lane
keys, waited until the hold's limit (2 s). The flush now waits only for a
workspace the person did not leave (shownWorkspaceToWaitFor). The keys
typed for the left workspace before the click go with it when the hold
ends: they run there if it shows by then, else they are dropped, as at
the limit before.

Regression test: PersonTerminalKeyboardClaimTests.aLeftWorkspaceThatNeverShowsDoesNotDelayALaterCreationsKeys
(red on the parent commit).

* test(focus): the failed-workspace rule (note 2) is tested where it lives, in CreationInputBuffer

The App-level test could not tell the fix from the bug: the test window's
focused terminal has no surface, so after the click its keys take that
terminal's surface lane (REOPEN-F4) and replay at the hold's end, which is
the right target, and before the fix they also replayed (as typed). The
rule itself is CreationInputBuffer's: CreationInputHoldTests.aFailedNewWorkspaceCountsAsLeftOnceThePersonActs
checks personLeftEveryCreation and passesOn. Red on the base before the
fix (red ref branch fix/creation-hold-left-workspace-release-red, never
merge).

* test(focus): a line typed for a new workspace before the click runs there when it shows a frame after the reply (red)

Review of #32352 (comment 6580371831), blocker: Cmd-N, type, click, then
the create's reply while the window does not show the workspace yet; the
store applies daemon events once per frame, so it shows one frame later.
The flush no longer waits for a workspace the person left
(shownWorkspaceToWaitFor), so in the reply's turn it dropped the line
(dropKeys(ofWorkspacesOtherThan:)). Fails on this commit: the line never
runs in the new workspace's terminal.

* fix(focus): a left new workspace's own keys wait for it to show with its terminal; the other lanes' keys go now

Review of #32352 (comment 6580371831), blocker: the flush no longer waited
for a new workspace the person left (note 1), so the keys typed for it
before the click were dropped in the create reply's turn. The store
applies daemon events once per frame, so the window usually shows the
new workspace a frame after the reply.

Now a left workspace that replied and still holds keys typed for it
(CreationInputBuffer.leftWorkspaceWithKeys) keeps only those keys
waiting: until the window shows it with its terminal (then they are bound
to its first terminal and run there), or until the hold's limit (then
they are dropped, as before). The keys of every other lane run at once
(take(focused:keeping:)), so note 1's gain stays: a left workspace with
no keys of its own keeps nothing waiting. Docs of open(...) and
personMovedOn() and shownWorkspaceToWaitFor say so.

Regression test: ColdFocusedTerminalTypeAheadTests.aLineTypedForANewWorkspaceBeforeTheClickRunsThereWhenItShowsAFrameAfterTheReply
(red on the parent commit).

* test(focus): a shown second new workspace ends the wait for a left one's keys (red)

Review of #32352 (comment 6581603361), note 2: Cmd-N (W1), type, click,
Cmd-N (W2). When the window shows W2 with its terminal, W1's kept keys
(keysKeptForLeftWorkspace) still kept the hold open until its limit, so
keys typed into W2's shown terminal waited up to 2 s; before #32352 the
hold ended there and W1's keys were dropped. Fails on this commit.

* fix(focus): a shown second new workspace ends the wait for a left one's keys

Review of #32352 (comment 6581603361), note 2: Cmd-N (W1), type, click,
Cmd-N (W2). Once the window shows W2 (the workspace the person did not
leave), keysKeptForLeftWorkspace returns nil: the hold ends there as it
did before #32352, so the keys typed into W2 go on at once, and W1's
kept keys are dropped with the hold (its workspace is not the one shown),
as before. While W2 has not shown, W1's own keys still wait for W1.

Regression test: CreationInputHoldTests.aShownSecondWorkspaceEndsTheWaitForALeftOnesKeys
(red on the parent commit).

This branch was previously deployed

1 inactive deployment
ci-routing-agent — 8856f8bd Deployed Nov 2, 2026 by cmux-bot[bot] via cmux-tui tree preflight #101169
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-land Reviewed at this head; next-autoland merges it into feat-cmux-next when its only reds are base reds risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant