agent cursor visibility: a typed C ABI call for the per-frame path (B9b step A)
Step A of a two-step FFI move (backlog B9 follow-up). The agent cursor visibility rules get a typed C ABI call, cmux_agent_cursor_visibility, for the app's per-frame path. It has no JSON encode or decode and no allocation per call. There's no Swift change and no behavior change: the app keeps calling the JSON operation until step B. Step B moves the Swift resolver to this call and removes the JSON path.
Check the app FFI pin: expected, step A of a two-step FFI move.
Why
#32028 moved the rules to Rust behind the JSON operation agent_cursor_visibility. That call runs on a per-frame path while an agent cursor target is tracked:
- Once per agent input event:
AgentCursorOverlayModel.draw, thenAgentCursorVisibilitySource.resolve. Streamed agent pointer moves come as often as one every 10 ms. - Once per frame of pane motion:
ScreenContentView+Motion.swift:255asks for an overlay sync on every changedapplyPresentationframe (perf-hot, 4 ms motion-frame).LayoutRootView.syncOverlaythen callsnotifyOverlaySync, andAgentCursorVisibilitySource.reresolveresolves every tracked target.
The JSON path costs 18083 ns p50, 20583 ns p95, 25709 ns p99 and 47209 ns max per resolve. That's AgentCursorVisibilityResolveBench, release build, cmux18-perf, job 81c8069152ac357eeb4c478d. The repo rule for a per-frame path is a typed C ABI, not JSON.
What changes
cmux-focus-core/src/agent_cursor_visibility.rs: the rules run once, inresolve_view, over aSnapshotViewtrait. The JSON operation'sresolvereads aSnapshotwith string ids through it. The typed call reads the caller's numberedu32ids and flat arrays through it. The rules compare ids only for identity, so both paths give the same answer.Rectis#[repr(C)], so it crosses the boundary unchanged.cmux-layout-reducer-ffi/src/agent_cursor.rsand the header:CmuxCursorSnapshot(target, tab location, windows front to back, with listed workspaces, sidebar rows, panes and chips as ranges into flat arrays) in,CmuxCursorVisibility(kind, window, anchor, reason, rects, zoom, scale) out. Every range is checked before the rules read it. A null array with a nonzero count isERR_NULL, a range outside its array isERR_INVALID, and a panic isERR_PANIC.outis written only on OK. This follows the pattern ofcmux_close_focus_*andcmux_focus_*.
Tests
cmux-layout-reducer-ffi/tests/agent_cursor_typed.rs:
- Every vector of
schemas/agent-cursor-visibility/vectors.json, numbered the way the app numbers it, answers itsexpectthrough the typed call. - The typed call and the JSON rules agree on every vector, and on each vector with each window removed, each pane's chips cleared, and each pane's page cleared.
- The structs have the header's sizes.
- Null and out-of-range input is refused, and a refused call leaves
outunchanged.
The existing cmux-focus-core and C ABI JSON tests still cover the JSON operation.
Changelog
none
Observed / Not run
Observed. cmux-layout-reducer-ffi is its own Cargo workspace, so the fleet's Rust step doesn't build it. The proofs run on never-merge refs that add it to the cmux-tui workspace (listed in stopped.txt). Their FFI and focus-core trees equal this PR's, checked with git rev-parse <sha>:<dir>.
- Red at 4924ad28f5a (349252c34b6 plus the workspace commit: header, structs, test and a stub that answers
notDrawn(tabClosed)):cmux-tui-rust-check.sh test agent_cursor, stepb8ba8d897ba6fcef94a9d3de, failed. 3 typed tests failed: vector replay, JSON agreement, bad input. The layout test and the existing JSON tests passed. - The first green round at 36495005df9 found that the new call indexed slices. The panic ratchet on the proof ref, step
0265e7ecf95d6286e7d8b471, countedindexing_slicinghits incmux-layout-reducer-ffi: 4 inagent_cursor.rs(this PR) and 5 insidebar_mirror.rs. Thesidebar_mirror.rshits are already on feat; that file isn't touched here and no ratchet counts that crate on feat. It also counted onearithmetic_side_effectsof this PR. 124066d2af7 removes all of this PR's hits: every read is a checkedget, the pane handle is achecked_add, the conversions usetry_from, and an impossible window index answersERR_INVALID. The step at 36495005df9 (8057d84448b2b2a72347d8f8) is cancelled, because that head is replaced. - 124066d2af7 didn't compile. A closure named
showshid the ruleshows(rect, clip)(E0057, clippy3ae289e4f44272fbb6e5a0f7, panic at that head602d6163f76794906c574980). 2973a299dc6 renames itshows_workspace. The steps at that head are cancelled or replaced. - The proof-ref panic run at 956947aefd6 (
7ddbad5edffa09e2067d89ed) counted 0 hits inagent_cursor.rs. It still counted 5indexing_slicinghits incmux-layout-reducer-ffi, all insidebar_mirror.rs(lines 131, 161, 216, 223, 245). These are older hits, last changed by b66fc32c7ee. By the lead's order this PR fixes them too, in 571a44d0116:Mirror::apply_batchreads withValue::getand sets"dropped"throughset_dropped. The answers stay the same, and the new testtests/sidebar_mirror_batch.rspins them. - This fix keeps the same behavior, so the new test has to PASS on both the old and the new code, and the new code has to bring the proof-ref panic run to 0.
- The test and the fix are one commit because the pre-commit hook refused the separate test commit: the crate still had the fix unstaged. A pushed commit isn't split, since that needs a force push.
- Old code (old
sidebar_mirror.rsplus the new test, never-merge ref a0672035f91):test sidebar_mirror_batch, step8db0f754cc2932208ac6c1d9, passed (2 of 2). The answers pinned from the old code are the answers of the new code. - Green at 16df66048da (571a44d0116 on the proof ref, same trees): clippy, which is the compile,
a3f475084b7419b96e4548bc;test agent_cursor23f91f9069afb4b8d1ec2215passed (10 of 10: the typed vectors, the JSON agreement, bad input, the header layout, and the JSON op and rules);test sidebar_mirror_batchc8763fb83d67022cc9457db1passed (2 of 2); panic ratchet1955895eadeec6d6e34a121c:cmux-layout-reducer-ffishows 0indexing_slicing. The run is red only on the base redcmux-rd-link2 (comment above). Clippya3f475084b7419b96e4548bcpassed. - Panic ratchet at this exact head 571a44d0116, which doesn't count the FFI crate: step
0bba00f98332c8ca75703b5f: red only on the base redcmux-rd-link2. At 2973a299dc6 (d493a83461ff7f5ce99c0579) its only red was the base redcmux-rd-link: clippy::indexing_slicing 2, which #32559 fixes (comment 6588758367). - Earlier, at 2973a299dc6's proof ref 956947aefd6: clippy
56f109a85d57fa6be42f9d71passed andtest agent_cursore4a1abb12bf465e3a631ed93passed. - Clippy at 36495005df9:
d2794f2b4aee6930a3af2003passed. prepush-gate.py: no new violations.
Review follow-up at 544894685e2 (lead order):
- The header now names the anchor and reason numbers:
CMUX_CURSOR_ANCHOR_*andCMUX_CURSOR_REASON_*. The Rust side uses constants of the same names. A new test checks that everyCMUX_CURSOR_*define equals the Rust constant of the same name, so a hand-written number that drifts fails a test. Step B adds the same check on the Swift side, against the header the module map exposes. - The new vector
row_scrolled_out_bottomproduces anchor 5 (columnEdge.bottom), and a new test checks that the vectors produce all eight anchors. The rules' answers don't change; an independent Python copy of the rules also gives all 27 vectors. - Red at 4ce37ac5041 (the two tests and the Rust constants, with no header constants and no new vector):
test agent_cursor_typed, step3b3cfaf9e28741be9ddac0e0, failed: the 2 new tests failed and the other 4 passed. - Green at 91b8699ce49 (544894685e2 on the proof ref, same trees):
test agent_cursor12513a33e35827a246f7b72c, clippyc47af9e76b8640181562a631, panica7f7e7df7a4b7c0924832f0d. Panic at the head:357ef43debbe4065ab060c96. Results: test passed 12 of 12; clippy passed; both panic runs are red only on the base redcmux-rd-link2 (#32559), andcmux-layout-reducer-ffishows 0indexing_slicing. - Head c605febc8f5 is cmux-bot's merge of feat ecce1e0d8f9. This PR's diff is unchanged, and the merge brings in #32559. The panic ratchet at this head, step
f065bfde57d8f7b7a29e7295, PASSED ("cmux-tui-rust-check: green"). The base red is gone. Receipt: SHIP at c605febc8f5 (comment 6590643422). - The review receipt at 571a44d0116 doesn't cover this head. A new review runs here.
Not run:
- Swift: this PR changes no Swift.
- The typed call's own speed: step B measures it on an M6 mini against the old Swift resolver and reports it next to the JSON numbers above.