Skip to content

fix(chat): redesign rehydration scroll lifecycle#11491

Closed
roomote[bot] wants to merge 1 commit intomainfrom
fix/chat-scroll-rehydration-lifecycle
Closed

fix(chat): redesign rehydration scroll lifecycle#11491
roomote[bot] wants to merge 1 commit intomainfrom
fix/chat-scroll-rehydration-lifecycle

Conversation

@roomote
Copy link
Contributor

@roomote roomote bot commented Feb 16, 2026

Related GitHub Issue

Supersedes: #11483

Description

Clean, squashed version of the scroll lifecycle redesign from PR #11483.

Redesigns chat rehydration scroll lifecycle so existing chats consistently converge to the newest message on open while preserving explicit user control.

Key implementation details:

  • Introduces an explicit phase model in ChatView for hydration, anchored following, and user-history browsing states
  • Replaces ad-hoc settle loops with a bounded initial settle window using frame and time caps (2.5s soft, 10s hard)
  • Re-anchoring uses deterministic phase transitions instead of race-prone sticky toggles
  • Upward user intent (wheel, keyboard navigation, or pointer/manual upward scroll) immediately disengages automatic follow
  • Scroll-to-bottom CTA re-anchors in one interaction
  • Settle loop respects user scroll intent via stickyFollowRef gating
  • stickyFollowRef is only cleared by explicit user actions (wheel-up, keyboard nav up, pointer drag up, row expansion), not by transient atBottomStateChange flicker during layout reflows
  • followOutput returns "auto" instead of true for consistent Virtuoso behavior
  • scrollToIndex used instead of scrollTo({ top: MAX_SAFE_INTEGER }) for reliable convergence

Test Procedure

  • 6 regression tests in ChatView.scroll-debug-repro.spec.tsx covering:
    • Rehydration converges to bottom
    • Transient settle-time not-at-bottom signals do not disable anchored follow
    • User escape hatch during settle stops forced follow (keyboard nav)
    • Non-wheel upward intent (pointer drag) disengages follow
    • Scroll-to-bottom CTA re-anchors reliably
    • followOutput callback returns correct values based on state
  • All tests pass locally: cd webview-ui && npx vitest run src/components/chat/__tests__/ChatView.scroll-debug-repro.spec.tsx
  • Type checking passes: cd webview-ui && npx tsc --noEmit
  • Full monorepo lint and check-types pass via pre-push hooks

Pre-Submission Checklist

  • Issue Linked: This PR supersedes fix(chat): redesign rehydration scroll lifecycle #11483.
  • Scope: Changes are focused on chat scroll lifecycle behavior and regression coverage only.
  • Self-Review: Code has been reviewed across iterative commits and squashed into a clean single commit.
  • Testing: 6 new regression tests added covering the scroll lifecycle scenarios.
  • Documentation Impact: No documentation updates required.
  • Contribution Guidelines: Read and agreed.

Documentation Updates

  • No documentation updates are required.

Additional Notes

This is a clean squash of the 4-commit scroll-fix branch from PR #11483. The debug plumbing (chatScrollDebug.ts) that was added and removed during iteration is not present in this PR. All reviewer feedback from PR #11483 has been addressed.

Redesigns chat rehydration scroll lifecycle so existing chats
consistently converge to the newest message on open while preserving
explicit user control.

- Introduce an explicit phase model in ChatView for hydration, anchored
  following, and user-history browsing states
- Replace ad-hoc settle loops with a bounded initial settle window
  using frame and time caps
- Re-anchoring now uses deterministic phase transitions instead of
  race-prone sticky toggles
- Upward user intent (wheel, keyboard navigation, or pointer/manual
  upward scroll) immediately disengages automatic follow
- Scroll-to-bottom CTA re-anchors in one interaction
- Settle loop respects user scroll intent via stickyFollowRef gating
- Explicit escape hatches cover keyboard nav, pointer drag, wheel,
  and row expansion scroll-away methods

Squashed from scroll-fix branch (PR #11483).
@dosubot dosubot bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Feb 16, 2026
@dosubot dosubot bot added the bug Something isn't working label Feb 16, 2026
@roomote
Copy link
Contributor Author

roomote bot commented Feb 16, 2026

Rooviewer Clock   See task

The scroll lifecycle redesign is well-structured overall. The explicit phase model and bounded settle window are solid improvements over the previous ad-hoc approach. One race condition found in the settle completion path.

  • completeInitialSettle can override user-initiated phase transitions when isAtBottomRef is still true at the time the settle loop exits after a user escape hatch

Mention @roomote in a comment to request specific changes to this pull request or fix all unresolved issues.

@github-project-automation github-project-automation bot moved this from New to Done in Roo Code Roadmap Feb 16, 2026
Comment on lines +321 to +331
const completeInitialSettle = useCallback(() => {
cancelInitialSettleFrame()
isSettlingRef.current = false
if (isAtBottomRef.current && settleBottomConfirmedRef.current) {
enterAnchoredFollowing()
return
}

transitionScrollPhase("USER_BROWSING_HISTORY")
setShowScrollToBottom(true)
}, [cancelInitialSettleFrame, enterAnchoredFollowing, transitionScrollPhase])
Copy link
Contributor Author

Choose a reason for hiding this comment

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

completeInitialSettle can override a user-initiated phase transition. If the user triggers an escape hatch (wheel-up, PageUp, pointer drag) during the settle window, enterUserBrowsingHistory sets the phase to USER_BROWSING_HISTORY. But the settle loop's next rAF detects that isSettleWindowOpen is now false (phase changed) and calls completeInitialSettle. If isAtBottomRef.current and settleBottomConfirmedRef.current are both still true at that instant (Virtuoso's atBottomStateChange(false) hasn't fired yet), enterAnchoredFollowing() re-engages follow mode, overriding the user's explicit disengage. Adding a phase guard at the top of this function would make it safe:

Suggested change
const completeInitialSettle = useCallback(() => {
cancelInitialSettleFrame()
isSettlingRef.current = false
if (isAtBottomRef.current && settleBottomConfirmedRef.current) {
enterAnchoredFollowing()
return
}
transitionScrollPhase("USER_BROWSING_HISTORY")
setShowScrollToBottom(true)
}, [cancelInitialSettleFrame, enterAnchoredFollowing, transitionScrollPhase])
const completeInitialSettle = useCallback(() => {
cancelInitialSettleFrame()
isSettlingRef.current = false
if (scrollPhaseRef.current !== "HYDRATING_PINNED_TO_BOTTOM") {
return
}
if (isAtBottomRef.current && settleBottomConfirmedRef.current) {
enterAnchoredFollowing()
return
}
transitionScrollPhase("USER_BROWSING_HISTORY")
setShowScrollToBottom(true)
}, [cancelInitialSettleFrame, enterAnchoredFollowing, transitionScrollPhase])

Fix it with Roo Code or mention @roomote and request a fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants