Skip to content

Focus: a focus choice the person makes from a menu counts as their move (the reply bound drops a held line after it) - #35874

Merged
lawrencecchen merged 5 commits into
feat-cmux-nextfrom
fix/r-input-menu-pick-person-move
Nov 4, 2026
Merged

lawrencecchen merged 5 commits into
feat-cmux-nextfrom
fix/r-input-menu-pick-person-move

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

REWRITE R-INPUT. A focus choice that the person makes from a menu now counts as their move (FocusState.personMoveSeq), so #35275's reply bound no longer types a held line into the press-time terminal after the person moved the focus from a menu.

Recorded first, before any code change (lead order after #35275 landed), on FEAT DOGFOOD BUILD 7dd31211 (e60197e), cmux18, whole host: real desktop mouse clicks (CuaDriver) on the menu bar's View and then on Focus Right Sidebar. The action ran (focusRightSidebar, committed, action-log entrypoint unattributed), and the window's only new focus event was responder(.textField, source: .programmatic). AppKit runs a chosen menu item's action after the menu's tracking, outside any input event's dispatch, so FocusEvent.Source.current is programmatic and a handler's .intent is the CLI's: neither counts as the person's choice. (STATUS 12884; run mm4-e60197eb-20261104T210106Z-cmux18.)

What changes:

  • ActionMenuTarget.performAction marks a run the person chose from an open menu (the main menu or a context menu) outside a key's dispatch: ActionInvocation.menuPick, carried by ActionRunScope and kept by the runs nested in it. A key equivalent is not marked: its own key dispatch names the person, and Creation hold: a daemon reply keeps the window's keys at most 10 s (its own bound), and no hold end drops a key (cmux-ad m12) #35275's round-10 rule (a key's responder change is no focus choice) stays.
  • FocusMachine.send counts a focus choice of such a run as the person's move (FocusEvent.choosesFocusInMenuPick): any focusPane, selectTab or focusTarget, and a responder report that no key's dispatch made (its source is not the keyboard's). FocusCoordinator passes the current run's mark. A run started inside a menu pick keeps the mark (ActionRunScope.nested); a held key that a creation hold runs again inside the menu run's task is judged by its source, so a responder change with the keyboard's source is no focus choice (Creation hold: a daemon reply keeps the window's keys at most 10 s (its own bound), and no hold end drops a key (cmux-ad m12) #35275, round 10). This only narrows that case (review round 2, finding 2): see Known limits.
  • No source that the focus core reads changes (its drag and sidebar-list rules read sources), so the reducer behaves as before. No FFI change.

Tests

MenuPickPersonMoveTests (Packages/macOS/CmuxNext), red first (b7b37b6 on feat 4a55412):

  • aFocusMoveChosenFromAMenuCountsAsThePersons: Focus Next Pane's menu item, its action sent as AppKit sends a chosen item's (outside any dispatch), moves the focus and must count as the person's.
  • theSameMoveFromTheCLIDoesNotCount: the same move from the CLI (focus: true) must not count.
  • aResponderMoveChosenFromAMenuCountsAsThePersons (round 2, the recorded case): View > Focus Right Sidebar's menu item; the sidebar's search field reports as the first responder with the app's source, and the count must move.
  • aKeyEquivalentRunIsNoMenuPick (round 2): the same menu path run from a key-down's dispatch moves the focus and must not count.
  • FocusPersonMovesTests.aMenuPicksChoicesCountButAKeysResponderReportDoesNot (round 2): under the mark, a responder report with the app's source and a CLI move count; a responder report with the keyboard's source does not.

Compile checks pass: 5db465d9 (head 0e5a693), 039b513a (red b7b37b6). Red: step 2e11c8579ee1265d870d4edc (MenuPickPersonMoveTests --red at b7b37b6, aws-m4pro-3): exactly the new case fails, aFocusMoveChosenFromAMenuCountsAsThePersons at MenuPickPersonMoveTests.swift:64 ("(personMoveSeq → 0) != (before → 0)": the focus moved, the count did not); theSameMoveFromTheCLIDoesNotCount passes. Green: step 47ce629abd6857411d6ab30d at 0e5a693 (aws-m4pro-6): all 7 suites pass (MenuPickPersonMoveTests, FocusPersonMovesTests, CreationHoldReplyBoundTests, CreationHoldReplyBoundRuleTests, PersonTerminalKeyboardClaimTests, MenuKeyRoutingTests, FocusWaitLeavesPageTests).

Merged tree: this head merged with feat b5e96a1 (after #35875, which moved FocusMachine.send onto cmux_input_*) on a never-merge ref, 61c9441: compile check 45d1cc24 passes, and step de999219fd818d2c35a92883 (aws-m4pro-7) passes all 10 suites (the 8 above plus FocusInputSharedRouterTests and MultiWindowInputRoutingTests). Review round 2: SHIP at 8b2b51e (#35874 (comment)).

Review round 1 (FIX at 0e5a693, #35874 (comment)): (1) the test did not cover the recorded responder-report case; (2) the mark also covered a held key run again inside the menu run's task; (3) closure-based menus get no mark; (4) two docs were out of date. Round 2: tests 3a3a39d and 0e213dd (red), fix 8b2b51e; (3) is a known limit with the reason (above). Compile checks pass: a16a09d2 (head 8b2b51e), 5eae1837 (red A 0e213dd), 6d24648b (red B 543fe01, a never-merge branch: feat plus the menu tests, no fix). Red A: step e365d6c0e52765fde7905357 (FocusPersonMovesTests at 0e213dd, the new key-report test on the round-1 fix, aws-m4pro-7): exactly the new case fails, aMenuPicksChoicesCountButAKeysResponderReportDoesNot at FocusPersonMovesTests.swift:95 ("(state.personMoveSeq → 3) == 2": a key's responder report inside a menu pick counted); the 5 other cases pass. Red B: step 73dd57404556ebbb5473b359 (MenuPickPersonMoveTests at 543fe01, aws-m4pro-9): both menu cases fail, aFocusMoveChosenFromAMenuCountsAsThePersons (:68) and aResponderMoveChosenFromAMenuCountsAsThePersons (:85, the recorded responder-report case), "(personMoveSeq → 0) != (before → 0)"; aKeyEquivalentRunIsNoMenuPick and theSameMoveFromTheCLIDoesNotCount pass. Green: step 9e35de97eef1b32c535ab36f at 8b2b51e (aws-m4pro-9): all 8 suites pass (MenuPickPersonMoveTests, FocusPersonMovesTests, CreationHoldReplyBoundTests, CreationHoldReplyBoundRuleTests, PersonTerminalKeyboardClaimTests, MenuKeyRoutingTests, FocusWaitLeavesPageTests, RightSidebarTests).

Known limits

  • An accessibility press on a menu item (a computer-use agent) or debug.menu_item also counts: ActionOrigin names menu runs the user's. It fails safe: held keys are dropped and logged, never typed into a wrong place.
  • Menus whose items call their own target, not the action registry (the Dock menu, Home's menus HomeMenuTarget, the browser context menus ClosureMenuTarget, the titlebar history menu, the remote toolbar menu), are not marked: a focus move chosen there still does not count. Not taken here: an override of CmuxApplication.sendAction(_:to:from:) that binds ActionRunScope for every menu item would make every registry menu run a nested run, so the top-level-run rule of ActionRouting+SidebarUserIntent (the person's next intent ends their new terminal's keyboard claim) would stop firing for a menu pick. A plain flag on CmuxApplication, set while sendAction runs with an NSMenuItem sender and no key-down is dispatching (as handlingSendEvent is), has neither problem; it is the likely shape of the fix, and it belongs to R-INPUT S8 (every entrypoint carries its origin into the router). (Round 1 also named the macOS 14 task-local crash class of TaskLocal+InTask.swift as a reason; review round 2 showed that it does not apply: below macOS 15.4 withValueInTask starts no task, and every registry run already uses it.)
  • A held key that a creation hold runs again when its reply bound or its limit ends the hold still runs inside the task-local values of the run that opened the hold, the mark included: both timers start with a plain Task {} (CreationInputBuffer.swift, CreationInputBuffer+ReplyBound.swift; scheduleFlush uses Task.detached on purpose). The keyboard-source check does not catch every such key: a held shortcut runs again outside any dispatch (.programmatic), and other held keys read a stale NSApp.currentEvent. Example: File > New Workspace with the mouse, then the person types a line and Cmd-F while the daemon does not answer; at the bound, the find field's responder report counts as the person's move. No held key goes to a wrong place (the bound reads the count before the keys run again); the count only moves for a later hold of that window. Follow-up (owned, R-INPUT): both timers detach from the opening run (Task.detached { @MainActor ... }), with a red test first, so held keys run again as the person's own keys and never under another run's scope.
  • The menu run's action-log entrypoint stays unattributed: R-INPUT S8 (entrypoints carry their origin into the router).

Observed / Not run

  • Observed: the recorded dogfood round above; the fleet compile checks and red and green steps; the prepush gate (no new violations).
  • Not run: the same real-mouse round on a build of this PR (it needs a dogfood build that contains it).

Changelog

Fixed: Keys typed while a new workspace opens slowly no longer run in the old terminal after you pick a menu item that moves the focus.

No capture: no visible UI change (a focus rule).

…eirs (red)

Recorded on dogfood build 7dd31211 (e60197e, STATUS 12884): a main-menu item chosen with a
real mouse ran its action outside any input event's dispatch, so its focus events carried the
CLI's or the app's source, and FocusState.personMoveSeq (the count #35275's reply bound compares)
did not move. A line held for a stalled Cmd-N's terminal was then typed into that terminal at the
bound although the person had moved the focus from the menu.

MenuPickPersonMoveTests: Focus Next Pane's menu item, its action sent as AppKit sends a chosen
item's (outside a dispatch), moves the focus and must count as the person's (fails on feat
4a55412: the count does not move); the same move from the CLI must not count (passes).
Recorded on dogfood build 7dd31211 (STATUS 12884): a menu item chosen with the mouse runs its
action outside any input event's dispatch, so its focus events carried the CLI's or the app's
source and FocusState.personMoveSeq did not move; #35275's reply bound then typed a held line
into the press-time terminal although the person had moved the focus from the menu.

- ActionMenuTarget.performAction marks a run the person chose from an open menu (the main menu
  or a context menu) outside a key's dispatch: ActionInvocation.menuPick, carried by
  ActionRunScope (and kept by the runs nested in it). A key equivalent is not marked: its own
  key dispatch names the person, and the round-10 rule of #35275 stays (a key's responder
  change is no focus choice).
- FocusMachine.send counts any focus choice (focusPane, selectTab, focusTarget, a responder
  report) of such a run as the person's move. FocusCoordinator passes the current run's mark.
- No source the focus core reads changes (its drag and sidebar-list rules read sources), so the
  reducer behaves as before; no FFI change. An accessibility press on a menu item also counts,
  as ActionOrigin names menu runs the user's; it fails safe (held keys dropped and logged).

Test: MenuPickPersonMoveTests (red b7b37b6).
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

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

Review 8b2b51e: 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=pr35874 -f ref=fix/r-input-menu-pick-person-move -f platform=macos -f backend_mode=local

Shipped in:

@lawrencecchen lawrencecchen added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 4, 2026
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent subagent, correctness first): FIX at 0e5a693

I reviewed the two commits against feat 4a55412 by reading the code only. I ran no build or test here. The fix is correct for the path it names. A main-menu item, or a context-menu item the registry built, that is picked outside a key dispatch gets the mark (ActionRegistry+Menus.swift:88). The mark reaches every event that its run reduces (FocusCoordinator.swift:112). The held line goes into the press-time terminal only when the count is equal (CreationInputBuffer.swift:400). So an extra count can drop a line, but it can never type a line into the wrong terminal. One item needs a fix before landing: the regression guard does not cover the failure that was recorded.

1. Fix: the guard does not cover the recorded failure (a responder report)

The PR body records View > Focus Right Sidebar, picked with the mouse on 7dd31211. Its only focus event was responder(.textField, source: .programmatic). That event comes from this path: RightSidebarHandlers.swift:13-16, then WindowController+RightSidebar.swift:20 (window?.makeFirstResponder(...)), then the ShellWindow.makeFirstResponder override (WindowController.swift:580-582), then ShellWindow+Events.swift:57. Outside sendEvent, source: .current is programmatic. This PR counts that event only through the .responder case of FocusEvent.choosesFocus (FocusEvent.swift:123).

MenuPickPersonMoveTests drives Focus Next Pane instead. Its only counted event is .focusPane(_, source: .intent) (PaneHandlers.swift:37, through focusByKey at :42-46). The coordinator drops the responder report from the applier's own makeFirstResponder while it applies (FocusCoordinator.swift:246-252). So if .responder is removed from choosesFocus, both new tests still pass. The other six suites of the green step also pass, because none of them reads the count of a menu run. The recorded bug would then be back. REVIEW-STANDARD says: "The reviewer checks the guard against the real bug, not only an intermediate version."

Please add a case that sends Focus Right Sidebar's menu item in the same way (makeMenuItem(for: "focusRightSidebar"), then NSApplication.shared.sendAction to its target) and expects personMoveSeq to move. RightSidebarTests already sets up the window. Please also guard the other half of what the body claims: when the same item runs during a key-down dispatch (registry.isDispatchingKeyDown = { true }, which tests can replace), its responder report must not count (round 10). Today no test fails if ActionRegistry+Menus.swift:88 marks every run.

2. Note: the mark follows the task tree into key dispatches

FocusCoordinator reads ActionRunScope.current?.menuPick when it reduces each event (:112). nested(in:) gives the outer run's mark to every run inside it (ActionRunScope.swift:38). So a key dispatched inside a task that inherited a menu run's scope counts like a menu pick, and round 10 forbids that for keys. Here is a real path. File > New Workspace with the mouse, for a workspace that opens a terminal, calls beginWorkspace, then CreationInputBuffer.open, then armReplyBound. That starts a plain Task (CreationInputBuffer.swift:382), which inherits the menu run's scope. At the bound, onBound calls flush(force:), and runAgain replays the held keys with NSApp.sendEvent inside that task (CreationInputCoordinator.swift:141-142, 271). The runs of those replayed keys are nested with menuPick = true, so a responder change that they cause counts. As far as I can trace, no line is lost today, because the bound reads the count before the replay. But a replayed held Cmd-N opens its own hold inside the same task, so the count of that hold is exposed. This is not blocking. A small guard is to apply the mark only outside a key dispatch (menuPick && !inputDispatch.isKeyDown in the coordinator). The opposite case also exists. A pick from a menu that pops up inside another run's task keeps that run's false, and outer.menuPick || menuPick would keep the pick. The code mostly moves popups out of runs (for example, BrowserToolbarButtonsView.present uses MainRunLoopBlock.perform), so I found no live case.

3. Note: menus whose items run closures are not covered

Only ActionMenuTarget items get the mark. Some menus open outside an input dispatch and their items run closures: AgentPaneMenuAction.item (AgentPaneView+ContextMenu.swift:102-108, which the page's cmuxAgentContextMenu message shows), the Dock menu (DockMenuController.swift:110), and the Chromium context menu of app pages (PageWebView+ContextMenu.menuItem, shown by chromiumMenuPresenter). A pick from these menus that moves the focus still does not count, so the held line still runs in the press-time terminal at the bound. The body says "the main menu or a context menu". Please list these menus under Known limits, or mark at the one place every pick goes through: a CmuxApplication.sendAction(_:to:from:) override, when the sender is an NSMenuItem and no key-down is being dispatched. Picks that go through a page (agent chat items call the page, and the page calls the app later) would still need their own mark.

4. Note: the contract next to the counter is out of date

The doc on FocusState.personMoveSeq (FocusState.swift:200-204) still says that the app's events and an agent's events never count. Inside a menu run, the app's programmatic events now count: for example hideRightSidebar sends .focusPane(_, source: .programmatic) (WindowController+RightSidebar.swift:28), and the coordinator sends its own returnToOmnibar (FocusMachine.swift:21). An accessibility press also counts, as your Known limits say. The same applies to "automation never" in the FocusMachine.send comment (FocusMachine.swift:47-48).

Checked, no finding

  • Over-marking: accessibility presses, debug.menu_item (DEBUG and bench builds only, and meant to act as a click) and performActionForItem all get the mark. Their runs already had the user origin. An extra count can only drop a held line and log it.
  • Key equivalents: AppKit's performKeyEquivalent, the terminal's (TerminalKeyEquivalent.swift:59,71), debug.key (dispatchingSynthetic) and replayed held keys all run inside InputDispatchContext.dispatching, so they do not get the mark. Not verified: a menu that a key opens (Ctrl-F2, or Cmd-? for the Help search). If AppKit tracks it inside that key's dispatch, isDispatchingKeyDown() stays true, the pick does not get the mark, and a move that only changes the responder still does not count there. If AppKit tracks it outside, the pick gets the mark, which is correct.
  • Async work: the scope is a task local. Task {} inherits it, and stored callbacks bind it again (WindowManager.swift:423-424, WindowManager+Placement.swift:17,102, DaemonService+Applied.swift:12-13, ActionRegistry+TrackedWork.swift:14,47). In CmuxNextApp I found no focus move that is put off through MainRunLoopBlock, MainDelivery, DispatchQueue.main.async or Task.detached. One side effect: when a menu run's focus lands late, it counts, but the same late landing from the action's key equivalent does not count (it runs outside the key's dispatch, and .intent there is .cli). That only drops a line, so it fails safe.
  • Swift 6: the new fields are plain Bool values on nonisolated, Sendable, Hashable structs. Neither struct is Codable, and nothing hashes or compares these values. FocusDigest and InputReplay do not read personMoveSeq, so a journal replay does not go out of step.
  • The red test runs the real menu path: it sends #selector(ActionMenuTarget.performAction) to the item's target, as performActionForItem does. It failed on the base for the right reason (the focus moved and the count did not). It catches the removal of the mark, of the scope copy, of the coordinator pass-through or of the count. It does not catch item 1.
  • Repo rules: the red commit comes first, and the body has the red and green fleet steps and Observed / Not run. The PR has risk:normal and a Changelog line, adds no user-facing strings and does not change CI. The fix commit names the red commit but does not give the green result, which CLAUDE.md asks the commit to state.

…counts as the person's; a key equivalent's run is no menu pick

Review of #35874 at 0e5a693 (finding 1): the round-1 test counts through Focus Next Pane's
focus move, so it did not cover the recorded case (dogfood build 7dd31211, STATUS 12884), whose
one focus event is a responder report with the app's source: removing `.responder` from the menu
pick rule left every suite green. MenuPickPersonMoveTests gets:
- aResponderMoveChosenFromAMenuCountsAsThePersons: View > Focus Right Sidebar's menu item, its
  action sent as AppKit sends a chosen item's; the sidebar's search field reports as the first
  responder and the count must move (fails on feat 4a55412: nothing counts it).
- aKeyEquivalentRunIsNoMenuPick: the same menu path run from a key-down's dispatch
  (`isDispatchingKeyDown`) moves the focus and must not count (fails if every run is marked).
The harness keeps the right sidebar's state in a test defaults suite.
…choice (red)

Review of #35874 at 0e5a693 (finding 2): the menu pick mark is a task local, so it also
covers a held key that a creation hold runs again inside the menu run's task (File > New
Workspace by mouse: the hold's bound task inherits the mark). FocusMachine then counted that
key's responder report as the person's choice, which round 10 of #35275 forbids.
FocusPersonMovesTests.aMenuPicksChoicesCountButAKeysResponderReportDoesNot: under `menuPick` a
responder report with the app's source and a CLI focus move count; a responder report with the
keyboard's source does not (fails on 0e5a693).
…de another run stays marked

Review of #35874 at 0e5a693:
- Finding 2: the menu pick mark is a task local, so it also covered a held key that a creation
  hold ran again inside the menu run's task, and FocusMachine counted that key's responder report
  as the person's choice. FocusEvent.choosesFocusInMenuPick replaces choosesFocus: inside a menu
  pick every focus or selection move counts, and a responder report counts only when no key's
  dispatch made it (its source is not the keyboard's). A key's own focus actions still count by
  their source, as before.
- ActionRunScope.nested(in:) keeps a menu pick made inside another run (`outer || inner`).
- Finding 4: the docs of FocusState.personMoveSeq and FocusMachine.send name the menu pick rule.

Tests: MenuPickPersonMoveTests (the recorded Focus Right Sidebar case and the key equivalent
guard, 3a3a39d) and FocusPersonMovesTests.aMenuPicksChoicesCountButAKeysResponderReportDoesNot
(red 0e213dd). Round 1: red 2e11c857, green 47ce629a.
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent subagent, correctness first): SHIP at 8b2b51e

Round 2. I reviewed 3a3a39d, 0e213dd and 8b2b51e on top of round 1 by reading the code only. I ran no build or test here. Nothing blocks the landing. Findings 1 and 4 are resolved. Finding 2 is narrower now but not removed (note A). It can only add a count, which drops a held line and logs it, and I find no delivery decision that it changes. Finding 3 stays a documented limit, which is acceptable, but the stated reason is only partly right (note B).

Round 1 findings

  1. Resolved. aResponderMoveChosenFromAMenuCountsAsThePersons isolates the responder report. In the Focus Right Sidebar run, the report from WindowController+RightSidebar.swift:21 through ShellWindow+Events.swift:57 is the only event that can count. The handler (RightSidebarHandlers.swift:13-16) and the default section (Sessions, VaultHostView) send no focusPane, selectTab or focusTarget. If .responder is removed from choosesFocusInMenuPick, or limited to .mouse, the assertion at MenuPickPersonMoveTests.swift:85 fails. aKeyEquivalentRunIsNoMenuPick fails if ActionRegistry+Menus.swift:88 marks every run. Red B (:68, :85) and red A (FocusPersonMovesTests.swift:95, 3 vs 2) match those assertions.
  2. Narrowed, not removed. See note A.
  3. Documented limit, acceptable. See note B.
  4. Resolved. The docs of FocusState.personMoveSeq and FocusMachine.send now name the menu pick rule.

A. Held keys still run again under the menu mark, and the source check does not catch them (not blocking)

  • Cause: the reply-bound and limit timers start with a plain Task { @MainActor … } (CreationInputBuffer.swift:382, CreationInputBuffer+ReplyBound.swift:59). They inherit the creating run's task locals, the mark included. scheduleFlush uses Task.detached on purpose, "without the creating action's task-locals" (CreationInputCoordinator.swift:357-358).
  • The round 2 rule skips only .responder(_, .keyboard) (FocusEvent.swift:126). Its doc (:122-123) and the PR body say that a held key run again in the run's task carries the keyboard's source. That is not reliable:
    • A shortcut held for the press-time terminal runs again as a .command key through RegistryKeyBindings.run (CreationInputCoordinator.swift:273-275). That is outside any dispatch, so its events carry .programmatic.
    • Any other held key runs again through NSApp.sendEvent(event) (AppDelegate.swift:139). FocusEvent.Source.current reads NSApp.currentEvent (FocusCoordinator.swift:272). InputDispatchContext.swift:10-11 says that this is the last event AppKit dequeued, not the event given to sendEvent.
  • Scenario: the person picks File > New Workspace with the mouse. During the stall, they type a line and then Cmd-F for the press-time terminal. At the reply bound, Cmd-F runs again in the marked task. The find field's responder report carries .programmatic, and the count moves.
  • Impact: I still find no delivery decision that this changes, as in round 1. endReplyWait reads the count before the same flush runs the keys (CreationInputCoordinator.swift:141-142), and a key that opens a new hold ends the pass (:278).
  • Ask: start both timers with Task.detached { @MainActor … }, as scheduleFlush does. Then a held key runs again as a top-level run with no mark, the round 10 rule applies to it as to any other key, and the source check is no longer needed. This also stops held keys from running again under the origin of the run that opened the hold (nested(in:) takes the outer origin), for example a CLI run with focus: true. At minimum, correct the comment and the body sentence.

B. The reason for not using one sendAction override is only partly right (not blocking)

  • (a) This is true only for an override that binds ActionRunScope itself, because the claim-end check is ActionRunScope.current == nil (ActionRouting+SidebarUserIntent.swift:13). A separate mark leaves that value nil.
  • (b) This is not the crash class that TaskLocal+InTask.swift documents. That crash came from a startOnMainActor task around a nested run loop on macOS 14. Below macOS 15.4, the helper no longer starts a task and keeps the binding in fallback storage (:17, :28). Every registry run already binds ActionRunScope with the same helper (ActionRunScope.swift:78).
  • Neither (a) nor (b) applies to a mark that is not a task local. Example: a flag on CmuxApplication, like handlingSendEvent, set while sendAction(_:to:from:) runs with an NSMenuItem sender and no key-down dispatch. FocusCoordinator would read it next to ActionRunScope.current?.menuPick. That covers the synchronous part of the Dock, Home, Chromium context and agent chat menus. Picks that go through a page still need S8.
  • Leaving this to S8 is acceptable, because the PR documents the limit and the gap existed before this PR. Please correct the reason in the body, so that S8 does not rule out the simpler design for a wrong reason.

Merge with #35875 (merged, now feat tip c6be943)

git merge-tree --write-tree HEAD c6be943058a is clean: FocusCoordinator.swift, FocusEvent.swift and FocusMachine.swift merge automatically. In the merged tree, FocusMachine.send calls cmux_input_focus_send and keeps the menu pick count. The coordinator still passes the mark (merged FocusCoordinator.swift:113) and is still the only app path that reduces focus events. Only #35875 changed this PR's files, or the code that uses the count, after the base. Not run: green step 9e35de97 ran on the base before #35875, not on the merged tree.

Checked, no finding

  • nested(in:) with outer.menuPick || menuPick (ActionRunScope.swift:39) is correct. I found no nested run that is marked but is not the person's pick. For example, an unhandled page key runs its binding directly (KeyRouter+PageFirstKeys.swift:48-51), not through the menu.
  • The test harness keeps the right sidebar's state in its own defaults suite, through an existing seam (AppServices.swift:274).
  • Repo rules: the red commits come before the fix, and the body has the red and green steps and Observed / Not run. The round 2 green result is in the body, not in the fix commit.

@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 4, 2026
@lawrencecchen
lawrencecchen merged commit cd5abc2 into feat-cmux-next Nov 4, 2026
249 of 251 checks passed
@lawrencecchen
lawrencecchen deleted the fix/r-input-menu-pick-person-move branch November 4, 2026 23:09
lawrencecchen added a commit that referenced this pull request Nov 5, 2026
…ide the opening run's task locals (review of #35874) (#35935)

* test(focus): a creation hold's reply bound and limit end the hold outside the task-local scope of the run that opened it (red, review of #35874 finding 2)

* fix(focus): a creation hold's reply bound and limit end the hold in a turn of their own, outside the opening run's task locals (review of #35874 finding 2)

* fix(focus): the hold timers sleep off the main actor and end the hold in a main-actor method (the region checker refused a main-actor detached closure with a weak capture)

This branch was previously deployed

1 inactive deployment
ci-routing-agent — 8b2b51ef Deployed Nov 4, 2026 by lawrencecchen via cmux-tui tree preflight #113235
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