fix(frontend): absorb block-window prepends in the reader transaction / 向上滚动时吸收块窗口前插补偿,消除会话跳位 - #9760
Merged
SivanCola merged 1 commit intoSep 3, 2026
Conversation
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
force-pushed
the
fix/transcript-reader-jump-ownership
branch
from
September 3, 2026 09:34
d893511 to
798beb4
Compare
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.
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:
scrollTopscrollHeightbottomDistancethen
transcript.scroll-anomaly(reverseDisplacement 7252,extentDelta 14492 ≈ 2×,restore-anchor), and one frame laterscrollTopis back at 19140.95. The recording shows the viewport landing on an unrelated region each time.+G scrollTop / +G scrollHeight / bottomDistance constantis theblock-window-prependcompensation written byMarkdownHistorywhen 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, andscrollTopmoved against the reader. The guard reads both as a reverse jump, firesrestore-anchor, and scrolls the reader G px up into the newly inserted blocks. That restore is the jump. The guard's positivetranslateYalso extends scrollable overflow for one frame, which is whyextentDeltareads2G.Change
useTranscriptReaderExtentStability.ts: newabsorbOffsetWrite(element, delta). The active transaction re-baselinesbaselineTop,lastAcceptedTop,expectedTopandanchor.offsetto the compensated position and resets its extent baselines to the grownscrollHeight, emitting areader-transaction … absorbed-offsetdiagnostic.useTranscriptScrollArbiter.ts: for an acceptedSCROLL_TO_OFFSETwith ownerblock-window-prepend, measure the scrollTop delta the write actually produced and hand it to the reader hook. Other owners are unchanged;block-window-prependalready bypasses reader cancellation, so this only fixes what the transaction does with the write it was already allowing through.MarkdownHistoryor to the corridor/full-mount policy: the field failure occurred with every row mounted and is not a Virtuoso range replacement.Verification
transcript-reader-visual-guard-race.test.tsxreplays the field shape (row starting 1,450px above the viewport, 7,252px in-row prepend with exact compensation). Against the previous head: one anomaly, one visual guard, onereader-stabilitycorrection, scrollTop rewound to 19204. Against this head: no anomaly, no guard, no correction, scrollTop stays compensated, the next wheel step continues cleanly, and a later 700px real displacement is still detected.transcript-reader-extent-race,transcript-anchor-compensation-race,transcript-history-prepend-race,transcript-scroll-release,markdown-history,transcript-recovery-race,test:transcript,test:motion,typecheck,test:typecheck,lint:hooks,check:scroll-writer,repolint,git diff --check: pass.main-v2after fix(frontend): omit empty fenced Markdown blocks / 去掉空代码围栏产生的空白卡片 #9757 and fix(frontend): fence transcript scrollbar transactions by generation / 滚动条事务按 generation 围栏,拖动中几何变化时重新对齐 #9759 merged (798beb4e8). Bundle on the rebased tree: gzip 463.292 KiB (budget 463.2 → 463.4; the next decimal left an 8-byte margin, so one cross-platform decimal of headroom is retained — fix(frontend): fence transcript scrollbar transactions by generation / 滚动条事务按 generation 围栏,拖动中几何变化时重新对齐 #9759's three CI platforms agreed to within 0.05 KiB), raw 2471.741 KiB (2471.0 → 2471.8).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.