[BUG] claude.ai/code (web): transcript jumps to much older messages when the message list is re-keyed (e.g. a streamed reply is finalized) — virtualizer loses its anchor (`rec-lost`) and keeps the raw scrollTop
Environment
- Claude Code on the web (claude.ai/code), Google Chrome 154.0.8037.93, macOS 26.6.2 (arm64)
- Long sessions with many tool calls; observed 3 times on 2026-10-02 (two traces below)
What happens
While reading the latest reply, the transcript suddenly shows a much older part of the conversation. The user has to drag back down to find where they were. The jump is not caused by user input: it happens when the message list itself is updated (a reply finishes streaming, or the list is re-fetched).
Expected
The row being read stays where it is when the list changes around it (items prepended above, items re-keyed when a streamed message gets its final id).
Evidence (the transcript's own debug trace, window.__rsTrace = [])
Case A — reply finalized (temporary id -> final id) + rows inserted above, while the user reads (slow downward scroll):
pick aIdx=8 key="<uuid-T>›<uuid-T>:card:<toolu_X>" st=576
range start=0 end=2 ext=4274 st=576 (rows inserted above, scrollTop unchanged)
pick aIdx=0 key="msg_B" st=576 (anchor lost -> row 0, a long older message)
meas idx=14 key="msg_C›msg_C:card:<toolu_X>" prev=null (same card, new key, index 8 -> 14)
Case B — whole window replaced by earlier turns, no user input (~1.4 s after a small upward nudge that was compensated correctly):
comp delta=368 write before=430 got=798
pick aIdx=11 key="msg_D›msg_D:t1114113" st=795
range end=16 ext=1748 -> 2929, layout itemsChanged=true, meas idx=0..15 prev=null
rec-lost key="msg_D›msg_D:t1114113"
pick aIdx=1 key="msg_E" st=755 (same scrollTop, older content)
Minimal reproduction with the production virtualizer
Loaded the transcript virtualizer hook from the claude.ai client bundle (2026-10-01 build) into a plain page with the same DOM structure, 150 rows, estimate 120 px. A single setItems that re-keys every row and prepends 6 rows reproduces the jump every time: the row under the reading line changes for 25–66 frames (~2 s) (3 runs, frames counted in a ResizeObserver callback).
Suggested direction
- When a streamed message is finalized, carry measured size and anchor from the temporary key to the final key (the block suffix, e.g.
:card:<toolu_X>, is stable). - When the anchored key disappears (
rec-lost), fall back to the nearest surviving row or the distance from the end of the list rather than keeping the raw scrollTop.
Related (closed): #68977, #83167.