fix(flow-chat): skip collapse compensation in follow+streaming to prevent viewport detaching from bottom#1187
Merged
limityan merged 1 commit intoJun 13, 2026
Conversation
…vent viewport detaching from bottom During streaming, when tool cards auto-collapse or the user toggles them, the previous design injected footer compensation + anchor lock to freeze the viewport on older content during the collapse animation, then relied on a deferred follow path to resume bottom-tracking. That deferred path had race conditions: if streaming ended during the 1000ms protection window, the continuous follow loop stopped and the deferred follow was never replayed, leaving the user stuck above the bottom. Fix: in follow+streaming mode, skip all collapse compensation injection entirely. The continuous follow RAF loop (60fps) naturally re-pins scrollTop to the new physical bottom on the next frame (~16ms), making the content shrink invisible. Since no compensation is injected, there is nothing to accumulate or drain — issue GCWing#1176 (permanent whitespace) cannot occur in this code path. Changes in VirtualMessageList.tsx: 1. measureHeightChange shrink branch: early return when follow+streaming 2. handleToolCardCollapseIntent: early return when follow+streaming 3. handleScroll shrink-clamp restore: update refs without compensation injection when follow+streaming, and return early to prevent the follow controller from misclassifying the browser-clamp delta as a user upward scroll 4. Add effect to clear residual collapse intent + compensation when entering follow+streaming, preventing stale intent from a prior non-follow browsing session from blocking compensation consumption Non-follow mode behavior is unchanged — compensation + anchor lock still protect the visual anchor when the user is browsing history.
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
During streaming, the viewport occasionally stops tracking the bottom of the conversation after tool cards collapse or expand. The user has to manually scroll back down even though they never explicitly scrolled up.
Root Cause
When a tool card collapses during streaming, the previous design:
scrollHeightstablescrollTopon the pre-collapse positionshouldSuspendAutoFollow()for the 1000ms protection windowreplayDeferredFollowIfSettled) to resume bottom-tracking after the protection expiredThis had race conditions:
replayDeferredFollowIfSettledwas only called on scroll events — which might never comeisAtBottomstate (from Virtuoso) did not account for compensation space, so the "scroll to latest" button would not appear even when the user was above the real content bottomThe core design flaw: collapse/expand is an internal system action, not a user navigation intent. It should never break "track the bottom" behavior.
Fix
In follow + streaming mode (
isFollowingOutput && isStreaming), skip all collapse compensation injection. The continuous follow RAF loop (60fps) naturally re-pinsscrollTopto the new physical bottom on the next frame (~16ms), making the content shrink invisible to the user.Changes in
VirtualMessageList.tsxmeasureHeightChangeshrink branchhandleToolCardCollapseIntenthandleScrollshrink-clamp restoreuseEffecton follow state transitionWhy no new "sink-down" jitter
The original design added compensation to prevent "sink-down" jitter (content visually jumping when
scrollHeightshrinks). However, the 60fps continuous follow loop correctsscrollTopwithin one frame (~16ms) — the single-frame deviation is imperceptible. The previous "freeze then jump" behavior was itself the source of the "occasionally not at bottom" bug.Adversarial review findings addressed
Two additional issues were identified during independent adversarial review and fixed:
Follow controller misclassification: Removing
returnafter the shrink-clamp branch allowed the follow controller'shandleScrollto see the browser-clamp delta. IfexplicitUserScrollIntentUntilMsRefhad a residual value from a recent user gesture, the follow controller would incorrectly exit follow mode. Fixed by restoringreturnafter updating refs.Stale collapse intent from non-follow session: A collapse intent set during non-follow browsing (up to 1s lifetime) could persist into a newly-entered follow+streaming session, blocking compensation consumption and suspending the follow loop. Fixed by clearing residual intent + compensation when entering follow+streaming.
Verification
pnpm run type-check:webpasses