agent cursor visibility: the rules move to Rust (cmux-focus-core), Swift calls them over the app FFI (B9; waits for the pre-merge FFI release)

Closed
LA
4d ago
+956-99

Waits for the pre-merge FFI release (STATUS 5013). This PR changes an FFI input (cmux-focus-core, cmux-layout-reducer-ffi) and holds the Swift call in the same PR. The Swift call works only after CCmuxAppFFI is re-pinned to a release built from this head, and today a release is built only from feat-cmux-next. Do not add ready-to-land until that release exists and the pin in Packages/macOS/CmuxNext/Package.swift moves to it on this branch.

The agent cursor's visibility rules now live in Rust. They decide where the cursor for a target browser tab draws: on its page, at an anchor in its window, or nowhere. The app asks them over the app FFI's JSON channel (agent_cursor_visibility) and keeps only a thin Swift wrapper. This is the whole move of rust-migration backlog B9. No behavior changes.

Rust (cmux-tui/crates/cmux-focus-core/src/agent_cursor_visibility.rs)

This is a port of the deleted AgentCursorVisibilityResolver rules:

  • The window: the front-most window that shows the target's workspace, else one that lists it. Minimized or on another Space: nothing draws.
  • Another workspace of that window: its sidebar row, else the window edge.
  • The pane off the active screen: the window edge. A column or page scrolled out: the strip edge it left through.
  • A background tab, or a selected page that isn't laid out yet: its chip, else the strip.
  • A shown page: its viewport, its clip, its zoom and its scale (no scale is 1).

Rects follow CGRect rules: sizes are standardized, an intersection is null when the rects don't meet, and "shows" means more than half a point of overlap on both axes.

JSON has no non-finite numbers, so they travel as "inf", "-inf" and "nan" in both directions. Both sides use these strings: Swift through its JSONEncoder and JSONDecoder string strategies. So a snapshot with a converted CGRect.null (infinite x and y) or an infinite scale gets the result the Swift rules gave it.

The op is in cmux-layout-reducer-ffi/src/lib.rs. The request is {"target", "snapshot"}, where the snapshot is AgentCursorVisibilitySnapshot's Codable shape. The answer is the vectors' expect shape plus scale. A malformed request returns ERR_INVALID.

The JSON channel, not a typed C ABI: the app resolves once per agent input event, and again on layout or window changes only while a target is tracked. The snapshot holds at most one pane, one chip and one sidebar row per window.

Swift

  • AgentCursorVisibilityResolver.resolve(target:in:) keeps its signature. It encodes {target, snapshot}, calls the op through the new raw-bytes FocusReducer.call(_:json:), and decodes the answer into AgentCursorVisibility.
  • The Swift rule code is deleted (AgentCursorVisibilityResolver.swift: 38 new lines replace 83).
  • AgentCursorAnchor now has its wire name. The debug socket (DebugAgentCursor) uses it instead of its own copy (app_lines -10).
  • The value types, placement(forWindow:) and the App's snapshot builder stay in Swift.

Tests

  • cmux-focus-core/tests/agent_cursor_visibility.rs replays all 26 cases of schemas/agent-cursor-visibility/vectors.json. It is a port of the Swift vector suite deleted on 2026-10-10. It also covers the answer's wire shape, the page scale and non-finite numbers.
  • cmux-layout-reducer-ffi/tests/agent_cursor_visibility.rs sends every vector through cmux_layout_reducer_json("agent_cursor_visibility"), and checks that malformed requests are refused.
  • CmuxNextAppTests/AgentCursorVisibilityVectorTests.swift replays the vectors and the non-finite cases through the Swift call, and keeps the old placement tests. It is in an existing target, so no new test target is added.

Changelog

none

Observed / Not run

Observed:

  • Rust red at 9ab32359bd6 (a stub resolver), cmux-tui-rust-check.sh test agent_cursor_visibility: step e8bcaea8fadd17bff9d01e80 failed. 3 ran, 2 failed: "25 of 26 vectors differ", and the scale case.
  • Rust green at ebe44ba1049: step ff2f9d6976777a31aba6dd0e passed, 3 of 3.
  • Non-finite red at 390575f10ee (the test before the codec): step f443e12824c30e91ff7c5fe6. PENDING
  • Non-finite green at 78cbd40da81: step 1c9f201aabde9547145ae87c. PENDING
  • FFI crate C ABI test: step 30c4eaa5853744cee8d63021 passed, 5 of 5 (2 of them in cmux-layout-reducer-ffi). It ran on the never-merge proof ref rust-migration/cursor-visibility-ffiproof (933db36027e), which is 51e38946918 plus one commit that builds that crate inside the cmux-tui workspace. The crate is its own Cargo workspace, so the fleet script never builds it. Its op and test are unchanged since then.
  • Swift compile-check at deb9c9e6461 (the Swift half before the non-finite change): fcc56a985698a6dcbaa2e6aa passed.
  • Swift compile-check at 78cbd40da81: 5600d8cb58e1f9195d7e8df1. PENDING
  • Old Swift rules under the new Swift vector suite (expectations = old behavior): never-merge ref rust-migration/cursor-visibility-oldswift (4cfb0fc3518) is this head with the old resolver restored. Compile 30c736448ac1883d5410de10, then AgentCursorVisibilityVectorTests on a Mac. PENDING
  • Review receipt for the Rust half at 51e38946918 (comment 6569177284). This head needs a new review.
  • tree-key-embeds.test.sh passes (the tree key now lists the vectors file and the base's missing agent-retry.json). prepush-gate.py: no new violations.

Not run:

  • AgentCursorVisibilityVectorTests at this head: the pinned CCmuxAppFFI has no agent_cursor_visibility op yet, so it can run only after the pre-merge release and re-pin.
  • Perf A/B of AgentCursorVisibilitySource.resolve main-thread time: it needs the re-pinned FFI.

Non-green checks, classified:

  • Check the app FFI pin: red until the re-pin (see the top).
  • Base findings, also red on unrelated PRs or on files this PR doesn't touch: acpmux tests (lib, hub_integration), tooling-tests display suites, nightly-guards release-notary, rust workspaces / plan (scripts/journeys/tui-kit/rust from #31748; its author was told), and the cmux-tui.yml lint reds that #31697 fixes.