Skip to content

fix(chat): release held disclosure anchor when reader scrolls up - #1855

Closed
jiangchuanso wants to merge 1 commit into
vastsa:mainfrom
jiangchuanso:fix/transcript-reader-scroll-anchor
Closed

jiangchuanso wants to merge 1 commit into
vastsa:mainfrom
jiangchuanso:fix/transcript-reader-scroll-anchor

Conversation

@jiangchuanso

Copy link
Copy Markdown

Summary

Fixes a transcript follow-scroll regression surfaced by the upstream trusted-extensions-e2e workflow: the transcriptLongHistoryProbe longHistory phase asserts "stream stole the reader's scroll position" (scripts/e2e/transcript-long-history.tsx:280). When the reader has scrolled up away from the bottom, an incoming stream delta must not move the viewport.

Root cause

Clicking a disclosure header (e.g. .tool-activity-header) holds a disclosure anchor so the toggled title stays in view during its height transition (disclosure.tsx → notifyAnchor → onHold → releaseFollow). That anchor is released only by markScrollGesture (real input: wheel / touch / key / pointer). When the reader scrolls up without that path releasing it — which is exactly what the test simulates with a synthetic gesture + manual scrollTop — the anchor stays held. The ResizeObserver then runs followScrollNow → restoreDisclosureAnchor(), which re-asserts the held position during a later stream and yanks the viewport back, stealing the reader's position.

Fix

In handleScroll, when transition.releasedFollow is true (the reader moved up and took the viewport back), also call releaseDisclosureAnchor(), mirroring what markScrollGesture already does for real gestures. This guarantees a held anchor cannot outlive a reader scroll-up.

  • Pinned/follow state is unaffected (the releasedFollow branches are unchanged).
  • Disclosure anchoring during a plain expansion (no reader scroll) is unchanged: releasedFollow is only true once the reader actually scrolls up.

Validation

The longHistory transcript e2e covers this exact scenario (disclosure click → while-reading stream → reader scrolls up → reader-stays stream → position must not move). Upstream CI will run it.

A reader scroll-up (releasedFollow) must also drop any disclosure anchor the transcript is holding, matching what markScrollGesture already does for real input. Otherwise a held anchor can be restored by the ResizeObserver during a later stream and yank the viewport back, stealing the reader's scroll position while they read history.
@jiangchuanso
jiangchuanso force-pushed the fix/transcript-reader-scroll-anchor branch from 9fa2f7c to c031985 Compare October 24, 2026 01:57

@vastsa vastsa left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix and the long-history coverage. I could not verify the reported gap on the current event path: the fixture dispatches a bubbling WheelEvent on .thread-scroll, and the existing listener on .thread-wrap classifies it as an accepted wheel (readScrollInputContext sees the transcript as the same owner), so markScrollGesture already calls releaseDisclosureAnchor() before handleScroll runs.

The added call also runs for layout-driven upward movement. releasedFollow is computed as movedUp && distanceFromBottom > 0 without checking gesturing, while the following branch explicitly treats non-gesture events as programmatic/layout noise. That can drop a held disclosure title during a layout clamp and stop the observer from preserving it through the transition. Please either demonstrate a real scroll path that bypasses the existing listener, or limit this release to an accepted gesture and cover the layout-clamp case as well. Thanks!

// anchor, exactly as a real gesture does in `markScrollGesture`. Without
// this, a held anchor can still be restored by the ResizeObserver during a
// later stream and steal the reader's position.
if (transition.releasedFollow) releaseDisclosureAnchor();

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition is broader than the stated user-scroll case: releasedFollow can be true for a non-gesture layout clamp, which this handler immediately classifies as layout noise below. Releasing the disclosure anchor here can make the next observer frame lose the held title. The PR's E2E wheel event already bubbles to markScrollGesture, which releases the anchor earlier.

@jiangchuanso
jiangchuanso deleted the fix/transcript-reader-scroll-anchor branch October 24, 2026 14:28

This branch was previously deployed

1 inactive deployment
Preview — c0319859 Deployed Oct 24, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants