Skip to content

fix(desktop): keep transcript auto-scroll pinned through stream end - #8291

Closed
Lxiny-zy wants to merge 1 commit into
esengine:main-v2from
Lxiny-zy:fix/transcript-stream-end-autoscroll
Closed

fix(desktop): keep transcript auto-scroll pinned through stream end#8291
Lxiny-zy wants to merge 1 commit into
esengine:main-v2from
Lxiny-zy:fix/transcript-stream-end-autoscroll

Conversation

@Lxiny-zy

Copy link
Copy Markdown
Contributor

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:

  • The streaming auto-scroll effect (Transcript.tsx) only runs on live token changes — after turn_done clears the live snapshot it never fires again.
  • The virtualizer's anchor compensation (anchorTo: "end" + shouldAdjustScrollPositionOnItemSizeChange) keeps the anchor row in place instead of following the tail.
  • The subsequent bottom-state re-evaluation (useScrollManager.updateBottomState) sees distance ≥ 80px, flips the transcript to manual mode, 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:

  1. 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 — the measureElement wrapper calls 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 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 (shouldRunStreamEndRepin gates on pinned) is never yanked back.

Tests

  • 20 assertions on the new pure arbitration functions: shouldAdjustScrollOnItemSizeChange (all mode×pinned quadrants incl. selection modes), shouldRunStreamEndRepin (repin only on live→absent while pinned), and canTranscriptScrollOwnerWrite("row-size", …) permissions.
  • Existing scroll-manager.test.tsx suite still passes (17 assertions).
  • 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, docs or config surface changed

…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
Copilot AI lite review requested due to automatic review settings August 11, 2026 02:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added v2 Go rewrite (1.x) — main-v2 branch, active development desktop Wails desktop app (desktop/**) labels Aug 11, 2026
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>
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>
@Lxiny-zy

Copy link
Copy Markdown
Contributor Author

Superseded by #8264 (stabilize native transcript scrolling), which adapts this PR's row-size ownership, pinned measurement rule and stream-end fallback into the unified gesture controller. Thanks for the review and the Co-authored-by credit in commit 2cae44a.

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.

2 participants