Skip to content

perf(agent-chat): sweeping the pointer across the session rail paints within each frame - #29086

Merged
teamleaderleo merged 6 commits into
feat-cmux-nextfrom
lane/rail-hover-sweep
Oct 31, 2026
Merged

teamleaderleo merged 6 commits into
feat-cmux-nextfrom
lane/rail-hover-sweep

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

Leo: "the left scroll rail still lags when i hover over a lot of it it needs to be faster"

Spec: design/session-rail.md (leo-workspace). Hover magnifies the ticks over 120 ms with no delay, the card opens on hover, and no work is O(rows) per token. The look and every state stay the same. Nothing is removed. Prior art kept: #28494, #28540, #26635, #24131, #25766.

What was slow

React was never the cost: about 0.3 ms and 1 commit per move. The cost was the browser restyling the whole page about 11 times per move.

  1. The running tray's :has() rule read the inline style attribute. bottomStack.css reserved space with :has(> .acpmux-running-tray__fold[style*="overflow"] ...). Chromium cannot scope a [style] test inside :has(). So every inline style write anywhere on the page restyled all of it, about 1,100 elements, and a sweep writes many: each tick bar's transform and the card's placement. The fold now marks its clip with data-fold-clip (clipFold() in fold.ts), and the rule reads that instead. The clip, the reservation and the tray motion are the same as before.
  2. The preview card measured its reply with a clone. Each new prompt appended a cloned reply node, measured it and removed it, and made a new ResizeObserver. Each of those is another restyle. Now the card measures the reply in place: it lifts the line clamp, reads the height and puts the clamp back in the same task. It keeps one ResizeObserver for its whole life, which skips a size it has already fitted.

Performance

Same harness, same run, same Linux Chromium job. "After" is this head. "Before" is this head with the webviews/src changes reverted. Each case is 535 moves: one per painted frame, a 120-tick sweep down and back up, clear of the fades. "Move to card" is the time from pointerover to the first paint that shows the tick active and the card with that tick's prompt. Run: https://github.com/manaflow-ai/cmux/actions/runs/41179784449 (webviews/test/latency, cases chat.rail.hover.sweep.3000 and .100).

metric before after delta harness or run link
move to card p50, 3,000 prompts 20.5 ms 7.9 ms -61% run 41179784449
move to card p95, 3,000 prompts 24.1 ms 9.1 ms -62% same
move to card max, 3,000 prompts 38.8 ms 11.5 ms -70% same
move to card p50, 100 prompts 23.4 ms 7.9 ms -66% same
move to card p95, 100 prompts 26.3 ms 8.7 ms -67% same
style recalcs per move, 3,000 11.4 6.1 -47% same (CDP Performance metrics)
style ms per move, 3,000 28.3 3.9 -86% same
style ms per move, 100 32.5 4.2 -87% same
layouts per move 5.0 3.0 -40% same
frame gap max (both sizes) 33.4 ms 16.8 ms no dropped frame same
React commits per move 1 1 0 same
long tasks 0 0 0 same

Before, each move took longer than a frame, so the tick and the card lagged the pointer by a frame or two. After, each move paints within its frame.

Timers, observers, animation loops

  • No new timer or rAF loop in the product.
  • The preview card's ResizeObserver was one per content change and is now one per card. Its stop condition: it disconnects in the cleanup of the layout effect that created it, when the card unmounts.
  • The harness's sweep (test/latency/rail-sweep.ts) runs one bounded pass of moves and removes its listeners at the end.

Tests

  • test/stylesheets.test.ts: "no :has() selector reads the style attribute". It parses every stylesheet's :has() arguments. It fails on base at agent-session/acpmux/bottomStack.css:182.
  • test/thread-minimap-rail.test.tsx: "sweeping the pointer across the rail adds no measuring node to the card and makes no observer per row". It fails on base: the card gained cloned reply nodes and observers.
  • New latency cases chat rail sweep 3000 and chat rail sweep 100 in test/latency/actions.ts. The harness also accepts a 100-turn fixture.
  • The rail suites, jsdom and browser, plus lint: see the green run below.

Red on the base and green at the head, from the same command (cd webviews && bun test test/thread-minimap-rail.test.tsx test/stylesheets.test.ts): with the webviews/src changes reverted, both new tests fail, with (fail) no :has() selector reads the style attribute naming agent-session/acpmux/bottomStack.css:182. At the head, both pass.

Observed / Not run

  • Observed: the hover sweep before and after (run linked above); the two new tests red with the fix reverted and green at the head; the rail suites, jsdom and browser, and lint on the fleet.
  • Not run: a tagged native build. This change is webview-only, CSS and TSX, and the gallery stills cover how it looks.

Changelog

Fixed: sweeping the pointer across the session rail no longer lags. Each move now paints within its frame.

Risk

Low to medium. One CSS selector changes: the running tray's space reservation now reads data-fold-clip instead of the inline style. clipFold() sets both together, and every place that wrote overflow on a fold now goes through it. If one were missed, the tray would reserve space one step late. The card's in-place measure restores the reply's exact style attribute in the same task, so nothing paints in between.

Screenshots

Stills: see the gallery advisory comment (agent-pane thread minimap, light and dark, 2x).

🤖 Generated with Claude Code

Agent session: agent session


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


Summary by cubic

Sweeping the pointer across the session rail no longer lags: each move now paints within its frame, and move-to-card latency drops 61–70%. Two sources of whole-page restyling are gone; behavior, look, and states are unchanged.

What was slow

  • The running tray's :has() rule read the inline style attribute, which Chromium can't scope; clipFold() now marks the clip with data-fold-clip, and the rule reads that instead.
  • The preview card measured its reply with a cloned node and a fresh ResizeObserver per prompt change; it now measures in place, in the same task, and keeps one observer for its life.

Performance

  • Move-to-card p50: 20.5 → 7.9 ms at 3,000 prompts; 23.4 → 7.9 ms at 100 prompts; p95 and max improve 62–70%.
  • Style recalcs per move drop 11.4 → 6.1, style time per move 28.3 → 3.9 ms, with no dropped frames and still one React commit per move.

Tests guard the regressions: a stylesheet rule banning :has() over style, a rail test checking no measuring node or per-row observer, and new latency sweep cases at 3,000 and 100 prompts.

Written for commit 55110d4. Summary will update on new commits.

View guided diff Turn on auto-fix

teamleaderleo and others added 5 commits October 30, 2026 21:43
Leo: "the left scroll rail still lags when i hover over a lot of it it needs to be faster".
A real pointer crosses the rail's view tick by tick and back, one move per painted frame;
each move is timed to the frame showing its tick lit and the card previewing its prompt,
with React commits, Profiler time and CDP style/layout counts per move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64
…card in place

Leo: "the left scroll rail still lags when i hover over a lot of it it needs to be faster".
Fails on base: each row crossed appends a measuring copy of the reply to the card and
makes a new ResizeObserver.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64
… and keeps one observer

Leo: "the left scroll rail still lags when i hover over a lot of it it needs to be faster".
Every row the pointer crossed appended a measuring copy of the reply to the card and removed it,
and replaced the card's ResizeObserver (which fits again on observe). Under the page's :has()
rules each insertion restyled the whole page (~1,100 elements). The reply's natural height is now
read in place with its clamp lifted for the read, and one observer lives as long as the card and
skips a size it already fitted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64
Fails on base: bottomStack.css's running tray rule tests [style*="overflow"] inside :has(),
which restyles the whole page on every inline style write (16 ms per tick a pointer crosses on
the session rail).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64
…, not the inline style

Leo: "the left scroll rail still lags when i hover over a lot of it it needs to be faster".
The tray's min-height rule tested [style*="overflow"] inside :has(). Chromium cannot scope that
invalidation, so every inline style write on the page (each rail tick's transform as the pointer
crosses it, the card's placement) restyled all ~1,100 elements. fold.ts now marks the clip it
sets with data-fold-clip (clipFold), and the rule reads that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64
@teamleaderleo teamleaderleo added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 31, 2026
@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

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

Review 55110d4: 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=pr29086 -f ref=lane/rail-hover-sweep -f platform=macos -f backend_mode=local

Gallery before/after for 55110d4: 16 states rendered, 6 images differ, 1621 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 41186119287 -R manaflow-ai/cmux -n gallery-advisory-29086-55110d4a28f66e7fe2f060f665222f0cc35999ce (then read gallery-review.json)

States
State Theme Status Diff Picked by
agent-pane.streaming-text/claude Apple System Colors Light unchanged 0% own bottomStack.css
agent-pane.streaming-text/claude Apple System Colors unchanged 0% own bottomStack.css
agent-pane.streaming-text/codex Apple System Colors Light unchanged 0% own bottomStack.css
agent-pane.streaming-text/codex Apple System Colors unchanged 0% own bottomStack.css
agent-pane.streaming-text/jump-during-stream Apple System Colors Light changed 0.228% own bottomStack.css
agent-pane.streaming-text/jump-during-stream Apple System Colors changed 0% own bottomStack.css
agent-pane.thread-minimap/card-markdown Apple System Colors Light unchanged 0% own PreviewCard.tsx
agent-pane.thread-minimap/card-markdown Apple System Colors unchanged 0% own PreviewCard.tsx
agent-pane.thread-minimap/card-failure Apple System Colors Light unchanged 0% own PreviewCard.tsx
agent-pane.thread-minimap/card-failure Apple System Colors unchanged 0% own PreviewCard.tsx
agent-pane.thread-minimap/card-metadata Apple System Colors Light unchanged 0% own PreviewCard.tsx
agent-pane.thread-minimap/card-metadata Apple System Colors unchanged 0% own PreviewCard.tsx
agent-pane.thread-minimap/card-prompt-fixed-2 Apple System Colors Light unchanged 0% own PreviewCard.tsx
agent-pane.thread-minimap/card-prompt-fixed-2 Apple System Colors unchanged 0% own PreviewCard.tsx
agent-pane.thread-minimap/card-prompt-fixed-3 Apple System Colors Light unchanged 0% own PreviewCard.tsx
agent-pane.thread-minimap/card-prompt-fixed-3 Apple System Colors unchanged 0% own PreviewCard.tsx
agent-pane.thread-minimap/card-prompt-growing Apple System Colors Light unchanged 0% own PreviewCard.tsx
agent-pane.thread-minimap/card-prompt-growing Apple System Colors unchanged 0% own PreviewCard.tsx
agent-pane.thread-minimap/rest Apple System Colors Light unchanged 0% own PreviewCard.tsx
agent-pane.thread-minimap/rest Apple System Colors unchanged 0% own PreviewCard.tsx
agent-pane.thread-minimap/scrubbing Apple System Colors Light changed 0% own PreviewCard.tsx
agent-pane.thread-minimap/scrubbing Apple System Colors changed 0% own PreviewCard.tsx
agent-pane.preview-card/not-loaded Apple System Colors Light unchanged 0% names PreviewCard.tsx
agent-pane.preview-card/not-loaded Apple System Colors unchanged 0% names PreviewCard.tsx
agent-pane.preview-card/loaded Apple System Colors Light unchanged 0% names PreviewCard.tsx
agent-pane.preview-card/loaded Apple System Colors unchanged 0% names PreviewCard.tsx
agent-pane.preview-card/open-in-menu Apple System Colors Light unchanged 0% names PreviewCard.tsx
agent-pane.preview-card/open-in-menu Apple System Colors unchanged 0% names PreviewCard.tsx
agent-pane.preview-card/gallery-stage Apple System Colors Light changed 66.8% names PreviewCard.tsx
agent-pane.preview-card/gallery-stage Apple System Colors changed 0.163% names PreviewCard.tsx
agent-pane.preview-card/loopback-host Apple System Colors Light unchanged 0% names PreviewCard.tsx
agent-pane.preview-card/loopback-host Apple System Colors unchanged 0% names PreviewCard.tsx

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

Passes: CI passes on 55110d4a28.

CI passes on 55110d4a28 (run 41187081697 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

Shipped in:

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by an agent session · session

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex gpt-6.1-sol xhigh, router-review): BLOCK at c1c038f

BLOCK at c1c038f

Visual verdict: compared all 16 supplied before-and-after pairs; no visible regressions.

  1. [P1] [blocking] rail-sweep.ts:152: The new Promise-resolving timeout lacks a // fixed-wait: explanation. The existing check:fixed-waits guard scans latency helpers and rejects this unmarked wait against the empty baseline, making the required bun run check fail. Add the supported comment explaining this per-move failure deadline.

Syntax parsing and stylesheet assertions passed. Full Bun and browser checks were unavailable in this box.

@teamleaderleo teamleaderleo removed the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 31, 2026
@github-actions github-actions Bot added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 31, 2026
… the card measure

The review flagged the sweep sampler's per-move timeout as an unmarked fixed wait; it is a failure
deadline that a painted move clears at once. Format the card's in-place measure as vp fmt wants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64
@blacksmith-sh

blacksmith-sh Bot commented Oct 31, 2026

Copy link
Copy Markdown

Found 3 test failures on Blacksmith runners:

Failures

Test View Logs
test_a_picked_signing_label_has_a_rerun_alternative (main.RunsOnFallbackTests.test_
a_picked_signing_label_has_a_rerun_alternative)/
test_a_picked_signing_label_has_a_rerun_alternative (main.RunsOnFallbackTests.test_
a_picked_signing_label_has_a_rerun_alternative)
View Logs
test_every_listed_hook_is_one_glaeda_released_with_the_check (main.SigningHooksInSt
epWithGlaedaTests.test_every_listed_hook_is_one_glaeda_released_with_the_check)/
test_every_listed_hook_is_one_glaeda_released_with_the_check (main.SigningHooksInSt
epWithGlaedaTests.test_every_listed_hook_is_one_glaeda_released_with_the_check)
View Logs
test_no_runs_on_chain_ends_on_the_reserved_side_label (main.RunsOnFallbackTests.tes
t_no_runs_on_chain_ends_on_the_reserved_side_label)/
test_no_runs_on_chain_ends_on_the_reserved_side_label (main.RunsOnFallbackTests.tes
t_no_runs_on_chain_ends_on_the_reserved_side_label)
View Logs

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

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex, router-review): SHIP at 55110d4

Reviewer: codex gpt-6.1-sol xhigh, fast tier.

Follow-ups: 1. Each lands as an issue in this repo that links this receipt; the lane that opened the PR fixes it forward.

SHIP at 55110d4

Visual verdict: no images; Leo tries the build

  1. [P2] [follow-up] No after screenshots or gallery artifact were supplied, so visual comparison is unavailable.

Follow-ups:

  • [P2] [follow-up] Provide gallery stills for visual verification.

@teamleaderleo teamleaderleo removed the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 31, 2026
@github-actions github-actions Bot added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 31, 2026
@teamleaderleo
teamleaderleo merged commit 4de490b into feat-cmux-next Oct 31, 2026
205 of 212 checks passed
@teamleaderleo
teamleaderleo deleted the lane/rail-hover-sweep branch October 31, 2026 10:13

This branch was previously deployed

1 inactive deployment
ci-routing-agent — 55110d4a Deployed Oct 31, 2026 by teamleaderleo via cmux-tui tree preflight #88661
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.

1 participant