Swift calls cmux-focus-core's focus rules; the Swift copies go (A12 step B, after #31921 and the FFI re-pin)
Step B of a two-step FFI move (lead decision 06:37Z). Held. Its Rust half lands alone as step A, #31921. After step A lands and the bot's re-pin of CCmuxAppFFI is on feat-cmux-next, this branch is rebuilt on that feat: only the Swift half (ae404b63654) and the line accounting stay, then the 6 Swift suites run on a Mac and the perf A/B runs. Do not add ready-to-land before that.
Summary
Moves the focus rules the macOS app keeps in Swift beside cmux-focus-core into that crate (rust-migration backlog A12, layer-ownership.md: Rust owns rules and input routing decisions), exposes them on the app FFI, and makes Swift call them: the Swift copies are deleted (commit ae404b63654).
What moves, with one rule set (the existing pane rule stays the one pane rule; it is now generic over the id type):
| Swift (deleted or a thin call in ae404b63654) | Rust |
|---|---|
CmuxNextDesignTokens/CloseFocus/FocusAfterClose.swift tab, openerFamily, liveOpeners, workspace | close_focus::{tab, opener_family, live_openers, workspace} |
CmuxNextFocus/PaneCloseAim.swift | close_aim::PaneCloseAim |
CmuxNextFocus/OmnibarFocusMemory.swift | omnibar_memory::OmnibarFocusMemory |
CmuxNextFocus/InputInvariants.swift, InputInvariants+World.swift | invariants::{model, transition, step, world, expected_responder} |
FocusState+TypingTab.swift, FocusState+GhosttyAgreement.swift, Resolved.kind | FocusState::{typing_tab, terminal_tab}, Resolved::{pane, tab, kind, is_text_input, ghostty_mismatch} |
The app FFI (cmux-layout-reducer-ffi, linked by cmux-app-ffi):
- Per close, typed (no JSON, no handle):
cmux_close_focus_tab,cmux_close_focus_workspace,cmux_close_focus_live_openersover the caller'su32ids. - 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 everycmux_focus_send/cmux_focus_set_topology, and the reply's two formerreservedbytes are nowomnibar_returnandreturns_to_omnibar(same layout). Atyping_tabstring follows the reply's last field. - Debug only, JSON channel:
focus_input_world,focus_input_step,focus_debug_window.
Swift half (ae404b63654): FocusAfterClose numbers the caller's ids and calls cmux_close_focus_* (callers unchanged); PaneCloseAim forwards to the window's FocusMachine (its Press keeps its shape for TabCloseGuard); OmnibarFocusMemory is deleted and FocusCoordinator reads the reply flags; InputInvariants calls the JSON ops; FocusState.typingTab is the reply's typing_tab. A refused call fails loud in debug builds. PaneCloseAimTests and OmnibarFocusMemoryTests are deleted (ported to Rust); FocusAfterCloseOpenerTests, GhosttyFocusAgreementTests, ChromiumPageFocusInvariantTests, CloseTabKeepsPaneTests, PaneCloseControllerTests and BrowserTabOpenersCloseFocusTests stay. CmuxNextApp shrinks by 3 lines. FocusEvent.Responder.pane lost its last caller and is deleted. The FocusState read view stays: CreationInputCoordinator (REOPEN-F4's file) and other readers use it; only its rules moved.
Behavior: no change, with one deliberate difference: 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). The layout.closeFocus value has one parser: the app's settings layer (CloseFocusSetting, CloseFocusPolicy.init(configValue:)), as for every other setting; that parse moves with the settings (backlog D). Violation detail strings are diagnostics and follow the Swift text where it is plain; Swift's own enum descriptions (for example of a FocusEvent) are not reproduced exactly. The invariant ids and windows are the Swift ones.
Expected red until the pre-merge release and re-pin: Check the app FFI pin, and every Swift compile of this head.
Line accounting
python3 -I scripts/cmux-next/swift-logic-lines.py --repo . --rev REV --json, with plans/cmux-next/swift-logic-classes.tsv updated in this PR (rows of the deleted rules removed; FocusAfterClose, PaneCloseAim, InputInvariants and the Ghostty check reclassified PLATFORM as thin FFI glue; a row for the new IDNumbering glue):
| Rev | swift_logic_lines | mirror_lines | CmuxNextFocus logic / mirror | CmuxNextDesignTokens logic / mirror |
|---|---|---|---|---|
| base d47dcbddc6c | 415520 | 70520 | 456 / 378 | 1834 / 1944 |
| head 75ced4de22c | 415068 | 70408 | 6 / 382 | 1834 / 1828 |
| change | -452 | -112 | -450 / +4 | 0 / -116 |
Unlisted lines are the same at both (168487); one table row matches nothing at both (NoActivateKeyboardGuard, not this PR). The CmuxNextFocus mirror rises by 4 for the new typingTab field of the read view.
Tests
The Swift tests of these types 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, 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.
Red first: commit 5efae6086e7 adds the tests with stub rules; commit 9cfa5f4b328 moves the rules; c2c1536502e adds the port of GhosttyFocusAgreementTests (after the move, so its red is not in the red job).
-
Red:
cmux-tui-rust-check.sh test ported::(Linux) at 5efae6086e7, fleet job 94d94b1d7c3c98a3d2d076ef: 54 tests run, 38 failed (every moved rule), 16 passed (the pane rule cases, which were already Rust, and the mutant checks). -
Green: the same command at 9f8b8c1f560, fleet job 76c926449f4a059ba75f2eb6: pending.
-
clippy at 9f8b8c1f560: fleet job 21ab6c6d39aa69a96a31fd87: pending. The first clippy run (5c4d2ab1b4aa46042ed05760 at 715db7bbfd4) found 4
collapsible_matcherrors ininvariants.rs, fixed in 9f8b8c1f560; it also fails on a base red not in this PR:clippy::suspicious_open_optionsincmux-tui/crates/cmux-tui/src/plugin_manager.rs:233(from #31462, base 86faa171ca1). -
fmt at 715db7bbfd4: fleet job caa85740387c887c01f39ba9: passed.
-
At the never-merge proof ref
rust-migration/focus-rules-ffiproof2(a7d2dbc653f = head 4ce4c01a230 plus one commit that buildscmux-layout-reducer-ffiinside the cmux-tui workspace; the crate is its own Cargo workspace, so the fleet script never builds it):test ported::(both crates' ported and C ABI tests) 33105b66402afc55f621106a: 63 of 63 passed. clippy 8d584d44510be52ab9b05e7d: both focus crates clean; red only on the base red inplugin_manager.rs:233. At the first proof ref 0acd4f013f0:test swift_step(the recorded focus corpus, which covers the reducer's call of the now generic pane rule) feaebcc95776d0be3f5c0775: passed.test ported::at 9f8b8c1f560 (76c926449f4a059ba75f2eb6): 56 of 56 passed. -
4ce4c01a230 fixes two clippy lints in the ported tests (found by 21ab6c6d39aa69a96a31fd87 at 9f8b8c1f560, which also hit the base red above).
-
At head 75ced4de22c (proof ref
rust-migration/focus-rules-ffiproof3, 7551cbe754b):test ported::6e96e84bf8ace9b8d95dcf1c and clippy ece2b45de78af2a18cbd0a31: pending.
Observed / Not run
Observed: the fleet jobs above. The prepush gate passed (no new violations).
Not run: any Swift compile or Swift suite of this head (it needs the pre-merge FFI release). After the re-pin: cmux-ci compile-check --app --package CmuxNext, then the Swift suites FocusAfterCloseOpenerTests, GhosttyFocusAgreementTests, ChromiumPageFocusInvariantTests, CloseTabKeepsPaneTests, PaneCloseControllerTests, BrowserTabOpenersCloseFocusTests on a Mac, and a perf A/B of the per-close and per-press calls on an exclusive capture host. Not done: lowering plans/cmux-next/arch-ratchet.json for the -3 App lines (the gate only suggests it).
Changelog
none