Session rail: the preview card opens in one commit, and the rail's hover, wheel and click are measured at 3,000 prompts
Leo (rulebook): "when it comes to the left scroll rail thing, i already said we want it ultra fast ultra responsive". Leo, 2026-10-29: "giving it faster scrttoll/hover over ... and being able to scroll it".
Stacked on #28494 (head ca1f3e26) until it lands. Until then the diff against base includes #28494's commits. Rebased onto #28494's merge with #24131 (the right-edge rail): the card places itself on either edge (left: card.left == rail.right; right: card.right == rail.left, in the layer by its own width or pinned to the right edge without one).
Takes back #26134: the card now measures and places itself in one commit (fit() plus a ResizeObserver in PreviewCard) instead of a setCardHeight round-trip; #26134's wrapped-prompt behavior is kept and minimap-card-layout.test.ts still passes.
Root cause: why the rail harness cases timed out at 3,000 prompts
The cause was the harness, not the rail. Three harness faults hit both arms:
- The preview server colours its port in its startup banner, so the harness never saw it start and then hung until the 1800 s CI limit. Fixed in #28494.
- A hover never started the probe's clock, because the probe's default inputs are presses, keys and wheels. The hover cases now arm on
pointerover. - The flows fixture opens Session Info, which hid the rail on base. The cases now close Session Info first.
With the harness fixed, the rail does no work that grows with the number of prompts. The 3,000 ticks are already virtualized to about 86 drawn. A Chromium trace at 300 and at 3,000 prompts gave the same per-step cost: wheel style time 233 vs 239 ms per 10 steps, and hover 19–36 ms to commit. The remaining cost is per pane DOM, not per prompt:
- Hover (fixed here): opening the card took 3 React commits and 6 forced layouts, because the card's origin, its height and its reply line budget each went through state.
- Wheel (not fixed here): any node inserted under the agent pane restyles the whole pane in Chromium, about 1,100–1,165 elements at 4.6 ms. Six pane
:has()rules cause this (the composer field menu, the PR dock's:has(> .acpmux-pr-dock) > :not(...), and twolabel:has(...)rules). With every:has()rule turned off, the same wheel step restyles 14–22 elements. A wheel step over the rail inserts nodes in two places: ticks entering the drawn window, and the open card's reply when the hovered row changes. Those rules are being handled in a separate PR, since they also affect transcript streaming. - I also tried snapping the tick window to 32-row chunks, so the mounted ticks change less often. It made no measurable difference (wheel p95 31.4 vs 30.5 ms on base, 12 vs 12 dropped frames), so it is not in this PR.
Change
- The preview card now positions itself in its layout effect. It reads the rail's origin and its own height and writes its transform before paint. It writes the reply line budget straight onto the reply. Its owner passes the row's center and the view height, not a top it had to measure for. The card now opens in one commit. The existing ResizeObserver keeps the card placed when its content grows; it is disconnected when the content changes or the card goes. This adds no timer or observer, and no work per prompt.
- The harness gains four cases at 3,000 prompts:
chat.rail.hover.tick,chat.rail.hover.card,chat.rail.wheelandchat.rail.click.
Performance
Same harness and same command for every arm: bun scripts/latency/run.ts --engine chromium --runs N --page "chat rail pointer", on the 3,000-prompt resident flows page, Chromium. Within a run, the arms differ only in webviews/src: base is 094dd560 (feat-cmux-next before #28494), "#28494" is f176bb22, "this PR" is the card change on top of it. The card change was measured before the rebase onto #24131; the rebase adds only the right-edge branch of its placement.
| metric | before | after | delta | harness or run link |
|---|---|---|---|---|
| hover → card painted p50 / p95, vs feat-cmux-next | 289.6 / 292.8 ms | 41.3 / 42.7 ms | −248 ms (−86%); the 250 ms dwell is #28494's, so −3 ms here | latency chat.rail.hover.card, runs=10, run 41136916156 |
| hover → card painted p50 / p95, vs #28494 | 44.1 / 46.0 ms | 41.3 / 42.7 ms | −2.8 ms (−6%) | same case; #28494 run 41139037067, this PR run 41136916156 |
| React commits, 6 hover cases, vs #28494 | 18 | 7 | −61% | chat.rail.hover.tick, runs=6, run 41139037067 |
| hover → tick lit p50 / p95, vs #28494 | 45.3 / 46.5 ms | 43.6 / 45.6 ms | −1.7 ms | same run |
| wheel frame time p50 / p95 / dropped (10 × 100 px, 6 runs), vs feat-cmux-next | 16.7 / 30.5 ms / 12 | 16.7 / 28.8 ms / 14 | within noise | chat.rail.wheel, base run 41138308077, this PR run 41139037067 |
| wheel React commits (6 runs), vs feat-cmux-next | 120 | 60 | −50% | same runs |
| click → prompt landed p50 / p95 | 84.1 / 115.9 ms | 93.4 / 127.2 ms | within run-to-run noise (#28494's head arm: 83.6 / 115.2) | chat.rail.click, runs 41138308077 / 41139037067 |
Harness note: when the cases share one page, a case that runs after another sometimes finds no rail ("rail not ready ... rail:false"), so some arms lack a case in a given run. The table uses only complete cases and links each to its run. The wheel and click costs are the pane :has() restyle described above, which is being fixed separately.
Stills
The card's placement code changed, so here are gallery stills with the hover card open on both edges. They were captured via cmux-ci in one job (run 41144770735), Chromium. Before is #28494's head ca1f3e26; after is this PR's head 444a364e. Left edge: agent-pane.thread-minimap / card-markdown. Right edge (#24131): agent-pane.thread-minimap-narrow / right-hovered. All eight before/after pairs are byte-identical (same PNG hash): the card lands on the same pixels on both edges, with card.left == rail.right on the left and card.right == rail.left on the right.
| Before | After | |
|---|---|---|
| Left, dark | ![]() | ![]() |
| Left, light | ![]() | ![]() |
| Right, dark | ![]() | ![]() |
| Right, light | ![]() | ![]() |
Viewed after image: yes
Testing
- Rail jsdom suites (
*minimap*,hover-intent-cards,minimap-*,thread-scroll-map*,HarnessStatusCard.dom): 134 pass, 0 fail. - Card browser suites after the rebase (
thread-minimap-feel,thread-minimap-motion,thread-minimap-side(right edge),thread-minimap-left-rail-stays,minimap-card-layout,thread-minimap-card-glass,thread-minimap-tick-states), Chromium and WebKit: 54 pass, 0 fail (run 41142877669). scripts/web-lint.sh: passes.
Changelog
Changed: The chat rail's preview card opens with less work per hover.
Agent session: agent session
🤖 Generated with Claude Code







