Skip to content

fix(frontend): absorb block-window prepends in the reader transaction / 向上滚动时吸收块窗口前插补偿,消除会话跳位 - #9760

Merged
SivanCola merged 1 commit into
esengine:main-v2from
SivanCola:fix/transcript-reader-jump-ownership
Sep 3, 2026
Merged

fix(frontend): absorb block-window prepends in the reader transaction / 向上滚动时吸收块窗口前插补偿,消除会话跳位#9760
SivanCola merged 1 commit into
esengine:main-v2from
SivanCola:fix/transcript-reader-jump-ownership

Conversation

@SivanCola

@SivanCola SivanCola commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes the primary #9711 symptom: while scrolling up through a long Markdown answer, the viewport skips thousands of pixels to another place in the document. Reproduced from the reporter's diagnostics and screen recording (stable d9cd713, Windows, reducedMotion: false, DPR 1.57, all 138 rows mounted).

What the diagnostics show

Eight upward wheel transactions carry the same signature:

before after (≤25 ms later)
scrollTop 19140.95 26393.02 (+7252)
scrollHeight 27812 35051 (+7239)
bottomDistance 8116 8103 (unchanged)

then transcript.scroll-anomaly (reverseDisplacement 7252, extentDelta 14492 ≈ 2×, restore-anchor), and one frame later scrollTop is back at 19140.95. The recording shows the viewport landing on an unrelated region each time.

+G scrollTop / +G scrollHeight / bottomDistance constant is the block-window-prepend compensation written by MarkdownHistory when a long answer's block window prepends older blocks. With the answer row starting above the viewport, the prepend grows the row above the visible blocks and the compensation keeps visible content still — correct. But the reader transaction's anchor is that row's top edge, which is now G px higher relative to the viewport, and scrollTop moved against the reader. The guard reads both as a reverse jump, fires restore-anchor, and scrolls the reader G px up into the newly inserted blocks. That restore is the jump. The guard's positive translateY also extends scrollable overflow for one frame, which is why extentDelta reads 2G.

Change

  • useTranscriptReaderExtentStability.ts: new absorbOffsetWrite(element, delta). The active transaction re-baselines baselineTop, lastAcceptedTop, expectedTop and anchor.offset to the compensated position and resets its extent baselines to the grown scrollHeight, emitting a reader-transaction … absorbed-offset diagnostic.
  • useTranscriptScrollArbiter.ts: for an accepted SCROLL_TO_OFFSET with owner block-window-prepend, measure the scrollTop delta the write actually produced and hand it to the reader hook. Other owners are unchanged; block-window-prepend already bypasses reader cancellation, so this only fixes what the transaction does with the write it was already allowing through.
  • No change to MarkdownHistory or to the corridor/full-mount policy: the field failure occurred with every row mounted and is not a Virtuoso range replacement.

Verification

Relationship to #9754

#9754 attributed the jump to Virtuoso range replacement and removed the small-session full-mount path; its own native smokes then reproduced 6.7k–13.7k px reverse jumps on both platforms. The field diagnostics show the failing transactions had all rows mounted, so the corridor change is not part of this fix. The scrollbar fences from #9754 landed in #9759 and the Markdown empty-fence fix in #9757.

Documentation-impact: none - internal scroll-ownership fix; no documented behavior changes.

@SivanCola
SivanCola requested a review from esengine as a code owner September 3, 2026 09:03
@github-actions github-actions Bot added v2 Go rewrite (1.x) — main-v2 branch, active development desktop Wails desktop app (desktop/**) labels Sep 3, 2026
Problem (esengine#9711): scrolling up through a long Markdown answer skips the
viewport thousands of pixels to another place in the document. The field
diagnostics (stable d9cd713, Windows, reducedMotion=false, all 138 rows
mounted) show eight upward wheel transactions where scrollHeight grows by
G, scrollTop moves by exactly +G with bottomDistance unchanged, and the
reader guard then fires restore-anchor and rewrites scrollTop to its
pre-growth value (19140.95 reappears one frame after a 26393 sample).

Root cause: the reader's transaction anchor is a row's top edge. When the
answer's block window prepends older blocks inside that row above the
visible blocks, MarkdownHistory compensates scrollTop by the same amount
so nothing visible moves. The anchor row's top edge is now G px higher
relative to the viewport and scrollTop moved against the reader, so the
guard reads a reverse displacement and "restores" the anchor, which
scrolls the reader G px up into the newly inserted blocks. That restore is
the reported jump. The guard's positive translateY also extends the
scroller's overflow, which is why extentDelta reads 2G for one frame.

Fix: the arbiter measures the scrollTop delta an accepted
block-window-prepend write produced and hands it to the reader hook's new
absorbOffsetWrite(). The active transaction re-baselines baselineTop,
lastAcceptedTop, expectedTop and anchor.offset to the compensated position
and resets its extent baselines to the grown scrollHeight, so neither the
scrollTop step nor the anchor-edge shift counts as displacement. Real
displacements after the absorption are still detected.

Verification:
- transcript-reader-visual-guard-race.test.tsx replays the field shape
  (row starting 1,450px above the viewport, 7,252px in-row prepend with
  exact compensation). On the previous head it records one anomaly, one
  visual guard, one reader-stability correction and rewinds scrollTop to
  19204; on this head no anomaly, no guard, no correction, scrollTop
  stays compensated, and a later 700px real displacement is still caught.
- transcript-reader-extent-race, native-scrollbar-generation,
  anchor-compensation-race, history-prepend-race, scroll-release,
  markdown-history, recovery-race, test:transcript, test:motion,
  typecheck, test:typecheck, lint:hooks, check:scroll-writer, repolint,
  git diff --check pass on the tree rebased onto esengine#9759.
- Bundle on the rebased tree: gzip 463.292 KiB (budget 463.2 -> 463.4,
  one cross-platform decimal of headroom over an 8-byte margin), raw
  2471.741 KiB (2471.0 -> 2471.8).
@SivanCola
SivanCola force-pushed the fix/transcript-reader-jump-ownership branch from d893511 to 798beb4 Compare September 3, 2026 09:34
@SivanCola
SivanCola merged commit 62dc1de into esengine:main-v2 Sep 3, 2026
28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

desktop Wails desktop app (desktop/**) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant