perf(agent-chat): stop replaying rail tick entrances (rail-speed)

Merged
TE
6d ago
+757-10

Existing prompts draw immediately when scrolling the chat rail into previously virtualized history. Entrance motion is reserved for the first rendered set and prompts appended later; new prompts keep the same easing and stagger. Jump settling reuses its mounted target row until virtualization replaces it.

The rail, tick colours, hover magnification and preview card match the current gallery exactly. The pixel comparison checks all RGBA values in the resting rail, hovered rail and preview card. This is a scoped performance change; height/overflow, paging behaviour and the shared scroll runner are unchanged.

Leo’s request (2026-10-28):

"Likewise, there's this odd stuff going on where sometimes the left scroll rail, it doesn't return to the left when, you know, the right pane closes and whatnot. And also along that same line, I think we should be able to have a taller max height for it, like a far taller max height and initial height for it. So it's, like, different from the rendering of the current pane and whatever. And also I think we can get stuff going a lot faster, like just being able to scroll through stuff. I mean, if you look at the ChatGPT rail, it's just super duper fast. Like they have skeletons sometimes, but a lot of the time it's also, like, they just—I mean, their stuff just renders faster, and they can even scroll through the rail thing. Like that becomes a scrollable area, and it also has a longer initial height and all that stuff."

The importable Chromium benchmark uses the real pane with papercutChat’s 1,200-turn fixture, 20 deterministic distant tick clicks and 120 rail-scroll frames at 1280×900. It emits the shared schemaVersion:1 envelope, raw samples and CDP traces. Tick jumps are target_painted, scrolling is frame_time; neither is labelled INP. Dropped frames are estimates from calibrated frame cadence. Long-task/React-commit/paint/idle counters are explicitly uncollected.

Same-run before/after, Chromium 151.0.7922.34, 1,200 loaded ticks, 20 clicks/reveals and 120 scroll frames (run 40628781432). Base 7e033f21c647b4abf22594aca3dea6444ab36ca0; product head 5116129cc6200b6441f2ff02a59dcebd5a11be9c; verification snapshot 75fb16ffb5d06cd60996815695db30f0c0fe8fe6 adds only the capture/verification fixtures.

MeasurementBeforeAfter
Tick click target_painted p50 / p9567.1 / 483.4 ms54.4 / 388.0 ms
Estimated dropped frames across 20 jumps8054
Jump RecalcStyle total2872.9 ms2278.3 ms
Rail reveal p50 / p95197.1 / 198.7 ms14.9 / 15.6 ms
Rail scroll frame_time p50 / p9516.7 / 16.8 ms16.7 / 16.8 ms
Rail scroll renderer task time / frame17.06 ms13.14 ms
Rail scroll RecalcStyle, 120 frames1581.4 ms1226.1 ms
Rail scroll estimated dropped frames41
Loaded-after-paging target_painted p50 / p9570.5 / 354.9 ms62.4 / 352.2 ms

The paged case exposes 300 loaded ticks after three event requests; its click style total falls from 2697.1 to 2144.1 ms. This case does not measure click-triggered loading. All four raw CDP traces completed and are retained with schema JSON/raw samples in the run log archive (RAIL_BENCH_TRACE is gzip/base64). Use the raw downloadable log archive: the rendered log view can omit oversized trace lines and subsequent output.

A paged fixture answers hasMore and beforeSeq/limit/maxBytes requests using mockPaging’s ordering. On this base the rail has no unloaded prompt index/loadThrough, so its separate loaded-after-paging cases click already loaded targets after loadOlder; an unloaded-target click is explicitly unsupported, not reported as zero.

Validation: 15 targeted tests pass (streaming stability, window ranges, tick appearance/reduced motion, attachment landing and the new regression). The new regression fails against base because the scrolled-in tick has opacity 0 instead of >=0.39. Full web check: passed on the same merged-source run (2,897 files typechecked, formatting/lint and all configured static gates). The browser suite reports 15 pass / 0 fail.

Resource lifecycle: no production timer, observer or animation loop is added. Entrance keyframes end within the existing micro duration plus 72ms stagger and are disabled under reduced motion. Eligibility state lives only for the mounted list/session. The existing settle runner retains its bounded frames/input cancellation and cleanup; its target lookup is cached only within one jump. The benchmark’s paint loops stop on success or a 10s deadline (31 frames for cadence, 120 frames for rail scrolling); browser/page teardown removes their listeners. Its CDP trace listeners detach in finally, the trace-completion deadline is 10s and its timer is cleared, and the browser, servers and temporary build close in finally. Raw trace JSON marks whether tracing completed. There is no scan of all prompts per streamed token; the prompt list identity remains stable, covered by the 300/3,000-prompt streaming tests.

Risk: entrance eligibility could suppress an intended new-prompt animation; the regression checks appended prompts still animate, and the existing first-mount and motion-gate tests pass. Hover/active transitions remain the existing transform/opacity/colour properties because those implement the frozen hover and lit-band appearance.

Before/after gallery captures and exact RGBA comparison are in the same run. Resting rail (36×320), hovered rail (36×320), and preview card (360×142) are all pixel-identical. The reference is the existing agent-pane.thread-minimap rest/hover gallery.

StateBeforeAfter
RestBefore restAfter rest
HoverBefore hoverAfter hover

Performance

metricbeforeafterdeltaharness or run link
Tick click target_painted p5067.1 ms54.4 ms-19%thread-rail-jump.bench, run 40628781432
Tick click target_painted p95483.4 ms388.0 ms-20%same run
Rail reveal p50197.1 ms14.9 ms-92%same run
Rail scroll renderer task time per frame17.06 ms13.14 ms-23%same run
Rail scroll RecalcStyle, 120 frames1581.4 ms1226.1 ms-22%same run
Estimated dropped frames across 20 jumps8054-33%same run

Visual

Viewed after image: yes (after-rest.png and after-hovered.png, below).
Visual verdict: SHIP. The rest state is equal short ticks with the prompts on screen brighter, and the hovered state magnifies the ticks near the pointer with the preview card flush right of the rail, as design/session-rail.md settles. Before and after are pixel-identical in rest, hover and card; the change is only that ticks scrolled back into view no longer replay their entrance.

Update at 2df1a50de2

Merged feat-cmux-next in (clean). The release-notes highlight gains action: palette.newAgentChat | Try it; the digest validator (scripts/whats-new/test_digest.py, the release-notary red at the old head) requires a try-it action or docs link, and it now passes.

Agent session: agent session