Repository navigation
perf(agent-chat): sweeping the pointer across the session rail paints within each frame - #29086
Conversation
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
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Gallery before/after for
States
Pick other states with a Passes: CI passes on CI passes on Written by web and size checks: green Shipped in:
|
|
Owned by an agent session · session |
|
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.
Syntax parsing and stylesheet assertions passed. Full Bun and browser checks were unavailable in this box. |
… 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
|
Found 3 test failures on Blacksmith runners: Failures
|
|
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
Follow-ups:
|
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
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.
:has()rule read the inlinestyleattribute.bottomStack.cssreserved 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 withdata-fold-clip(clipFold()infold.ts), and the rule reads that instead. The clip, the reservation and the tray motion are the same as before.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 oneResizeObserverfor 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/srcchanges 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 frompointeroverto 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, caseschat.rail.hover.sweep.3000and.100).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
ResizeObserverwas 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.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 atagent-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.chat rail sweep 3000andchat rail sweep 100intest/latency/actions.ts. The harness also accepts a 100-turn fixture.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 thewebviews/srcchanges reverted, both new tests fail, with(fail) no :has() selector reads the style attributenamingagent-session/acpmux/bottomStack.css:182. At the head, both pass.Observed / Not run
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-clipinstead of the inline style.clipFold()sets both together, and every place that wroteoverflowon 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 exactstyleattribute 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
Need help on this PR? Tag
@codesmith-botwith 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
:has()rule read the inlinestyleattribute, which Chromium can't scope;clipFold()now marks the clip withdata-fold-clip, and the rule reads that instead.ResizeObserverper prompt change; it now measures in place, in the same task, and keeps one observer for its life.Performance
Tests guard the regressions: a stylesheet rule banning
:has()overstyle, 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.