cx-j33s4: keep rows under the view during a fast transcript fling
A fast fling in a long chat showed an empty edge on about 55% of frames at the capped (60 fps) render rate. The transcript now mounts the rows the next frames will show, so the view stays covered.
The cause was the overscan cap, not render time. A scroll commits a frame after the offset moved, so the transcript mounts two scroll steps ahead, capped at four viewports. The bench's capped fling moves about 4.8 viewports a frame, so the next view often landed past the cap. React used 6–7 ms of the 17 ms frame, one commit per frame. At full rate a frame moves about 2.8 viewports and stayed covered.
Past the cap, the transcript keeps the capped lead and also mounts each view the next two frames reach (the scroll so far, one and two steps on), with a margin of a quarter step, at most half a viewport, for uneven frames. The rows in between, which no frame shows, aren't built. The blank-frame check (perf.ts isBlank) now reads the separate mounted runs. flingStats also reports the fling's distance and viewports per frame.
Evidence
Frame pacing bench (120 Hz virtual display, 4998 rows, 3 flings per mode), blank frames per fling:
| Before (run) | After, 4b99503b807c (run) | |
|---|---|---|
| Capped | 109/180, 96/181, 101/181 | 5/181, 5/179, 3/180 |
| Full rate | 1/360, 4/360, 2/361 | 2/356, 2/358, 4/359 |
The cost is more rows built per frame: capped p50 81 → 137 row renders, React p50 6–7 → 9 ms (p95 11 ms), still inside the 17 ms frame with 1–3 dropped frames per fling. Full rate: 97 → 113 row renders, React p50 3 ms either way, 4–5 dropped frames per fling against 1–3 before.
webviews/test/transcript-fling.test.ts drives the fling in real WebKit, one fixed step per frame so a slow runner scrolls the same path, and counts frames whose view the mounted rows leave partly empty. Same focused command (local WebKit), base vs fix:
| Fling | Base | Fix |
|---|---|---|
| Up, ~5 viewports a frame with jitter | 63/64 | 2/64 |
| Down, same steps | 64/64 | 1/64 |
| Uneven drag, 2–4 viewports a frame | 2/107 | 2/107 |
The drag case guards the range the old lead already covered. The test (then on 5000 rows) passed in CI on 4b99503b807c; it now seeds 3000 rows to stay well inside the runner's 120 s per-file limit (50 s locally). CI never ran it on the test-only head: react-apps-check hit its 15-minute limit before reaching it. transcript-long-chat-updates now compares placement only between rows next to each other, since separate runs leave a gap, and fails if it compares none. The other transcript-* browser tests and bun run check pass locally.
Changelog
Fixed: Fast scrolling through a long agent chat no longer flashes empty space at the edge of the view.
🤖 Generated with Claude Code