Skip to content

Hover rules in Rust, step B: Swift calls the Rust hover rules; the Swift reducers are deleted - #31836

Open
lawrencecchen wants to merge 17 commits into
feat-cmux-nextfrom
rust-migration/hover
Open

lawrencecchen wants to merge 17 commits into
feat-cmux-nextfrom
rust-migration/hover

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Step B of the hover FFI move (B8): the app's hover rules now run in Rust. Step A (#31927) landed the Rust rules and their C ABI, and the re-pins #32391 and #32459 put them in the app's CCmuxAppFFI. This PR switches the Swift types to call them and deletes the Swift copies of the rules. Nothing the person sees should change: the same rules, the same timing values, now in one place.

This PR changes no Rust: git diff origin/feat-cmux-next...HEAD -- cmux-tui is empty, and Package.swift's pin is feat's. So the Rust panic step does not apply.

What changes

The public Swift API stays the same, so HoverCardCoordinator, HoverCardPanel, TooltipPresenter and their tests do not change.

  • HoverCardMachine keeps the Rust machine's state as the C struct and calls cmux_hover_card_reduce once per event (no JSON, no heap for effects). HoverCardHandles maps each HoverTargetID to the 64-bit handle the rule compares, and converts events and effects.
  • HoverIntent (tooltips) calls cmux_hover_intent_call (HoverIntentHandles holds the target handles). CGRect.isAimedAt calls cmux_hover_is_aimed_at.
  • HoverCardEscapeGeometry (init, zone, room, rowBand, edgeGap) calls the C functions. keepsCard(at:) reads the Rust zone and maps it, with no rule of its own. rowGap stays a Swift constant (0 pt): the C ABI exports no row gap, and moving one design token would need a new step A.
  • HoverCardTiming and HoverIntentTiming.standard read cmux_hover_timing; the tunables' seconds go through cmux_hover_duration_from_seconds.
  • GroupHoverCardSummary.make calls the group_hover_summary JSON op once per card show (GroupHoverCardSummaryWire) and puts its typed lines into the localized strings.
  • webviews/test/hover-intent-parity.test.ts reads cmux-motion's hover/timing.rs, and two comments in webviews/src/ui/hoverIntent.ts name it. This is this PR's own diff, not the feat merge's: the pages keep a copy of the native hover timing (HOVER_INTENT), and the test that holds the two equal read HoverIntent.swift. That Swift file no longer holds the numbers (they come from Rust), so the test now reads the Rust file. The values are unchanged (450, 500, 200 and 300 ms).
  • plans/cmux-next/layer-ownership.md: the hover card and tooltip rules are Rust's; hit tests, the timer and the panel stay Swift's.
  • HoverCardSweepBench (off unless CMUX_HOVER_SWEEP_BENCH=1) measures the main-thread cost per pointer move of a sidebar sweep, for the perf A/B below.

Swift logic lines

python3 -I scripts/cmux-next/swift-logic-lines.py --repo . --rev <sha> --json:

sha swift_logic_lines mirror_lines unlisted lines
base (merge base with feat) 7361813 421367 71121 172468
head f7d0d97 420569 71121 172468

-798 logic lines. The C glue and the JSON wire are their own declarations, classified PLATFORM in plans/cmux-next/swift-logic-classes.tsv. The bench added since is a test file (not counted).

Tests

  • Swift compile at the head: job 7f664a66c0e59bf8fe0f4bca (result here when it ends).
  • Swift suites on a Mac (proof-step.py): WorkspaceHoverCardBehaviorTests, HoverCardEscapeGeometryTests, HoverCardRowAlignmentTests, HoverCardWindowClampTests, HoverCardFlushTests, GroupHoverCardSummaryTests, TabHoverCardTimingTests, TooltipHoverIntentTests, ThemedTooltipTests and the SidebarHoverCard*Tests (job ids here after the compile passes).
  • bun test test/hover-intent-parity.test.ts at the head: 1 pass, 11 expect() calls.
  • Web lint (python3 scripts/web_lint_prepush.py --base origin/feat-cmux-next --head HEAD, the pre-push hook's and CI's bun run check for webviews) at 06911b5: "web-lint: web lint passed in 24.7s (webviews)".
  • The Rust rules' own tests are step A's (Hover rules in Rust, step A: the hover card machine, tooltip intent, safe triangle, row card placement and group summary, with their C ABI (no Swift change) #31927).

Perf

Mini scheduler A/B, both arms on cmux18, --measurement, release build, HoverCardSweepBench: arm A the Swift rules (scratch ref rust-migration/hover-ab-base: the merge base 7361813 plus the bench), arm B this head. Specs rmhv-b8-ab-r1-a.json and -b.json; results here when they end. Main-thread time per pointer move must not rise.

Risk

risk:normal. Every hover card and tooltip runs through the new path; the Swift behavior suites above prove the same behavior.

Changelog

none

Observed / Not run

Observed: the merge with feat (no Rust and no pin diff), the line accounting, the pre-push gate, the web parity test and web lint.

Not run yet: the Swift compile, the Mac suites and the perf A/B (all queued above). No capture: no UI changes.

… tests (red: stub bodies)

The Swift hover reducers (HoverCardMachine, HoverIntent, the safe triangle
CGRect.isAimedAt, HoverCardEscapeGeometry, HoverCardTiming,
GroupHoverCardSummary) move to Rust: cmux-motion `hover` and
cmux-layout-reducer `group_hover_summary`, with a typed C ABI in cmux-app-ffi
(hover.rs, the Hover section of cmux_motion_ffi.h) and the JSON op
`group_hover_summary`.

This commit has the API, the C forms and the tests, ported from
WorkspaceHoverCardBehaviorTests, HoverCardEscapeGeometryTests,
HoverCardRowAlignmentTests, HoverCardWindowClampTests,
GroupHoverCardSummaryTests and the machine and intent halves of
TabHoverCardTimingTests and TooltipHoverIntentTests. The rule bodies are
stubs (no effects, zero geometry), so the tests fail: the red step.
…ow card placement and group summary in Rust

The bodies of the rules the previous commit tested, ported line for line
from Swift with the same inputs and outputs: HoverCardMachine (phases,
delays, aiming, bridging, pins, an owner's card), HoverIntent (tooltip warm
group), CGRect.isAimedAt (the safe triangle), HoverCardEscapeGeometry
(row band, room, placement, zones), HoverCardTiming and HoverIntentTiming
(Leo's and Lawrence's tuned values, unchanged), and GroupHoverCardSummary
(as typed lines; the app keeps the localized words and the age formatter).
Core Graphics rect rules (standardized edges, half-open contains, null on
an over-inset) and Swift's min, max and rounding are kept exactly.

No Swift change: the app keeps the pinned CCmuxAppFFI until the re-pin; the
Swift half (calling these and deleting the Swift copies) follows it.
… are deleted

The Swift half of the hover move (one PR with the Rust change, as the
FFI rule of 2026-11-02 04:35Z asks). Same public Swift API, so the
coordinator, the panel, the tooltip presenter and their tests do not change:

- HoverCardMachine keeps the Rust machine's state as the C struct and maps
  each HoverTargetID to a 64-bit handle; reduce is one cmux_hover_card_reduce
  call per event (no JSON, no heap for effects).
- HoverIntent (tooltips) calls cmux_hover_intent_call; CGRect.isAimedAt calls
  cmux_hover_is_aimed_at.
- HoverCardEscapeGeometry, room, rowBand and edgeGap call the C functions.
- HoverCardTiming and HoverIntentTiming.standard read cmux_hover_timing;
  the tunables' seconds go through cmux_hover_duration_from_seconds.
- GroupHoverCardSummary calls the group_hover_summary JSON op once per card
  show and puts its typed lines into the localized strings.
- webviews hover-intent-parity.test.ts reads cmux-motion's timing.rs.

Waits for the pre-merge FFI release: the pinned CCmuxAppFFI has none of the
cmux_hover_* symbols, so this does not compile until the repin.
…ed value types (REVIEW.md: pure values do not inherit @mainactor)
@lawrencecchen lawrencecchen added the risk:normal 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 06911b5: 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=pr31836 -f ref=rust-migration/hover -f platform=macos -f backend_mode=local

Passes: CI passes on 18b6e2c9d4.

CI passes on 18b6e2c9d4 (run 41550255291 attempt 1).

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

web and size checks: green

Default changes

This pull request changes 19 defaults at 18b6e2c9d463 (what everyone gets without configuring anything):

  • tunable-source.CmuxNextDesign/HoverCards/GroupHoverCardStrings.swift:allIdle: "8246e988b98dc43f28b082158304985112fb3a693fc8fd59425575118f120e57" -> (none)
  • tunable-source.CmuxNextDesign/HoverCards/GroupHoverCardStrings.swift:branches: "86cc177cd0d38fdb968ac155c4f398fc699627f06f1a9382a5ae51b4a11ad08b" -> (none)
  • tunable-source.CmuxNextDesign/HoverCards/GroupHoverCardStrings.swift:count: "2d747d6a1134703dec631a35d6c8c6e7fc4c2d38cf44e2788af77104fa3528e7" -> (none)
  • tunable-source.CmuxNextDesign/HoverCards/GroupHoverCardStrings.swift:failed: "39bc6df019b2f1348b61d287b00dd9b446a2f646062c04d24078edf6ecdece21" -> (none)
  • tunable-source.CmuxNextDesign/HoverCards/GroupHoverCardStrings.swift:justNow: "15695b109ac06514133fd51dc9fd8a0c6c7a4a204e7b82c5359385a6e40ffc8c" -> (none)
  • tunable-source.CmuxNextDesign/HoverCards/GroupHoverCardStrings.swift:latest: "e0cda1080666b19aeba9db3a52faeae63ff0fc093506266e56db3110751dfb81" -> (none)
  • tunable-source.CmuxNextDesign/HoverCards/GroupHoverCardStrings.swift:more: "e6e103a5963830dbfec03ce63b6d6d6578573a19e8772c20f8e31a477421c5ae" -> (none)
  • tunable-source.CmuxNextDesign/HoverCards/GroupHoverCardStrings.swift:ports: "38f5be67ec4a517166914ce83ed1d25e8f24edbc4f9cd59ee55f6181645d8bcf" -> (none)
  • tunable-source.CmuxNextDesign/HoverCards/GroupHoverCardStrings.swift:unread: "5136e611d16f92d0944fd096600851d72fed31d4d13853f1d4f238de97cccc7e" -> (none)
  • tunable-source.CmuxNextDesign/HoverCards/GroupHoverCardStrings.swift:waiting: "755b7b265a5dac0f23d63668cdabbbeeff39aa27bd3da8f1b704a36d2d6642e4" -> (none)
  • tunable-source.CmuxNextDesign/HoverCards/GroupHoverCardStrings.swift:working: "bf101fba15c5a7e6cbb6b9487c5652faac8dc85ae05f48c4aa94b1274ed0c126" -> (none)
  • tunable-source.CmuxNextDesign/HoverCards/GroupHoverCardSummary.swift:age: "75309727a3fa1ef3024aabf8b1f6b6f6a0487573f0304502002f10224a33ebb4" -> (none)
  • tunable-source.CmuxNextDesign/HoverCards/GroupHoverCardSummary.swift:ageFormatter: "ea9e1979ea476d92dc87061f711f4e6333d58a2b4dbc41297d6590da3ed1a8bb" -> (none)
  • tunable-source.CmuxNextDesign/HoverCards/GroupHoverCardSummary.swift:joined: "a62ce0ceec977e16acd62eb86c2c76d0523e09d784320a23050f942c5c2137b5" -> (none)
  • tunable-source.CmuxNextDesign/HoverCards/GroupHoverCardSummary.swift:make: "4c42a9b8662d11a680ef2add79a913bdb0b2ea2ec293f6642ded155841bd0307" -> "33813f3d48cd9325c2e1f8a33bf1327c745c292adfa3d85d2ca24c442ad7874c"
  • tunable-source.CmuxNextDesign/HoverCards/GroupHoverCardSummary.swift:maxListed: "e0e4dd56c498dd439a7c746a037e5bddeec4657393e870ad81b5680cbcdee486" -> (none)
  • tunable-source.CmuxNextDesign/HoverCards/GroupHoverCardSummary.swift:status: "6b4933ee0b358d6038e7c178f80e7eb47ab12db951e8f10fd92f8ab8154fee18" -> (none)
  • tunable-source.CmuxNextDesign/HoverCards/GroupHoverCardSummary.swift:unique: "536fc3d83b9da332aa165add76e7be27b4284525a12184a1efec3ce743396759" -> (none)
  • tunable-source.CmuxNextDesign/HoverCards/GroupHoverCardSummaryWire.swift:run: (none) -> "7318b37e3fc7b10360d348814b6f803455603c523ce7ecdc90e3ee47bf53c4ce"

Signed off on this commit by review (next-review patch-id; carried), carried from 56a6e6bfff98 across base merges.

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

Gallery before/after for 18b6e2c: 16 states rendered, 0 images differ, 3020 more touched states not rendered. Images are in the run's artifact, not on this public PR; the independent review of this head reads them on its own, so the PR needs no hand-made screenshots of these states.

gh run download 41550255045 -R manaflow-ai/cmux -n gallery-advisory-31836-18b6e2c9d4633a54927dc910b96057b9949b1e9f (then read gallery-review.json)

States
State Theme Status Diff Picked by
agent-pane.composer/font-size-chat-14 Apple System Colors Light unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer/font-size-chat-14 Apple System Colors unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer/font-size-new-chat-14 Apple System Colors Light unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer/font-size-new-chat-14 Apple System Colors unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer/font-size-new-tab-14 Apple System Colors Light unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer/font-size-new-tab-14 Apple System Colors unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer/font-size-chat-21 Apple System Colors Light unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer/font-size-chat-21 Apple System Colors unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer/font-size-new-chat-21 Apple System Colors Light unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer/font-size-new-chat-21 Apple System Colors unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer/font-size-new-tab-21 Apple System Colors Light unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer/font-size-new-tab-21 Apple System Colors unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer/font-size-chat-28 Apple System Colors Light unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer/font-size-chat-28 Apple System Colors unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer/font-size-new-chat-28 Apple System Colors Light unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer/font-size-new-chat-28 Apple System Colors unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer-pickers/closed Apple System Colors Light unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer-pickers/closed Apple System Colors unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer-pickers/pill-rail Apple System Colors Light unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer-pickers/pill-rail Apple System Colors unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer-pickers/model-menu Apple System Colors Light unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer-pickers/model-menu Apple System Colors unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer-pickers/model-more Apple System Colors Light unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer-pickers/model-more Apple System Colors unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer-pickers/model-focus Apple System Colors Light unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer-pickers/model-focus Apple System Colors unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer-pickers/model-press-drag-release Apple System Colors Light unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer-pickers/model-press-drag-release Apple System Colors unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer-pickers/effort-menu Apple System Colors Light unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer-pickers/effort-menu Apple System Colors unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer-pickers/effort-focus Apple System Colors Light unchanged 0% imports hoverIntent.ts (1 hops)
agent-pane.composer-pickers/effort-focus Apple System Colors unchanged 0% imports hoverIntent.ts (1 hops)

Pick other states with a Gallery: entry.id, entry.id/variant line in the PR body.

@blacksmith-sh

blacksmith-sh Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Found 4 test failures on Blacksmith runners:

Failures

Test View Logs
test/agent-pane-wheel-glide.test.ts/agent-pane-wheel-glide.test View Logs
test/agent-pane-wheel-glide.test.ts/agent-pane-wheel-glide.test View Logs
test/model-menu-compact.test.tsx/model-menu-compact.test.tsx View Logs
test/model-menu-compact.test.tsx/model-menu-compact.test.tsx View Logs

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

…ent.hover; HoverIntent stays Hashable

- HoverIntent.hover passed the mutating `handle` to `map` unapplied, which
  Swift refuses (review of fe72ef2).
- HoverIntent keeps its public Hashable conformance, by hand: the C state
  struct has none.
- GroupHoverCardSummary: the list line no longer shadows `listed` and `more`.
- layer-ownership.md: the hover card and tooltip rules are Rust's now; hit
  tests, the timer and the panel stay Swift's.
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (claude-review-hover): SHIP at 95443c5
reviewer-session: 19584883-8e64-4115-ba03-b890ea0ddce6
author-session: rm-hover
R9: pass | Hover card and tooltip rules row in plans/cmux-next/layer-ownership.md (Rust cmux-motion hover and cmux-layout-reducer group_hover_summary, in process through cmux-app-ffi; Swift hit-tests, runs the timer and the panel, and words the lines)
perf claim: none

Read-only review of origin/feat-cmux-next...95443c5 (no build or test on this machine). Correctness first.

Checked and found equivalent to the merge-base Swift

  • HoverCardMachine: every branch of hit, deadline, dismiss, suppress, unsuppress, targetRemoved, owned, pin, resume, arm and end; token order and effect order are the same. No event makes more than 3 effects (end + resume is the largest).
  • HoverIntent, the safe triangle, HoverCardEscapeGeometry, room and rowBand: Swift min/max argument order, rounding modes (.up/.down/.toNearestOrAwayFromZero), standardized edges, half-open contains and the null rect from insetBy are kept.
  • Group summary: the tie keeps the first member (max(by:)), empty port parts from split are dropped by unique, CharacterSet.whitespaces is Zs plus tab, the 60 s "Just now" limit (NaN too), and the age formatter gets now minus date (both sent as timeIntervalSinceReferenceDate).
  • C ABI: each struct in cmux_motion_ffi.h matches its #[repr(C)] struct in cmux_motion::hover::c (field order and types, bool, size_t), and the 13 function signatures match. Null pointers are checked, unknown kinds read as safe values, no panic path.
  • Swift wrappers: the handle map keeps the phase target and the last hit (the only handles the rule compares); show effects are mapped before prune; an unknown id for targetRemoved sends 0, which is never given out. Duration to ns clamps to Int64. pinLifetime and stillPointerDelay defaults are the same. Localized lines are the same. No main-thread block (the JSON call runs once per card show).
  • webviews/test/hover-intent-parity.test.ts reads HoverIntentTiming::STANDARD in timing.rs, the value the app gets through cmux_hover_timing, and still checks the tunables' defaults.

Fixed in 95443c5 (from my review of fe72ef2)

  • HoverIntent.hover passed the mutating handle(_:) to map unapplied (compile error). It now takes the handle in a closure first.
  • HoverIntent is Hashable again (hand-written == and hash over phase, next token, delay and warmFor, the same fields the old synthesized conformance used).
  • The list line in GroupHoverCardSummary no longer reuses the names listed and more.
  • layer-ownership.md has a row for the hover rules.

Nits (not blocking)

  • Two Unicode inputs differ from Swift (stated in the PR body): branches "é" (U+00E9) and "e" + U+0301 are one branch in Swift (String equality is canonical) but two in Rust; a "," followed by a combining mark is one Character in Swift (no split) but Rust splits there.
  • serde_json without float_roundtrip can read now/last_activity 1 ULP off (about 1e-7 s of age; no visible effect).
  • A Duration under 1 ns (in attoseconds) now reads as no delay; real values are whole milliseconds.
  • HOVER_INTENT in webviews is still a hand copy kept equal by a parity test (L5); that was so before this PR.

Not run by me: Swift compile (the pinned FFI release lacks the new symbols, stated in the PR), Swift Mac suites, perf A/B. Rust: fleet job 8ac118bd8b58417df771344f green (75/75) at b581621, red job 9651cf0dfca75b673f29a7a8 at the stub commit, as the PR states.

…ce and splits ports on grapheme clusters, as Swift's String and Character do (red: Rust compares bytes and splits on the byte ',')
…s (NFC equality, comma split on grapheme clusters)

A move keeps the behavior (lead 2026-11-02): Swift's `Set<String>` treats a
precomposed and a decomposed "é" as one value, and `split(separator: ",")`
splits on the Character ",", so a comma with a combining mark is no
separator. `unique` now compares NFC keys (first value seen is listed) and
the port split walks grapheme clusters. unicode-normalization and
unicode-segmentation are workspace dependencies already in the app FFI
lock (cmux-palette-ranker, cmux-focus-core); no new package reaches the app.
…ons (HoverCardHandles, HoverIntentHandles, GroupHoverCardSummaryWire); the logic table classifies the thin hover calls PLATFORM
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (claude-review-hover): SHIP at 4b073d1
reviewer-session: 5ec6a888-df26-4dd0-9f48-695368f5bdd8
author-session: rm-hover
R9: pass | Hover card and tooltip rules row in plans/cmux-next/layer-ownership.md (Rust cmux-motion hover and cmux-layout-reducer group_hover_summary, in process through cmux-app-ffi; Swift hit-tests, runs the timer and the panel, and words the lines)
perf claim: none

Read-only review of 95443c5..4b073d1, the author's commits only (c4776fd, 4ead4a4, 4b073d1). I skipped the merge f2490c1 of origin/feat-cmux-next (merge base e88c4a5). It changes only the PR's cmux-tui/Cargo.lock (cmux-host-budget) and brings in swift-logic-classes.tsv. My earlier review of the whole PR (receipt at 95443c5) still applies to the files these commits do not touch.

c4776fd + 4ead4a4 (group summary Unicode)

  • The red tests cover the two cases from my review: a precomposed and a decomposed "é" (branches and ports), and a comma followed by U+0301 in a port list.
  • unique now keys on NFC and lists the first value seen. This is Swift's Set<String> (canonical equivalence) with first-seen order.
  • split_on_comma splits on extended grapheme clusters that are exactly ",". This is Swift's split(separator: ",") on Character. Empty parts are dropped later by unique after the trim, as before.
  • Cargo: both crates are cmux-tui workspace dependencies (unicode-segmentation = "1", unicode-normalization = "0.1"). Each has one version in both locks (0.1.25, 1.13.3), and their own deps (tinyvec) were already in the app FFI lock. So the two added lines in each lock are what cargo writes: names with no version suffix, in alphabetical order. The app FFI lock has no proptest, because cmux-layout-reducer is a path dependency there, not a member. Against the merge base, only those two lines change in each lock.

4b073d1 (glue split)

  • The behavior is the same. HoverCardHandles and HoverIntentHandles keep the same handle maps and prune rules. reduce and call still map the effects before prune. GroupHoverCardSummaryWire has the same wire and the same words.
  • The declarations compile in principle:
    • package members of CmuxNextDesignTokens (hoverNanoseconds, Duration(hoverNanoseconds:), HoverIntentTiming.rustTable) are used from CmuxNextDesign, a target of the same SwiftPM package. An internal import is enough for internal use.
    • Internal types are used only as private stored properties of public structs.
    • HoverIntentHandles<Target>.effects returns [HoverIntent<Target>.Effect] with a typed closure.
    • The nested Codable types sit under nonisolated struct GroupHoverCardSummaryWire, as TasksPreview's do.
  • Logic table: each row key (module, path, kind, name, ordinal) matches a top-level declaration. I find the PLATFORM rows honest. GroupHoverCardSummaryWire.Line.words only maps typed lines to localized strings (join separator, "All idle" when there are no parts, the "+N more" suffix). Which lines show, the counts and the order are Rust's.

Nits (not blocking)

  • HoverCardEscapeGeometry.keepsCard(at:) is still a 4-line Swift copy of Rust keeps_card (row or card keeps the card), and rowGap copies HOVER_ROW_GAP. The C ABI could return keeps_card, or leave it as it is.
  • The earlier nits still apply: 1 ULP JSON float reads, a Duration under 1 ns reads as no delay, and the HOVER_INTENT TS copy (L5) was there before this PR.

Not run by me: anything. The fleet jobs (red 41fb34bc3f1df61721789c48, green 6d821e84132797854e9b7bcf, clippy 19b1283f2af5a1faabb6a29c, fmt b6392995e2e2a3e64abcadbc) were still running when I wrote this. I did not check the swift-logic-lines counts (base 415723, head 414925). The Swift half still waits for the FFI release, as the PR states.

lawrencecchen added a commit that referenced this pull request Nov 2, 2026
…safe triangle, row card placement and group summary, with their C ABI (no Swift change) (#31927)

* feat(hover): step A of the hover FFI move: the hover rules in Rust, with their C ABI and JSON op (no Swift change)

The Rust half of the hover move (B8; #31836 becomes step B, the Swift half,
after the re-pin). Same inputs and outputs as the Swift rules it replaces:

- cmux-motion `hover`: the hover card machine (phases, delays, aiming,
  bridging, pins, an owner's card), the tooltip hover intent, the safe
  triangle, the sidebar row card's placement and zones, and the hover timing
  tokens (Leo's and Lawrence's values, unchanged, with their history).
- cmux-layout-reducer `group_hover_summary`: the group card summary as typed
  lines, with Swift's String rules (NFC equality, comma split on grapheme
  clusters); unicode-normalization and unicode-segmentation are already in
  the app FFI lock.
- cmux-app-ffi `hover.rs` and the Hover section of cmux_motion_ffi.h (ABI 4):
  a typed, batched C ABI (machine as a plain struct, no JSON, no allocation);
  the JSON op `group_hover_summary` on cmux_layout_reducer_json.

The Swift tests of these rules are ported as Rust tests. Proof (fleet, on
rust-migration/hover): red 9651cf0dfca75b673f29a7a8 at 9512791 (stub
bodies, 63 of 75 failed), red 41fb34bc3f1df61721789c48 at c4776fd (the
2 Unicode tests failed), green 6d821e84132797854e9b7bcf at 4ead4a4
(77 of 77), clippy df9bf645ec81bf01dc66a491 at 4b073d1 (pass).

* fix(hover): the duration cast's allow names its reason (the panic ratchet refuses an allow without reason =)

* refactor(hover): the suppression bits are literals (no shift for the arithmetic ratchet to count)

---------

Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>
…ion/hover

# Conflicts:
#	cmux-tui/crates/cmux-layout-reducer-ffi/src/lib.rs
#	cmux-tui/crates/cmux-layout-reducer/src/lib.rs
#	plans/cmux-next/swift-logic-classes.tsv
…a 60-row sidebar sweep through the hover rules (off unless CMUX_HOVER_SWEEP_BENCH=1; the B8 step B perf A/B)
@lawrencecchen lawrencecchen changed the title Hover rules in Rust, step B: Swift calls the Rust hover rules; the Swift reducers are deleted (waits for #31927 and the re-pin) Hover rules in Rust, step B: Swift calls the Rust hover rules; the Swift reducers are deleted Nov 2, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator

Review (codex gpt-6.1-sol xhigh, next-review): SHIP at 06911b5

SHIP at 06911b5
Visual verdict: unchanged by this diff

No actionable findings in the ABI mappings, handle lifetimes or reducer parity. All 16 gallery pairs are identical; native compilation, suites and perf A/B were not executable here.

Follow-ups: none

@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 2, 2026 19:57 Inactive
@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 2, 2026 19:57 Inactive
@teamleaderleo

Copy link
Copy Markdown
Collaborator

Review (next-review patch-id; carried): SHIP at 8a752f7

The independent SHIP at 06911b5 carries to this head: the PR's own change (git diff $(git merge-base feat-cmux-next HEAD) HEAD) has the same git patch-id --verbatim (34ab963ec032a350) at both heads, and no merge between them was resolved by hand. Nothing new to review. A newer BLOCK, or any change to the PR's own diff, ends the carry.

@cursor

cursor Bot commented Nov 2, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 2, 2026 21:58 Inactive
@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 2, 2026 21:58 Inactive
@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 2, 2026 21:58 Inactive
@teamleaderleo

Copy link
Copy Markdown
Collaborator

Review (next-review patch-id; carried): SHIP at 56a6e6b

The independent SHIP at 06911b5 carries to this head: the PR's own change (git diff $(git merge-base feat-cmux-next HEAD) HEAD) has the same git patch-id --verbatim (34ab963ec032a350) at both heads, and no merge between them was resolved by hand. Nothing new to review. A newer BLOCK, or any change to the PR's own diff, ends the carry.

@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 3, 2026 00:05 Inactive
@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 3, 2026 00:05 Inactive
@cmux-bot
cmux-bot Bot temporarily deployed to ci-routing-agent November 3, 2026 00:05 Inactive

teamleaderleo commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Independent adversarial review — HQ #6236 / brief 26: avoid per-pointer temporary Set in hover Step B

Scope / owner: Existing implementation owner @lawrencecchen; reviewed this PR's exact head 18b6e2c9d4633a54927dc910b96057b9949b1e9f. This is a read-only code review plus a locally executed, synthetic Swift optimization experiment. No Mac app compile, allocator trace, native suite or 120 Hz A/B was run by me. The prior SHIP/parity reviews remain valuable; this is an additional performance question, not a claim of an observed UI regression.

Source finding

HoverCardHandles.prune(keeping:) lines 75–83 currently computes

let keep = Set([phaseHolds ? state.target.id : nil,
                state.has_last_hit ? state.last_hit.id : nil].compactMap(\.self))
guard ids.count > keep.count else { return }
ids = ids.filter { keep.contains($0.key) }
handles = handles.filter { keep.contains($0.value) }

HoverCardMachine.reduce lines 139–149 calls this once for every event, including each pointer move. Rust's typed C ABI avoids JSON and allocations inside its reducer, but the Swift wrapper always materializes an optional array, compactMap, and a Set before checking if pruning is needed. That is avoidable hot-path work; I have not counted actual heap allocations in the AppKit process.

The Rust machine can reference only the phase target and last hit. For an equivalent small change preserving the current dictionary contents, replace just the temporary collection with scalar comparisons:

let phaseTarget: UInt64? = (1...4).contains(state.phase) ? state.target.id : nil
let lastHit: UInt64? = state.has_last_hit ? state.last_hit.id : nil
let keepCount = (phaseTarget == nil ? 0 : 1)
              + (lastHit != nil && lastHit != phaseTarget ? 1 : 0)
guard ids.count > keepCount else { return }
ids = ids.filter { $0.key == phaseTarget || $0.key == lastHit }
handles = handles.filter { $0.value == phaseTarget || $0.value == lastHit }

Why this is equivalent: the original set has exactly 0, 1 or 2 distinct IDs. keepCount calculates that cardinality, and both dictionary predicates test membership in the same two possible IDs. It changes neither when filtering runs nor which mappings survive.

Actual experiment (executed, synthetic Linux/Swift)

I wrote and compiled a standalone Swift -O harness mirroring the current HoverCardHandles two dictionaries, ID interning, current prune, the scalar alternative above, and an intentionally more aggressive max-two-handle alternative. The harness ran 300,000 seeded synthetic events through current and scalar variants and compared both complete dictionaries after every event: PASS, including churn, reused targets, removed targets, duplicate IDs and different phases. A separate 300,000-event bounded-map check confirmed the more aggressive variant retains active/last-hit identity, but its stale-ID lifecycle differs, so the scalar variant is the conservative option.

500,000 operations per arm with swiftc -O, Linux Debian 13; several runs (no Mac runtime):

  • Stable one live handle: current ~140–179 ns/event, scalar ~2.7–3.2 ns/event.
  • Stable two live handles: current ~183–198 ns/event, scalar ~2.9–3.6 ns/event.
  • Sixty-ID churn: current ~682–886 ns/event, scalar ~409–512 ns/event.
  • At typical steady state, ~0.14–0.19 μs of isolated pruning cost is removed per move. The 50–65× relative change is for this tiny helper only; these figures cannot establish a user-visible frame-rate benefit. The experiment excludes the C call, mapping effects, CoreGraphics, AppKit, allocator tracing and whole-app input dispatch.

Reproducer procedure: create the same [HoverTargetID: UInt64] / [UInt64: HoverTargetID] mappings, seed 1, 2, or 60 IDs, call each prune on a fixed state 500k times in an optimized executable, and assert map equality after every operation of a seeded 300k-event phase/target/removed-ID sequence. I retained the complete standalone harness as a local artifact in the investigating chat. The code block above is the production-relevant change.

Acceptance / request for the existing owner

  1. Add a focused Swift regression for exact map equality after mixed hit/deadline/targetRemoved/reappear sequences; ensure the nextHandle identity and C effect mapping are unchanged.
  2. Run the existing HoverCardSweepBench at the same pinned FFI on one Mac, before/after only this helper edit; report p50/p95/p99 and allocation count if instrumentation is available, followed by the native hover suites. The broader Swift→Rust hover A/B remains a separate gate.
  3. Check the Swift wrapper for any additional allocation or event buffer churn before describing the whole hover path as allocation-free. The current C-level property can remain as stated.

Disposition: a small, isolated performance improvement for the already-owned #31836; no competing branch or PR created. Treat the measured gain as local synthetic evidence until Mac A/B confirms the end-to-end benefit. Linked back from HQ #6236 brief 26.

Repro now committed in an independent HQ draft (2026-11-03)

The source and acceptance notes are preserved in HQ draft PR #6653: executable Swift probe. Recompiled/reran with Swift 6.2.1 -O on Linux: PASS 300,000 exact two-map-and-next-ID transitions; one/two live entries 124.4→2.3 ns / 170.3→2.2 ns per prune call, respectively (one representative run). All limitations above still apply. The source is in HQ to avoid touching this actively owned cmux branch until the native Mac A/B and suites run.

Optional second probe: zero-effect array conversion

I also added a standalone effect-buffer Swift benchmark to draft HQ #6653. The source at `HoverCardMachine.reduce` always builds `[buffer.0, buffer.1, buffer.2].prefix(count)` before converting effects, even when `count == 0`. On Debian/Swift 6.2.1 `-O`, one million synthetic calls per arm showed current/early-guard ~22.3/6.9 ns at zero effects, and ~26.5/11.8 ns with an assumed 90% zero-effect workload. The guard must preserve the subsequent `handles.prune(keeping: state)` and ordering. This is a small, source-backed optional follow-up (~15 ns per empty event), not a measured Mac regression; prioritize the scalar handle-pruning improvement and native A/B first.

This branch was previously deployed

3 inactive (2 outdated) deployments
ci-routing-agent — 18b6e2c9 Deployed Nov 3, 2026 by cmux-bot[bot] via wait for same-key tree publisher #103062
artifacts — 4f033289 Deployed Nov 2, 2026 by lawrencecchen via publish tree-addressed cmux-tui #100477
sentry-dsyms — 4f033289 Deployed Nov 2, 2026 by lawrencecchen via upload cmux-tui dSYMs to Sentry #100477
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