fix(desktop): stop long-session transcript from drifting to old turns - #69019
Merged
Conversation
content-visibility:auto on turn groups (perf: off-screen turns skip style/layout/paint) pairs with contain-intrinsic-size:auto, which only remembers a turn's size after it renders. A turn that finished streaming near the bottom had its smaller mid-stream size remembered; once it scrolled off the top edge and got skipped, it collapsed to that stale height. With overflow-anchor:none the viewport can't self-correct, so the stick-to-bottom lock drifts and the view creeps up over older turns — the 'long session eventually shows old responses' visual glitch. Exempt the newest turns (live tail) from virtualization so a turn is only ever skipped after its layout has settled at its final size (remembered == real -> skipping changes no height). Off-screen older turns still skip, so the dialog/popover whole-document recalc win on long transcripts is kept (it scales with the hundreds of old turns, not the small tail).
Contributor
૮ >ﻌ< ა ci reviewrunning on 94f6dff CI timingsCI timings · View jobWall time 7m30s vs 7m42s (-2.6%). 4 job(s) slower, 1 faster, 2 unchanged.
|
Collaborator
This was referenced Jul 22, 2026
This was referenced Jul 25, 2026
7 tasks
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
…NousResearch#69019) content-visibility:auto on turn groups (perf: off-screen turns skip style/layout/paint) pairs with contain-intrinsic-size:auto, which only remembers a turn's size after it renders. A turn that finished streaming near the bottom had its smaller mid-stream size remembered; once it scrolled off the top edge and got skipped, it collapsed to that stale height. With overflow-anchor:none the viewport can't self-correct, so the stick-to-bottom lock drifts and the view creeps up over older turns — the 'long session eventually shows old responses' visual glitch. Exempt the newest turns (live tail) from virtualization so a turn is only ever skipped after its layout has settled at its final size (remembered == real -> skipping changes no height). Off-screen older turns still skip, so the dialog/popover whole-document recalc win on long transcripts is kept (it scales with the hundreds of old turns, not the small tail).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
In a long-running desktop session, the transcript would eventually drift up and show old responses while idle/waiting — purely visual, no functional impact (the session worked end-to-end).
Root cause
Regression from
9b8b054c2("off-screen turns skip rendering"), which addedcontent-visibility: auto+contain-intrinsic-size: auto 37.5remto every turn group inlist.tsx.contain-intrinsic-size: autoonly remembers a turn's height after it renders. A turn that finished streaming near the bottom had its smaller mid-stream size remembered; when it later scrolled just off the top edge andcontent-visibilityskipped it, it snapped back to that stale smaller height, shifting content down. Because the viewport usesoverflow-anchor: none(souse-stick-to-bottomowns scrollTop), the browser can't self-correct — the bottom lock drifts and the view creeps up over older turns. The error is small per turn but accumulates over a long session, so it only shows up "eventually."This is a well-known
content-visibilityfailure modeNot Hermes-specific — it's the documented CLS/scroll-jump behavior of
content-visibility+contain-intrinsic-sizewhen the placeholder estimate differs from real height and the shift lands above the scroll position:Hy3-previewchat removedcontent-visibilityentirely for the identical reason ("scroll-anchoring does not reliably compensate for size changes triggered by content-visibility transitions"), and recommended, if re-adding: apply it only to older rows, with per-row measured intrinsic sizes rather than a fixed guess. This PR is the "only older rows" half of that.Fix
Exempt the newest turns (a small live tail) from virtualization, so a turn is only ever skipped after its layout has settled at its final size (remembered == real → skipping it changes no height). Off-screen older turns still skip, so the dialog/popover whole-document recalc win on long transcripts is fully preserved (it scales with the hundreds of old turns, not the small tail).
Extracted a pure, tested helper (
isVirtualizedGroup+LIVE_TAIL_GROUPS = 6) rather than inlining the boundary.History worth knowing (a latent mismatch this sidesteps)
overflow-anchor: noneon the viewport is a leftover from the virtualizer era. The transcript used to be a TanStack-Virtual component (thread-virtualizer.tsx, now deleted);overflow-anchor: nonewas added specifically for it ine67ab2e04/ #37866 ("stop chat scroll jumping by disabling native scroll anchoring") so only the virtualizer compensates. Then76b93869d("rebuild thread autoscroll on use-stick-to-bottom") replaced the virtualizer with the currentcontent-visibility+use-stick-to-bottomlist — but leftoverflow-anchor: nonebehind.So the transcript now pairs
content-visibility(which relies on scroll anchoring to smooth placeholder↔real deltas) withoverflow-anchor: none(which disables it) — which is why we're more exposed to this than a typical app.Deeper alternatives (not done here; this PR is the minimal safe fix)
overflow-anchor: none, setoverflow-anchor: autoon the viewport +noneon children + a 1px bottom sentinel, and let the browser compensate for above-viewport size deltas. Precedent: svelte-virtual-chat #55, aura-os.anchorTo: 'end',followOnAppend,scrollToEnd) — and the sidebar already uses@tanstack/react-virtual.Verification
isVirtualizedGroup(live tail always rendered, older turns virtualized, short-transcript, custom tail) —list.test.ts12/12.thread/suite: 56/56.tsctypecheck clean; eslint clean; prettier clean.Note: jsdom has no real layout /
content-visibility, so the scroll-drift itself can't be reproduced in a unit test — the helper boundary is tested; the visual fix needs a real long-session spin to confirm.