Focus rules move into cmux-focus-core with app FFI calls: close successors, Close Tab aim, address bar memory, input invariants (A12 step A)

Merged
LA
4d ago
+4080-51

Check the app FFI pin: expected, step A of a two-step FFI move.

Summary

Step A of rust-migration backlog A12: the Rust half only. The focus rules the macOS app keeps in Swift beside cmux-focus-core move into that crate, with their Swift tests ported as Rust tests, and the app FFI (cmux-layout-reducer-ffi, linked by cmux-app-ffi) gets the calls. No Swift change and no behavior change: no Swift code calls the new symbols yet. Step B (the Swift half, held in #31835) makes Swift call these rules and deletes the Swift copies after the bot re-pins CCmuxAppFFI on feat.

Swift rule (deleted in step B)Rust
CmuxNextDesignTokens/CloseFocus/FocusAfterClose.swift tab, openerFamily, liveOpeners, workspaceclose_focus::{tab, opener_family, live_openers, workspace}
CmuxNextFocus/PaneCloseAim.swiftclose_aim::PaneCloseAim
CmuxNextFocus/OmnibarFocusMemory.swiftomnibar_memory::OmnibarFocusMemory
CmuxNextFocus/InputInvariants.swift, InputInvariants+World.swiftinvariants::{model, transition, step, world, expected_responder}
FocusState+TypingTab.swift, FocusState+GhosttyAgreement.swift, Resolved.kindFocusState::{typing_tab, terminal_tab}, Resolved::{pane, tab, kind, is_text_input, ghostty_mismatch}

The existing pane rule stays the one pane rule. It is now generic over the id type, with no behavior change. The layout.closeFocus value keeps one parser: the app's settings layer.

The app FFI:

  • Per close, typed (no JSON, no handle): cmux_close_focus_tab, cmux_close_focus_workspace, cmux_close_focus_live_openers over the caller's u32 ids.
  • Per press and per reduction, on the existing window focus handle: cmux_focus_pane_moved, cmux_focus_frame_presented, cmux_focus_pane_emptied, cmux_focus_aims_at_emptied_pane. The handle notes the address bar memory on every cmux_focus_send and cmux_focus_set_topology. The reply's two former reserved bytes are now omnibar_return and returns_to_omnibar (same layout), and a typing_tab string follows the reply's last field. The pinned Swift reader reads the reply by field, so the added fields do not change what it reads.
  • Debug only, JSON channel: focus_input_world, focus_input_step, focus_debug_window.

One deliberate difference from the Swift rules: past its 64-tab bound, the address bar memory drops the oldest tab. The Swift Set.removeFirst() dropped one in hash order, which no rule relied on; the Rust order is deterministic and tested (ported::omnibar_memory::the_memory_is_bounded_oldest_first). A full memory also evicts only when it adds a new tab; the Swift copy evicted one even when the tab was already remembered. It takes effect in step B. Violation detail strings are diagnostics and follow the Swift text where it is plain; the invariant ids and windows are the Swift ones.

Tests

The Swift tests of these rules are the Rust corpus (cmux-tui/crates/cmux-focus-core/tests/focus_rules, filter ported::): FocusAfterCloseOpenerTests, the deleted FocusAfterCloseModelCheckTests (exhaustive pane model check for both policies with its 7 mutants, tab check with its 2 mutants, workspace check), the focus cases of plans/cmux-next/formal/closefocus.tla (strip, strip-recent and list configurations: SuccessorRule, C1-C3, and the "history" mutant), PaneCloseAimTests, OmnibarFocusMemoryTests, ChromiumPageFocusInvariantTests, GhosttyFocusAgreementTests, plus one case per F, T and W invariant and the app's InputObservation JSON. cmux-layout-reducer-ffi/tests/close_focus_rules.rs drives each new C call and JSON op (also under ported::).

Red first: commit 99164cab0b3 adds the tests with stub rules; b823d9258c9 moves the rules.

The proof refs are this head plus one never-merge commit that builds cmux-layout-reducer-ffi inside the cmux-tui workspace: the crate is its own Cargo workspace, so the fleet script does not build it otherwise.

Observed / Not run

Observed: the fleet jobs above; the prepush gate passed.
Not run: any Swift build (no Swift changes here). Swift suites, the perf A/B and the line accounting belong to step B (#31835).

Earlier review: an independent review passed the same Rust at 75ced4de22c on #31835. A new review at this head follows.

Changelog

none