Prompt rail: a hover sweep forces fewer layouts; the card's blur follows a move in one measure
Leo, 2026-11-04:
hovering across the session rail gets a bit laggy and the missing chunks of the wave bother me :( it needs to get faster
likewise hover previews kinda lag across me hovering over the sidebar stuff :( idk if more things need to be at higher priority or whatnot
Leo-ask: ask-0eea9d95e5
The rail's height and the wave's gaps are fixed in #34666. This PR cuts the work done each time the hovered row changes during a sweep, when the row changes about every 40 ms:
- Preview card (
PreviewCard.tsx): it reads before it writes, and it remembers each row's fit. Placing and fitting the card used to alternate reads and writes, which forced 4 layouts per row. Now there is one read pass and then one write. A row seen before writes its remembered lines and line height and reads once. That fit is reused only at the same card width and the same type (the prompt's and the reply's computed line heights). So a text size or density change, even while the card showed another row, measures fresh, as do a resize the card's observer catches (a font, an image) and a width change. - See-through blur (
seeThroughBlur.ts): the card moves through its inline transform. Each move used to restart the blur's follow loop, which ran at least 4 frames of re-measuring every covered element and rewriting the SVG hole filter on the whole transcript. A change to the surface's own style now measures once on the next frame. Transitions, animations and resizes still run the full loop. The Summary card and Session Info share this path. - Tick labels (
ThreadMinimap.tsxTickList): each drawn row keeps its spoken label while its position, row count and bookmark state don't change. A hover no longer rebuilds about 115 labels to change about 8.
Performance
| metric | before | after | delta | harness |
|---|---|---|---|---|
| forced layouts per new hovered row | 4 | 3 | -1 | test/thread-rail-hover-reads.test.tsx (jsdom dirty/read model) |
| forced layouts per revisited row | 4ā5 | 1 | -3 | same |
| blur measures per card transform move | 4 | 1 | -3 | test/see-through-blur-follow.test.ts |
| main-thread busy ms per frame during a sweep (p50/p95) | pending | pending | bench/thread-rail-hover.bench.ts (#34658); cmux-ci is down until #34673 lands, numbers follow then |
No new timers or observers run on their own. The blur's new style observer is the existing one, split out: it disconnects in the same cleanup, and its single measure is one rAF that is cancelled on teardown. The card's fit cache is a module-level WeakMap keyed by each turn's reply blocks, then by the prompt text: an entry goes when its turn's blocks do, and it is shared by every card that shows that turn.
Tests
Red first, each before its fix:
bun test test/thread-rail-hover-reads.test.tsx: fails on base (4 forced layouts per row), passes on head (3 new, 1 revisited).bun test test/see-through-blur-follow.test.ts: fails on base ([4,4,4,4]measures per move), passes on head ([1,1,1,1]). The hole still follows the card, and a transition still follows for 3 frames.bun test test/thread-rail-hover-reads.test.tsx("measures again once the text size changed"): fails at 449d336f (the old 20px budget kept), passes on head (31px).test/thread-minimap-rail.test.tsx: the label follows a bookmark on and off.- Every jsdom suite in
thread-minimap*,thread-rail*,minimap*,chat-minimap*,thread-scroll-map*, glass, Summary andhover-cards-glasspasses.
Observed / Not run
- Observed: the jsdom suites above;
vp fmt,vp lintandtscare clean on the changed files. - Not run locally: the browser suites (
minimap-card-layout,see-through-blur,thread-minimap-motion,session-info-glass); this PR's webview CI runs them. The frame bench is pending as above. - Skipped on purpose: caching the list's rect for pointer moves. A test showed those reads hit a clean layout, and a cache could go stale.
Follow-ups
Review follow-ups: #35239.
Changelog
- Changed: Hovering across the agent chat's prompt rail does less work per row, so the preview card keeps up with the pointer.
risk:normal
š¤ Generated with Claude Code
Agent session: agent session