Swift calls cmux-focus-core's focus rules; the Swift copies go (A12 step B, after #31921 and the FFI re-pin)

LA
4h ago
+4367-754

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, 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 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_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 / cmux_focus_set_topology, and the reply's two former reserved bytes are now omnibar_return and returns_to_omnibar (same layout). A typing_tab string 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):

Revswift_logic_linesmirror_linesCmuxNextFocus logic / mirrorCmuxNextDesignTokens logic / mirror
base d47dcbddc6c41552070520456 / 3781834 / 1944
head 75ced4de22c415068704086 / 3821834 / 1828
change-452-112-450 / +40 / -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).

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