R-INPUT S2a: the input router reduces each window's focus as the app's focus handle does (WindowFocus), Rust only

LA
2d ago
+283-74

R-INPUT stage S2a (DESIGN.md, stage S2). Stage S2 makes the input router (cmux-focus-core input::InputRouter, #34052) own every window's focus, so that the app's per-window focus handle can become the router's in S2b and S2c. Today the router and the app's handle reduce the same focus event in different ways:

  • A topology report: the router runs reducer::reduce again. The app's handle takes reducer::set_topology's fast path: an equal topology changes nothing. The daemon sends the topology again after every delta, so the router reported ran for events that the app's handle drops.
  • The router had no address bar memory (Chrome's per-tab omnibox focus). The app's handle keeps it beside the state.

This PR makes the router reduce focus as the app's handle does. It changes no caller and no app behavior:

  • New cmux_focus_core::window::WindowFocus: one window's focus (the state, the address bar memory, the Close Tab aim), with the operations that the app's focus handle performs (send, set_topology, pane_moved, frame_presented, pane_emptied, aims_at_emptied_pane, set_close_focus, import, rederive).
  • The FFI focus handle (cmux-layout-reducer-ffi src/focus.rs) now holds a WindowFocus and delegates to it. The C ABI and the reply bytes do not change.
  • The router holds a WindowFocus for each window, and a focus event goes through WindowFocus::send. After the focus effects, the router emits the new InputEffect::FocusReduced { window, reduction }: whether the reducer ran and what the address bar memory asks for. The app's reply carries the same data.
  • The fuzz's reference model also holds a WindowFocus for each window, so the fuzz compares the routing over the same focus rules.

This is step A of the FFI two-step rule (STATUS 5388): cmux-focus-core and cmux-layout-reducer-ffi are app FFI inputs. After this lands, the bot publishes the FFI release and opens the re-pin. There is no Swift half: the ABI does not change.

Test (cmux-tui/crates/cmux-focus-core/tests/input_router/key_router/window_focus.rs):

  • key_router::window_focus::the_router_reduces_focus_events_as_the_apps_focus_handle_does: the same focus events go through the router and through a WindowFocus (the app handle's type). The events are a topology, the equal topology again, a mouse focus on a page pane, the address bar by keyboard, a mouse focus on the terminal pane, a keyboard focus back to the page pane, and the return event. The router's focus effects, its FocusReduced reduction and the window's state must be equal to the handle's after each event.

Red/green, the same filter key_router:: (scripts/ci/cmux-tui-rust-check.sh --os linux):

  • Red 0c9fe162e4f4 (the test and WindowFocus, with the router as on feat), mode test: step 3d17eb26c48f34c41d3ce7b1. 72 tests ran and 1 failed: key_router::window_focus::the_router_reduces_focus_events_as_the_apps_focus_handle_does, on its assertion at window_focus.rs:68, on the equal topology report (left Some(Reduction { ran: true, .. }), right Some(Reduction { ran: false, .. })). It failed again when it ran alone. The first red step (bf51f012572b93d9e4bd7045) also ended in failure, but its log is lost: GitHub answers BlobNotFound and the controller has no full log. So I ran it again.
  • Green d5cb5d44ed68, mode all: step 28af93a87fd6752c31c2dbd8. fmt passed. Clippy (--workspace --all-targets -D warnings) passed. 72 of 72 key_router:: tests passed: the new test, the router fuzz (20,000 cases against the reference model), key_router::mutants::every_mutant_is_caught and key_router::allocations::a_key_its_release_and_a_bound_chord_allocate_nothing (a key still allocates nothing).
  • FFI crate: cmux-layout-reducer-ffi is its own Cargo workspace, so the fleet's Rust check does not build it. The never-merge ref fix/r-input-s2a-ffi-proof-never-merge (439a4c88b5e, listed in stopped.txt) adds the crate to the cmux-tui workspace and runs every workspace test, including the FFI crate's Swift-recorded focus corpus (the_rust_focus_rules_reproduce_every_recorded_swift_step) and the handle tests in close_focus_rules.rs. Step 6203ede8b4514ffe3266189d (mode test, no filter): cmux-layout-reducer-ffi 74 of 74 passed, cmux-focus-core 133 of 133 passed. The step as a whole failed on 10 tests in crates that this PR does not touch (acpmux 3, cmux-agent-pane-policy 2, cmux-tui 4 and their reruns; for example acpmux::hermetic_daemon_guard every_test_binary_runs_in_the_hermetic_environment, which the proof ref's extra workspace member can cause, and cmux-agent-pane-policy::typescript the_page_sends_only_allowed_acpmux_meta). The GitHub check cmux-tui/crates/cmux-layout-reducer-ffi of this PR also builds the FFI crate in its own workspace, runs clippy -D warnings on all targets and runs every FFI test: it passed.

Independent review: SHIP at d5cb5d44ed68f208cc5c60ccde1fc57b1be9ca5a (https://github.com/manaflow-ai/cmux/pull/34929#issuecomment-6624349281). Its three small points go to a follow-up (S2b touches the same files): an FFI handle test of the reply to an equal topology; a reference model that keeps reducer::reduce for topology events, so that the fuzz also proves the fast path changes no routing; and a doc line that the FFI sends a topology only through set_topology.

Observed / Not run

  • Observed: the fleet steps above.
  • Not run: anything in the app. This is Rust only, and no caller changes until S2b. The S3 gate (KeyRouterBench p99 ≤ 2 µs and 0 allocations per key before any route turns on) does not apply here: this stage turns on no route.

Changelog

none

No capture: Rust-only library code; the app's behavior does not change.