fix(frontend): fence transcript scrollbar transactions by generation / 滚动条事务按 generation 围栏,拖动中几何变化时重新对齐 - #9759
Merged
SivanCola merged 1 commit intoSep 3, 2026
Conversation
Problem (esengine#9711): scrollbar interaction fights the app's own scroll handling. Dragging the custom scrollbar while rows are still mounting writes through a mapping frozen at pointerdown, so the next pointer move lands on a stale extent. A native-thumb transaction is only ended by call ordering (cancel before generation bump), not by the transaction itself. Root cause: the custom-scrollbar drag captures overflow/maxThumbTop once and never rebases when geometry changes mid-drag. The native-thumb transaction carries no generation, so observe()/finish() cannot tell a stale transaction from a live one and rely on every generation-advancing caller to cancel first. Fix: - useCreationTranscriptScrollbar: rebase the frozen drag mapping at the current physical ratio whenever overflow/maxThumbTop/thumbHeight change, keeping the last pointer sample so the thumb does not visibly jump. The rail-click settle loop waits for two quiet frames but is bounded by a 1000 ms wall-clock budget (RAIL_SETTLE_BUDGET_MS) instead of spinning while geometry keeps changing. - useTranscriptNativeScrollbarOwnership: bind each transaction to the surface generation. A stale-generation observe() ends ownership without claiming the tail; a stale finish() clears the marker and reports no transaction; isActive() is false. The arbiter passes generationRef. Verification: - transcript-native-scrollbar-generation.test.tsx (new): 14 assertions covering same-generation claim-tail, stale observe, stale finish with and without prior observe. Four fail against the previous hook. - creation-transcript-scrollbar.test.ts: rebase preserves the physical ratio and later movement uses the current extent. - transcript-reader-extent-race (native-thumb section), test:transcript, typecheck, test:typecheck, lint:hooks, check:scroll-writer, repolint, git diff --check pass. - Bundle: gzip 463.102 KiB (budget 462.9 -> 463.2), raw 2470.932 KiB (2469.9 -> 2471.0). The generation fence and drag rebase are extracted from esengine#9754; the settle deadline is new.
SivanCola
added a commit
to SivanCola/DeepSeek-Reasonix
that referenced
this pull request
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).
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
Part of the #9711 report: "scrollbar buttons work only intermittently… jump incorrectly" and "the scrollbar could move independently of the viewport". Two scrollbar owners were fragile in the same way — they trusted geometry captured at the start of a gesture and relied on callers to end them.
Changes
Custom (creation-mode) scrollbar —
useCreationTranscriptScrollbar.tsoverflow/maxThumbTop/thumbHeightat pointerdown. When content grows or the viewport resizes mid-drag, the mapping is now rebased at the current physical scroll ratio and the last pointer sample (rebaseFrozenScrollbarDrag), so the next pointer move writes against the live extent and the thumb does not visibly jump.RAIL_SETTLE_BUDGET_MS, same magnitude as the arbiter's mount budget) rather than spinning while geometry keeps changing.Native scrollbar —
useTranscriptNativeScrollbarOwnership.tsobserve()ends native ownership without claiming the tail; a stalefinish()clears the DOM marker and reports no transaction;isActive()is false. On the current base every generation bump already cancels the transaction first, so this turns an ordering invariant into a data invariant rather than fixing a reproduced defect.useTranscriptScrollArbiter.tspassesgenerationRef(one-line plumbing).Verification
transcript-native-scrollbar-generation.test.tsx(new, 14 assertions): same-generation forward progress still claims the tail; stale observe / stale finish with and without a prior observe clean up silently. Four assertions fail against the previous hook.creation-transcript-scrollbar.test.ts: rebase preserves the physical ratio; subsequent movement uses the current extent.transcript-reader-extent-race(native-thumb section),test:transcript,typecheck,test:typecheck,lint:hooks,check:scroll-writer,repolint,git diff --check: pass.Relationship to #9754
The generation fence and drag rebase are extracted from #9754 with their tests. The settle deadline is new. The remaining #9754 scroll-ownership changes (corridor policy, question-jump landing, anchor-compensation gating) need rework and will follow separately.
Documentation-impact: none - scrollbar interaction fixes; no documented behavior changes.