fix(chat): preserve virtualized rows and anchors during upward scrolling

TE
6d ago
+385-12

Summary

Leo reported that upward scrolling causes a “weird recalculation.” A stale queued scroll frame was dropped whenever the native offset moved again before its callback. Under continuous wheel input the virtualized range stopped advancing, and newly measured rows also anchored an already parked reader from a stale offset.

Recover stale frames from the live offset, route the resulting follow state through the authoritative ref/parent callback, and anchor recent upward input from the live offset even after follow was already released. The existing 4px follow deadband and shared anchor compensator remain in use. The change adds no transcript-wide loop.

This is the focused correctness split from the incomplete rowDelta performance program. #26591 remains blocked on expanded child-list traversal per streamed chunk. The rowDelta producer/consumer work remains separate and blocked while transformed arrays lose provenance.

Testing

The 3,000-turn upward-scroll oracle (webviews/test/transcript-upward-anchor.test.ts, WebKit, 9,000 source rows, 240 fractional precise-pixel wheels plus 24 rest frames = 264 after-paint observations) measures, at every frame, how far the first visible row moved beyond the distance the wheel applied.

  • Before: run 40636441298, source eba7eaf8e50c8d6c192b3ce17eaa2f5e65d16580: 168 blank frames, 96 comparable observations, max residual drift 56.5px.
  • First repair: run 40637819221, source 1fa5b53fb3f7c77e42016d80f09aff44d2e5085a: 0 blank frames, 264 comparable observations, transient drift up to 42.5px.
  • This head, after the live-reader repair: 0 blank frames, 264/264 compared, 0 cancelled wheels, max drift 0.5px (12 of 264 frames, every one +0.5px; total 6 to 6.5px over 8,820px of wheel).

Tolerance, stated honestly: the oracle was first written with a 1/64px bound, which this branch never met: fast-checks run 41106367530 at a4b8faef fails with Expected: <= 0.015625, Received: 0.5. WebKit keeps scrollTop in whole CSS pixels (the trace's applied wheel distances are whole: -18.25 asked, -19 applied) while rows lay out on quarter pixels, and the anchor correction skips residuals of 0.5px or less (Math.abs(nextTop - top) > 0.5) rather than fight that rounding. So the assertion is now maxDrift <= 0.5, the half-pixel quantum this change actually achieves; fast-checks run 41106639072 at 060d27d8 passes (1 pass, 0 fail). A residual above half a pixel, or any blank frame, still fails.

Focused tests: 5 tests / 33 assertions in scroll-pacer.test.ts and transcript-jump-latest.test.tsx, including real VirtualTranscript stale-queued-frame recovery, the parent onFollowingChange callback, and no later snap. Reverting just the authoritative setter reproduces expected false / received true at the parent callback.

Observed / Not run

  • Observed: the WebKit oracle red at a4b8faef (0.5px vs 1/64px) and green at 060d27d8, both through cmux-ci test on Linux fast checks.
  • Not run: native CEF pixel proof and trusted OS wheel input; Chromium's fractional scrollTop path is covered only by the PR's normal CI shards.

Performance

Same 3,000-turn upward-scroll oracle (9,000 source rows, 264 after-paint observations).

metricbeforeafterdeltaharness or run link
blank frames during continuous upward wheel1680-168 (-100%)upward-anchor oracle: before https://github.com/manaflow-ai/cmux/actions/runs/40636441298, after https://github.com/manaflow-ai/cmux/actions/runs/41106639072
comparable after-paint observations96264+168same runs
max residual anchor drift56.5px0.5px-56px (-99%)same runs

Repair for the Astra P2 at 3bd24d63

readerLeft had lost its following gate, so a parked reader's click, downward key or wheel made the live offset the anchor base and a shrink above was applied twice. A parked view now takes the live offset only after upward input (readerUpAt); a following view keeps any input as before.

Agent session: agent session