Skip to content

fix(desktop): stop long-session transcript from drifting to old turns - #69019

Merged
OutThisLife merged 1 commit into
mainfrom
bb/desktop-long-session-scroll-drift
Jul 22, 2026
Merged

fix(desktop): stop long-session transcript from drifting to old turns#69019
OutThisLife merged 1 commit into
mainfrom
bb/desktop-long-session-scroll-drift

Conversation

@OutThisLife

@OutThisLife OutThisLife commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

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 added content-visibility: auto + contain-intrinsic-size: auto 37.5rem to every turn group in list.tsx.

contain-intrinsic-size: auto only 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 and content-visibility skipped it, it snapped back to that stale smaller height, shifting content down. Because the viewport uses overflow-anchor: none (so use-stick-to-bottom owns 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-visibility failure mode

Not Hermes-specific — it's the documented CLS/scroll-jump behavior of content-visibility + contain-intrinsic-size when the placeholder estimate differs from real height and the shift lands above the scroll position:

  • MDN and the browser-rendering guide describe it directly.
  • Tencent's Hy3-preview chat removed content-visibility entirely 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: none on 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: none was added specifically for it in e67ab2e04 / #37866 ("stop chat scroll jumping by disabling native scroll anchoring") so only the virtualizer compensates. Then 76b93869d ("rebuild thread autoscroll on use-stick-to-bottom") replaced the virtualizer with the current content-visibility + use-stick-to-bottom list — but left overflow-anchor: none behind.

So the transcript now pairs content-visibility (which relies on scroll anchoring to smooth placeholder↔real deltas) with overflow-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)

  1. Native scroll anchoring: drop overflow-anchor: none, set overflow-anchor: auto on the viewport + none on children + a 1px bottom sentinel, and let the browser compensate for above-viewport size deltas. Precedent: svelte-virtual-chat #55, aura-os.
  2. Back to an end-anchored virtualizer: TanStack Virtual now has first-class chat support (#1173: anchorTo: 'end', followOnAppend, scrollToEnd) — and the sidebar already uses @tanstack/react-virtual.

Verification

  • New unit tests for isVirtualizedGroup (live tail always rendered, older turns virtualized, short-transcript, custom tail) — list.test.ts 12/12.
  • Full thread/ suite: 56/56.
  • tsc typecheck 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.

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).
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

running on 94f6dff

CI timings

CI timings · View job

Wall time 7m30s vs 7m42s (-2.6%). 4 job(s) slower, 1 faster, 2 unchanged.

  • OSV scan / Scan lockfiles / osv-scan: +25.0s
  • Build&Test Docker image / build (arm64, ubuntu-24.04-arm, linux/arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope...: -15.0s
  • Build&Test Docker image / build (amd64, ubuntu-latest, linux/amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=do...: +6.0s
  • Detect affected areas: +4.0s
  • OSV scan / Emit review status: +3.0s

@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/desktop Electron desktop app (apps/desktop/*) labels Jul 22, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related to #40433 and #43914 in the desktop scroll-stability family. This patch addresses a distinct content-visibility/intrinsic-size mechanism in the assistant-ui thread list.

@OutThisLife
OutThisLife merged commit 3de91c6 into main Jul 22, 2026
31 checks passed
@OutThisLife
OutThisLife deleted the bb/desktop-long-session-scroll-drift branch July 22, 2026 01:30
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants