R-INPUT S2b step B: every window's focus machine keeps its focus in the app's one input router (cmux_input_*)

Merged
LA
2d ago
+241-66

REWRITE R-INPUT stage S2, step B of the FFI two-step (step A #35641, re-pin #35853, DESIGN.md section 5): every window's FocusMachine keeps its focus in the app's one input router (cmux_input_*) under a window key of its own, instead of a per-window cmux_focus_* handle. Pure refactor: the same requests and the same reply bytes (step A's corpus test compares them), so every focus rule is unchanged.

What changes:

  • FocusInput (CmuxNextFocus): owns the CmuxInput handle; made once at the composition root (AppServices.focusInput) and given to each window's FocusCoordinator through WindowState. The router's key lanes are bounded by the Mac's physical memory (cmux_input_new(memory)).
  • One window key per machine: each machine opens a window under a key no other machine of that router had, and closes it when it is freed. So a window id the app reuses (a restored closed window) never meets the state of the machine it replaces, whichever is freed first.
  • A machine made with no input (tests, the journal replay InputReplay) gets a router of its own.
  • FocusMachine's Close Tab aim calls move to FocusMachine+CloseAim.swift (the A12 follow-up); FocusMachine.swift goes from 467 to 458 lines.
  • FocusResponderClassifier.describe moves to the package as FocusEvent.Responder.debugName. Arch app_lines -10.

Tests

FocusInputSharedRouterTests (Packages/macOS/CmuxNext, new):

  • aMachineInTheSharedRouterReducesAsAMachineAlone: a machine in the shared router and a machine alone get the same reply (changed, effects, omnibar flags) and the same state after every event of a sequence (topology, person moves, a selection, the address bar, a click report, an intent, a CLI move, a topology change, a palette move), while another window of the same router acts between its events; the same checkpoint, history and remembered panes at the end.
  • aNewMachineNeverMeetsAFreedMachinesFocus: a machine freed before a new one is made leaves no history or remembered panes to it; the window still open keeps its own.
  • everyRequestGoesThroughTheInputHandle: the close-focus policy, the Close Tab aim (pane emptied, pane moved, aimed at, presented), the history, the remembered panes and the checkpoint all go through cmux_input_*; a checkpoint that does not decode is refused and leaves the state as it was (#35641 review notes: the C ABI test did not call every export, and did not compare the state before and after a bad import).

A pure refactor has no red step: the old path is gone in this PR. The check that the new path answers as the old one is step A's Rust corpus test the_input_handle_replies_to_every_corpus_step_as_the_window_handle (cmux-tui/crates/cmux-layout-reducer-ffi/tests/input_handle.rs, landed in #35641): it replays every corpus sequence (100+ sequences, 1,000+ focus events) through the input handle and through the old window handle and compares each reply. The Swift tests above check the app's side only: a machine in the shared router answers as a machine alone while another window acts, windows stay apart, and every Swift call goes through cmux_input_*.

Compile check 1e9b30f5 of 83c1758d99e (this branch merged with feat 00655a40430) passes; e79c99cc59b changes only the new test file. Green: step e498ede205b71ebe3d903591 at e79c99cc59b (aws-m4pro-4): all 21 suites pass (FocusInputSharedRouterTests, FocusPersonMovesTests, FocusWaitLeavesPageTests, CloseTabKeepsPaneTests, MultiWindowInputRoutingTests, FocusFollowsMouseTests, FocusedPaneShowsFirstTests, KeyRouterRepeatAndChordTests, MenuKeyRoutingTests, PaneCloseApplyTests, PaneCloseControllerTests, WindowFocusRestoreTests, WebViewFocusedContextTests, NewTabHoldFocusTests, CreationHoldReplyBoundTests, CreationHoldReplyBoundRuleTests, PersonTerminalKeyboardClaimTests, HomePendingTerminalFocusTests, SwitchToColdTabTypeAheadTests, HeadlessRenameTakesNoKeyboardTests, HeldKeyRunPanelTests). An earlier run at 83c1758d99e (step b998c98b) failed one case of my new test everyRequestGoesThroughTheInputHandle: it called the Close Tab aim in an order PaneCloseAim does not document (the frame stamped before the focus moved); e79c99cc59b follows the documented order and compares every answer with a machine alone.

ASan (CMUX_SWIFT_SANITIZE=address, step 085409561b6c6bf6a53fb226): 5 of 8 suites pass (FocusInputSharedRouterTests, FocusPersonMovesTests, FocusWaitLeavesPageTests, WindowFocusRestoreTests, CreationHoldReplyBoundRuleTests: the machine reply reads under the sanitizer). The 3 window suites (CloseTabKeepsPaneTests, PaneCloseControllerTests, MultiWindowInputRoutingTests) abort in the ASan runtime itself before any assertion: "AddressSanitizer failed to deallocate 0x40000 bytes at address 0x...b28 (error code: 22) / Failed to munmap" (a non-page-aligned unmap inside the sanitizer's allocator). Classified as a base finding, not this change: the same 3 suites under ASan on the base feat 00655a40430 (without step B, step dc3b33cf482e2f0a0de4c745, aws-m4pro-2) abort with the identical runtime error ("failed to deallocate 0x40000 bytes at address 0x...b28 (error code: 22) / Failed to munmap") in all 3 suites. So under ASan the fleet Macs (macOS 26/27, Xcode 26.6) cannot run these window suites at all, with or without this PR; the 5 suites that do run under ASan include the machine-level ones that read every reply of the input handle.

S2 test phase: the input router fuzz at 1,000,000 cases on the merged step A commit dba5f44a7bd1 passed (step 85d683ea37e2, Linux, 180.3 s, 0 findings).

Review

Review (independent subagent a95e30fa): SHIP at e79c99cc59bf5b0650d7cabb6709ade5dbd1bdcc (https://github.com/manaflow-ai/cmux/pull/35875#issuecomment-6643442362). Its non-blocking findings go to one follow-up PR: test 3 compares the new path with itself and the absolute checks of history and remembered() came out (F1); one check of test 1 cannot fail (F2); test 2 does not prove that deinit closes the window (F3); FocusMachine.deinit must never run off the main thread now that it writes to the shared router (F5); the default FocusInput() of WindowState.init can give a future production window a router of its own (F6); a stale cmux_focus_* comment in Package.swift (F7). F4 (this body named the Swift test as the parity proof) is fixed above.

Observed / Not run

  • Observed: the fleet compile check, the 21 focus and hold suites, and 8 of them under CMUX_SWIFT_SANITIZE=address (the lead's sanitizer plan for the app's reads of a reply: ASan's interceptors cover the Rust library's heap too); the prepush gate (no new violations, app_lines -10).
  • Not run: KeyRouterBench A/B (focus bench within 5 %, DESIGN.md S2 acceptance): it runs on the fleet after this lands, with the stage's test phase.

Changelog

none

No capture: no visible change (a refactor).