fix(desktop): keep transcript auto-scroll pinned through stream end - #8291
Closed
Lxiny-zy wants to merge 1 commit into
Closed
fix(desktop): keep transcript auto-scroll pinned through stream end#8291Lxiny-zy wants to merge 1 commit into
Lxiny-zy wants to merge 1 commit into
Conversation
…sengine#8269) Long answers (>=8k chars) render their final blocks through an async markdown worker after the stream settles. Nothing repinned after that layout growth: the streaming auto-scroll effect only runs on live token changes, and the virtualizer's anchor compensation keeps the anchor row in place instead of following the tail - then the bottom-state re-evaluation (distance >= threshold) flips the transcript to manual mode, permanently disabling auto-scroll until the user scrolls again. Two coordinated fixes: 1. While pinned (tail-follow), the virtualizer no longer compensates scroll position on row-size changes (shouldAdjustScrollOnItemSizeChange returns false when pinned). Every measured row-size change while pinned is instead folded into the existing frame-batched repin (measureElement wrapper -> scheduleRepinIfWasPinned with a new "row-size" scroll owner), so tail growth, async worker swaps and image loads all keep the viewport at the bottom. 2. Stream-end fallback: when the live snapshot settles (turn_done) and the transcript is still pinned, scrollToBottomAfterLayout waits a few frames for the synchronous markdown layout and snaps to the bottom. A user who scrolled away during the stream is never yanked back. Tests: 20 assertions on the new pure arbitration functions (shouldAdjustScrollOnItemSizeChange, shouldRunStreamEndRepin, row-size owner permission); existing scroll-manager tests still pass; tsc --noEmit and eslint clean. Cache-impact: none - frontend-only rendering/scroll behavior Cache-guard: none System-prompt-review: none - no prompt, config schema, memory, output style, or skill behavior changes Documentation-impact: none - fixes existing transcript scroll behavior; no CLI or user-facing docs change
SivanCola
added a commit
to SivanCola/DeepSeek-Reasonix
that referenced
this pull request
Aug 11, 2026
Problem: - Nested downward handoff at the physical bottom detached streaming tail-follow. - Passive tail writes rejected during an active gesture were dropped, so final token and row-size growth could leave the viewport behind. Root cause: - Nested handoff discarded wheel direction and always released auto-scroll. - Gesture arbitration had no generation-aware replay for blocked passive owners. Fix: - Propagate normalized nested deltas and release only for upward or detached reading intent. - Replay blocked stream, resize, footer, and row-size repins once after gesture idle using the current scrollHeight. - Cancel deferred work on manual intent, selection, explicit navigation, and tab generation changes. - Adapt esengine#8291's row-size ownership, pinned measurement rule, and stream-end fallback into the unified gesture controller. - Add deterministic and real Chromium regression coverage. Verification: - pnpm test:transcript - pnpm test:transcript-browser - pnpm test:typecheck - pnpm test:motion - pnpm build - go run ./tools/repolint Co-authored-by: LI Xinyu <Lxy1962@bupt.edu.cn>
8 tasks
SivanCola
added a commit
that referenced
this pull request
Aug 11, 2026
…8264) * fix(desktop): stop trackpad scroll stalls in the chat transcript Problem: On macOS, two-finger scrolling the chat content area felt sticky and jumpy—especially over markdown tables—because nested overflow containers latched the trackpad and virtualizer/stream rewrites fought native inertia mid-gesture. Root cause: CSS overflow-x:auto promotes overflow-y to auto, so ordinary tables/code blocks became vertical scroll ports; virtualizer height compensation and stream tail-follow could still write scrollTop while the user was gesturing. Fix: Add a short user-gesture lock that blocks compensating scroll writes, promote edge/non-scrollable nested wheels to the transcript with latching handoff, keep small tables and code blocks out of nested vertical scroll, and mark large virtual tables for handoff. Verification: npx tsx scroll-manager, transcript-scroll-session, nested-scroll-handoff, markdown-table-virtual, typography-overflow-contract, and transcript-virtualization tests. * fix(desktop): fold large tables and idle-remeasure after scroll Problem: Large markdown tables still introduced nested vertical scroll ports by default, and virtualizer remeasure could jump after a trackpad gesture settled. Root cause: Oversized tables mounted a max-height virtual scroller immediately, and height compensation was deferred only mid-gesture, not batched once after the hold window. Fix: Collapse tables above the virtual threshold to a 12-row document-flow preview with Expand all / Collapse; expanded tables keep the nested virtual scroller with handoff. After gesture idle, measure once and reconcile the reading anchor. Verification: npx tsx markdown-table-virtual, scroll-manager, transcript-scroll-session, and nested-scroll-handoff tests. * fix(desktop): stabilize native transcript scrolling Problem: Long variable-height transcripts could stall or jump while trackpad inertia, native scrollbar input, or Windows middle-button auto-scroll overlapped virtualizer and resize compensation. Creation scrollbar sensitivity could also change while rows mounted. Root cause: Scroll ownership only covered wheel, touch, and keyboard intent; treated unowned native scroll events ambiguously; remeasured the entire virtualizer after every idle gesture; and recomputed custom-scrollbar geometry during a drag. A stale scrollend could also settle a newer gesture. Fix: Model native input sessions explicitly, arbitrate controller-owned scroll events, normalize wheel delta modes, guard stale scrollend, freeze custom-scrollbar drag geometry, and measure only real layout invalidations with anchor reconciliation. Split interaction and scrollbar concerns out of Transcript and add real-browser stability coverage. Verification: - pnpm --dir desktop/frontend test:transcript - pnpm --dir desktop/frontend test:transcript-browser - pnpm --dir desktop/frontend typecheck - pnpm --dir desktop/frontend test:typecheck - pnpm --dir desktop/frontend test:motion - pnpm --dir desktop/frontend build - go run ./tools/repolint * fix(desktop): latch manual transcript scroll intent Problem: A short upward wheel, trackpad, native scrollbar, or Creation scrollbar movement could remain inside the 80px near-bottom band. After the gesture quiet window, streaming output could therefore re-enable tail-follow and pull the viewport back down. Root cause: The scroll manager treated geometric proximity as user intent and recomputed stick mode from the near-bottom threshold after every native scroll event. Fix: Latch explicit user detachment across gesture settlement, keep resize and stream repins disabled while detached, and restore tail-follow only at the physical bottom or through an explicit bottom/reset action. Apply the same contract to the Creation scrollbar. Verification: - pnpm --dir desktop/frontend test:transcript - pnpm --dir desktop/frontend test:transcript-browser - pnpm --dir desktop/frontend test:typecheck - pnpm --dir desktop/frontend test:motion - pnpm --dir desktop/frontend build - go run ./tools/repolint * fix(desktop): preserve tail follow across scroll ownership Problem: - Nested downward handoff at the physical bottom detached streaming tail-follow. - Passive tail writes rejected during an active gesture were dropped, so final token and row-size growth could leave the viewport behind. Root cause: - Nested handoff discarded wheel direction and always released auto-scroll. - Gesture arbitration had no generation-aware replay for blocked passive owners. Fix: - Propagate normalized nested deltas and release only for upward or detached reading intent. - Replay blocked stream, resize, footer, and row-size repins once after gesture idle using the current scrollHeight. - Cancel deferred work on manual intent, selection, explicit navigation, and tab generation changes. - Adapt #8291's row-size ownership, pinned measurement rule, and stream-end fallback into the unified gesture controller. - Add deterministic and real Chromium regression coverage. Verification: - pnpm test:transcript - pnpm test:transcript-browser - pnpm test:typecheck - pnpm test:motion - pnpm build - go run ./tools/repolint Co-authored-by: LI Xinyu <Lxy1962@bupt.edu.cn> --------- Co-authored-by: LI Xinyu <Lxy1962@bupt.edu.cn>
Contributor
Author
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.
Problem
Fixes the transcript auto-scroll failure reported for long answers. Answer text ≥8000 chars (STREAMING_TAIL_THRESHOLD) renders its final blocks through an async markdown worker after the stream settles (
MarkdownHistory.tsx→ worker parse). Nothing repins after that layout growth:Transcript.tsx) only runs on live token changes — afterturn_doneclears the live snapshot it never fires again.anchorTo: "end"+shouldAdjustScrollPositionOnItemSizeChange) keeps the anchor row in place instead of following the tail.useScrollManager.updateBottomState) sees distance ≥ 80px, flips the transcript tomanualmode, and permanently disables auto-scroll — the viewport is stuck mid-transcript with only the jump-to-bottom button as an escape.Fix
Two coordinated changes:
Pinned rows never go through anchor compensation. New pure rule
shouldAdjustScrollOnItemSizeChange(pinned, mode): while pinned (tail-follow) the virtualizer does not compensate on row-size changes. Every measured row-size change while pinned is instead folded into the existing frame-batched repin — themeasureElementwrapper callsscheduleRepinIfWasPinnedwith a new"row-size"scroll owner — so tail growth, async worker swaps and image loads all keep the viewport at the bottom.Stream-end fallback repin. When the live snapshot settles (
turn_done) and the transcript is still pinned,scrollToBottomAfterLayout(3)waits a few frames for synchronous markdown layout and snaps to the bottom. A user who scrolled away during the stream (shouldRunStreamEndRepingates on pinned) is never yanked back.Tests
shouldAdjustScrollOnItemSizeChange(all mode×pinned quadrants incl. selection modes),shouldRunStreamEndRepin(repin only on live→absent while pinned), andcanTranscriptScrollOwnerWrite("row-size", …)permissions.scroll-manager.test.tsxsuite still passes (17 assertions).tsc --noEmitand eslint clean.Cache-impact: none - frontend-only rendering/scroll behavior
Cache-guard: none
System-prompt-review: none - no prompt, config schema, memory, output style, or skill behavior changes
Documentation-impact: none - fixes existing transcript scroll behavior; no CLI, docs or config surface changed