Skip to content

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) - #31781

Closed
lawrencecchen wants to merge 10 commits into
feat-cmux-nextfrom
rust-migration/cursor-visibility
Closed

lawrencecchen wants to merge 10 commits into
feat-cmux-nextfrom
rust-migration/cursor-visibility

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

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 9ab3235 (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 ebe44ba: step ff2f9d6976777a31aba6dd0e passed, 3 of 3.
  • Non-finite red at 390575f (the test before the codec): step f443e12824c30e91ff7c5fe6. PENDING
  • Non-finite green at 78cbd40: 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 (933db36), which is 51e3894 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 deb9c9e (the Swift half before the non-finite change): fcc56a985698a6dcbaa2e6aa passed.
  • Swift compile-check at 78cbd40: 5600d8cb58e1f9195d7e8df1. PENDING
  • Old Swift rules under the new Swift vector suite (expectations = old behavior): never-merge ref rust-migration/cursor-visibility-oldswift (4cfb0fc) is this head with the old resolver restored. Compile 30c736448ac1883d5410de10, then AgentCursorVisibilityVectorTests on a Mac. PENDING
  • Review receipt for the Rust half at 51e3894 (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:

…Rust rules (failing)

The Rust home of the agent cursor visibility rules (backlog B9) gets its
types and the replay of schemas/agent-cursor-visibility/vectors.json, ported
from the deleted Swift AgentCursorVisibilityVectorTests, plus the page scale
case. The resolver is a stub that answers notDrawn(tabClosed): 25 of 26
vectors fail until the rules move in the next commit.
The rules of CmuxNextAgentCursorVisibility's AgentCursorVisibilityResolver,
moved as they are: the window that shows (else lists) the target's
workspace, minimized and other-Space windows draw nothing, another
workspace anchors at its sidebar row (else the window edge), a pane off
the active screen at the window edge, a scrolled-out column or page at the
strip edge it left through, a background tab at its chip (else the strip),
and a shown page draws in its viewport with its clip, zoom and scale.
CGRect semantics: sizes are standardized, intersection is none when the
rects do not meet. No behavior change; the Swift copy goes in PR 2.
…SON channel

cmux_layout_reducer_json answers agent_cursor_visibility ({target,
snapshot} in, the visibility in the vectors' expect shape out) from
cmux-focus-core's rules. cmux-app-ffi links this crate already, so the next
CCmuxAppFFI release carries the op; the Swift resolver calls it in PR 2.
The C ABI test sends every vector through the op.
@lawrencecchen lawrencecchen added the risk:low Risk tier (docs/ci/risk-tiers.md) label Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 78cbd40: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr31781 -f ref=rust-migration/cursor-visibility -f platform=macos -f backend_mode=local

red: cmux-next checks (god files, concurrency, crash safety, l10n); no finished base run on 2429d60 to compare; do not merge until green; see lane-rules

Default changes

This pull request changes 2 defaults at 78cbd40da813 (what everyone gets without configuring anything):

  • tunable-source.CmuxNextApp/Control/DebugAgentCursor.swift:encode: "840eed3b1fde8c381f685895220e2714abb46dae8a92cf89cc86df95902a4804" -> "cbd9ef3abf1f3c464b8214b3122a72e0fd91b3fc70b95820431c809107fc2544"
  • tunable-source.CmuxNextApp/Control/DebugAgentCursor.swift:name: "26150b1c49bc5cab04e35e5e68d797ea51fd8885703ecd2d5930d4edf8bb7d83" -> (none)

Needs a review. It passes with an independent review receipt on this commit: a comment or review whose first line is Review (<reviewer>): SHIP at 78cbd40da8132f860ec439f97103998a4aca729f, from a member, not a self-review (the newest verdict naming the head decides; a later BLOCK beats an earlier SHIP), or with an approving review on this commit from @austinywang or @teamleaderleo (the call owners in .github/defaults-call-owners.json, never the author). A receipt posted as a review re-runs this check; after a plain comment, re-run it. The needs a call label alone does not pass, and a new push needs a new receipt.

Regenerate the snapshot with python3 scripts/ci/defaults_guard.py snapshot.

@github-actions github-actions Bot added risk:normal Risk tier (docs/ci/risk-tiers.md) and removed risk:low Risk tier (docs/ci/risk-tiers.md) labels Nov 2, 2026
@blacksmith-sh

blacksmith-sh Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Found 14 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmp7quv1k_y/summary-fail.json (live):/summary-fail.json (live): View Logs
/tmp/tmpigec2yp5/summary-missing.json (live):/summary-missing.json (live): View Logs
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows/
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows
View Logs
[new_workspaces] the window draws no frames/[new_workspaces] the window draws no frames View Logs
[tabs] debug.tab_switch timed 0 of 40 switches/
[tabs] debug.tab_switch timed 0 of 40 switches
View Logs
[tabs] the window draws no frames/[tabs] the window draws no frames View Logs
main.DeveloperIdAppTests.test_download_prints_the_active_profile_only/
test_download_prints_the_active_profile_only
View Logs
stubs: /System/Library/Frameworks/Security.framework/Versions/A/Security has no stub/
Security has no stub
View Logs
stubs: CoreFoundation.tbd: install-name /System/Library/Frameworks/
CoreFoundation.framework/Versions/A/CoreFoundation, missing 1: ['_CFRetain']/
CoreFoundation, missing 1: ['_CFRetain']
View Logs
stubs: CoreFoundation.tbd: install-name /System/Library/Frameworks/
CoreFoundation.framework/Versions/A/CoreFoundation, missing 1: ['_log2']/
CoreFoundation, missing 1: ['_log2']
View Logs

...and 4 more test failures. View all on Blacksmith: run 41391178939, run 41391178721

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

…ules over the app FFI (B9 PR 2)

AgentCursorVisibilityResolver keeps its signature and becomes a thin call:
it sends {target, snapshot} to cmux_layout_reducer_json
"agent_cursor_visibility" (cmux-focus-core, through CmuxNextFocus's
FocusReducer.call) and decodes the answer into AgentCursorVisibility. The
Swift copy of the rules is deleted (window choice, Space and minimize
checks, sidebar row and window edge anchors, column edges, chip and strip
anchors, the visible clip). AgentCursorAnchor gets its wire name, which
the debug socket now uses instead of its own copy.

CmuxNextAgentCursorVisibilityTests replays the shared vectors through the
call, so the wire between Swift and Rust is proved both ways; it needs the
CCmuxAppFFI re-pin that carries the op (#31781).
…s-core tests embed

Also lists schemas/settings/source/agent-retry.json, which acpmux embeds
and the key missed on the base (tree-key-embeds.test.sh red at 0511de2).
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (claude-review-subagent): SHIP at 51e3894

What I checked:

  • Rules, line by line against AgentCursorVisibilityResolver.swift. They match: tab closed; the front-most window that shows the workspace, then one that lists it; minimized, then other Space; the sidebar row or the window edge; the first pane with the id, which must have both frame and clip; the frame shows test, then a column edge; selected tab plus page, else the chip, then the strip, then frame ∩ clip; the viewport shows test; visible with viewport ∩ clip and scale ?? 1.
  • CGRect rules: min/max/width/height standardize negative sizes. intersection returns none only when the rects are apart, and a touching edge gives a zero-size rect, as CGRect does. The > 0.5 thresholds and the ± 0.5 side tests match. span matches overlap. The window edge uses minX, minY, 4 and abs(height). Every overlap() call comes after a passing shows, so its fallback never runs.
  • Optionals: shownWorkspace == workspace and selectedTab == target compare the same way in both languages.
  • Wire in: every Codable key decodes. The names are camelCase. nil optionals that are left out decode as None. Non-optional fields are required, as Swift always writes them. Dictionaries with String keys decode as maps. Unknown keys are ignored.
  • Wire out: the answer is tagged by kind (visible, hidden or notDrawn). Anchor strings follow the vectors (columnEdge.<side>), and reason strings equal the Swift raw values. scale is always written.
  • Tests: the red commit 9ab3235 has a stub that answers notDrawn(tabClosed). With it, 25 of 26 vectors and the scale test fail. The test files did not change after the red commit.
  • Compile reading of the FFI crate: it is rlib + staticlib on edition 2024, so the c"" literal and the unsafe call are valid. The deps (cmux-focus-core, serde_json) are already there, the include_str! path goes up to the repo root, the imports are all used, and &String == &str resolves. I found no type, borrow or clippy issue in either crate.
  • Layering: a pure per-window rule in cmux-focus-core, called in process over the existing JSON op channel, follows the precedent of focus_type_ahead and focus_child_window_key (layer-ownership.md L1 and L4).
  • Tree inputs: check_cmux_tui_tree_inputs.py fails on the base (agent-retry.json) and passes on the head. tree-key-embeds.test.sh passes on a head export.

Notes (non-blocking):

  • cmux-layout-reducer-ffi/tests/agent_cursor_visibility.rs has not been compiled or run anywhere yet. Run it on the fleet before the Swift PR re-pins.
  • agent_cursor_visibility_op_refuses_a_malformed_request would also pass with no op arm, because an unknown op also returns ERR_INVALID. The vector replay test is the one that proves the op.
  • Rust standardizes the visible.viewport and workspaceRow rects, and Swift returns them as given. They differ only for negative sizes, which the app never sends, because AgentCursorRect(CGRect) standardizes. CGRect == treats them as equal.
  • No vector covers columnEdge.bottom or the chip fallback to frame ∩ clip.
  • For the Swift PR: JSONEncoder throws on a non-finite Double, and serde_json writes NaN or inf as null. If a converted rect can be .null or infinite, guard it before you encode.
  • The vectors.json description still names the deleted Swift test target as its replayer.

…y wire as strings (failing)

The app's JSONEncoder refuses non-finite numbers, and a CGRect.null the
snapshot builder converts has infinite x and y. The Swift rules gave a
result for such a snapshot; the Rust rules must give the same one.
…y wire as "inf", "-inf", "nan"

Every number of the snapshot and of the answer reads and writes the
strings the Swift side's JSONEncoder/JSONDecoder convertToString and
convertFromString strategies use, so a snapshot with a converted CGRect.null
or an infinite scale gets the result the Swift rules gave it.
…ss the Rust call

The resolver encodes and decodes with the convertToString strategies the
Rust wire reads ("inf", "-inf", "nan"), through a new raw-bytes
FocusReducer.call, so a converted CGRect.null or an infinite scale gets the
result the Swift rules gave instead of a refused call. The vector suite
covers it; the vectors file names its three replays.
@lawrencecchen lawrencecchen changed the title focus-core: agent cursor visibility rules in Rust, as an app FFI op (B9 PR 1 of 2) 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) Nov 2, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator

Review (codex gpt-6-astra high, next-review): BLOCK at 78cbd40

BLOCK at 78cbd40

Visual verdict: no images; Leo tries the build.

  1. [P1][blocking] Packages/macOS/CmuxNext/Sources/CmuxNextAgentCursorVisibility/AgentCursorVisibilityResolver.swift:14 calls the new agent_cursor_visibility operation, but Package.swift:177 still pins CCmuxAppFFI to the pre-change ae1499… release. That binary returns ERR_INVALID; debug builds hit the assertion and release builds silently classify every cursor as tabClosed. Publish and pin an FFI release built from this head, including its checksum, before landing.

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Replaced by #31934 (step A: Rust only, the same Rust commits with no Swift history), under the two-step FFI rule (lead 06:37Z). Step B (the Swift thin call, the vector suite, the deletion of the Swift rules) is on branch rust-migration/cursor-visibility-step-b and opens after the CCmuxAppFFI re-pin is on feat-cmux-next.

cmux-bot Bot pushed a commit that referenced this pull request Nov 2, 2026
… the Swift rules are deleted (B9 step B) (#32028)

* test(focus-core): agent cursor visibility vectors replay against the Rust rules (failing)

The Rust home of the agent cursor visibility rules (backlog B9) gets its
types and the replay of schemas/agent-cursor-visibility/vectors.json, ported
from the deleted Swift AgentCursorVisibilityVectorTests, plus the page scale
case. The resolver is a stub that answers notDrawn(tabClosed): 25 of 26
vectors fail until the rules move in the next commit.

* feat(focus-core): agent cursor visibility rules in Rust (backlog B9)

The rules of CmuxNextAgentCursorVisibility's AgentCursorVisibilityResolver,
moved as they are: the window that shows (else lists) the target's
workspace, minimized and other-Space windows draw nothing, another
workspace anchors at its sidebar row (else the window edge), a pane off
the active screen at the window edge, a scrolled-out column or page at the
strip edge it left through, a background tab at its chip (else the strip),
and a shown page draws in its viewport with its clip, zoom and scale.
CGRect semantics: sizes are standardized, intersection is none when the
rects do not meet. No behavior change; the Swift copy goes in PR 2.

* feat(layout-reducer-ffi): agent_cursor_visibility op in the app FFI JSON channel

cmux_layout_reducer_json answers agent_cursor_visibility ({target,
snapshot} in, the visibility in the vectors' expect shape out) from
cmux-focus-core's rules. cmux-app-ffi links this crate already, so the next
CCmuxAppFFI release carries the op; the Swift resolver calls it in PR 2.
The C ABI test sends every vector through the op.

* refactor(next agent cursor): the visibility resolver calls the Rust rules over the app FFI (B9 PR 2)

AgentCursorVisibilityResolver keeps its signature and becomes a thin call:
it sends {target, snapshot} to cmux_layout_reducer_json
"agent_cursor_visibility" (cmux-focus-core, through CmuxNextFocus's
FocusReducer.call) and decodes the answer into AgentCursorVisibility. The
Swift copy of the rules is deleted (window choice, Space and minimize
checks, sidebar row and window edge anchors, column edges, chip and strip
anchors, the visible clip). AgentCursorAnchor gets its wire name, which
the debug socket now uses instead of its own copy.

CmuxNextAgentCursorVisibilityTests replays the shared vectors through the
call, so the wire between Swift and Rust is proved both ways; it needs the
CCmuxAppFFI re-pin that carries the op (#31781).

* cmux-tui tree key: cover the agent cursor visibility vectors the focus-core tests embed

Also lists schemas/settings/source/agent-retry.json, which acpmux embeds
and the key missed on the base (tree-key-embeds.test.sh red at 0511de2).

* test(next agent cursor): the vector replay lives in CmuxNextAppTests, no new test target

* test(focus-core): non-finite numbers cross the agent cursor visibility wire as strings (failing)

The app's JSONEncoder refuses non-finite numbers, and a CGRect.null the
snapshot builder converts has infinite x and y. The Swift rules gave a
result for such a snapshot; the Rust rules must give the same one.

* feat(focus-core): non-finite numbers cross the agent cursor visibility wire as "inf", "-inf", "nan"

Every number of the snapshot and of the answer reads and writes the
strings the Swift side's JSONEncoder/JSONDecoder convertToString and
convertFromString strategies use, so a snapshot with a converted CGRect.null
or an infinite scale gets the result the Swift rules gave it.

* next agent cursor: non-finite snapshot numbers keep their result across the Rust call

The resolver encodes and decodes with the convertToString strategies the
Rust wire reads ("inf", "-inf", "nan"), through a new raw-bytes
FocusReducer.call, so a converted CGRect.null or an infinite scale gets the
result the Swift rules gave instead of a refused call. The vector suite
covers it; the vectors file names its three replays.

* CmuxNextFocus: public import Foundation for the raw-bytes FocusReducer.call

Its public signature takes and returns Data (InternalImportsByDefault);
compile-check 5600d8cb58e1f9195d7e8df1 failed on it.

* test(focus-core): no redundant clone of the last null rect (clippy -D warnings)

clippy (changed cmux-tui packages), run 41403471881 job 125140181043,
failed on redundant_clone at agent_cursor_visibility.rs:158.

(cherry picked from commit 0355b6f)

* next agent cursor: the FFI glue is its own PLATFORM declarations; the resolver row follows the code

The anchor's wire name moves to AgentCursorAnchor+FFI.swift, and
AgentCursorVisibilityResolver now only encodes, calls cmux-focus-core and
decodes: both are classified PLATFORM in swift-logic-classes.tsv, so the
move lowers swift_logic_lines by the deleted rules.

* test(next agent cursor): AgentCursorVisibilityResolveBench, ns per resolve over the vectors (off unless CMUX_AGENT_CURSOR_VISIBILITY_BENCH=1)

The perf A/B of B9 step B: the per-agent-input-event resolve, Swift rules
against the Rust call, run alone by scripts/measure/swift-test-bench.sh.

* next agent cursor: the anchor's wire-name extension is nonisolated, as the enum

Compile-check 3bcc994ff74702812b1e11a1 failed: the target's default
MainActor isolation made init?(wireName:) MainActor, and the resolver's
nonisolated decode could not call it.

* next agent cursor: the anchor wire names are a MIRROR of the Rust names; doc comments name the Rust rules

AgentCursorAnchor+FFI.swift copies cmux-focus-core's Anchor::as_str/parse
by hand, as DaemonCapability copies Rust capability names: MIRROR, not
PLATFORM (review note on #32028, lead decision). FocusReducer's doc lists
the agent_cursor_visibility call; AgentCursorVisibilitySource's no longer
names a Swift resolver.

This branch was previously deployed

3 inactive deployments
artifacts — 78cbd40d Deployed Nov 2, 2026 by lawrencecchen via publish tree-addressed cmux-tui #99348
sentry-dsyms — 78cbd40d Deployed Nov 2, 2026 by lawrencecchen via upload cmux-tui dSYMs to Sentry #99348
ci-routing-agent — 78cbd40d Deployed Nov 2, 2026 by lawrencecchen via cmux-tui tree preflight #99348
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants