Skip to content

fix(flow-chat): skip collapse compensation in follow+streaming to prevent viewport detaching from bottom#1187

Merged
limityan merged 1 commit into
GCWing:mainfrom
limityan:fix/flow-chat-follow-collapse-stability
Jun 13, 2026
Merged

fix(flow-chat): skip collapse compensation in follow+streaming to prevent viewport detaching from bottom#1187
limityan merged 1 commit into
GCWing:mainfrom
limityan:fix/flow-chat-follow-collapse-stability

Conversation

@limityan

@limityan limityan commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

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:

  1. Injected footer compensation (synthetic whitespace at the bottom) to keep scrollHeight stable
  2. Activated anchor lock to freeze scrollTop on the pre-collapse position
  3. Froze the continuous follow loop via shouldSuspendAutoFollow() for the 1000ms protection window
  4. Relied on a deferred follow path (replayDeferredFollowIfSettled) to resume bottom-tracking after the protection expired

This had race conditions:

  • If streaming ended during the 1000ms window, the continuous follow RAF loop stopped, and replayDeferredFollowIfSettled was only called on scroll events — which might never come
  • The isAtBottom state (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 bottom
  • Browser-clamp-induced scrollTop changes during collapse could be misclassified as user upward scrolls, permanently exiting follow mode

The 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-pins scrollTop to the new physical bottom on the next frame (~16ms), making the content shrink invisible to the user.

Changes in VirtualMessageList.tsx

Location Change Purpose
measureHeightChange shrink branch Early return when follow+streaming Don't inject compensation + anchor lock for content shrink
handleToolCardCollapseIntent Early return when follow+streaming Don't pre-inject compensation before collapse animation
handleScroll shrink-clamp restore Update refs without compensation, return early Prevent follow controller from misclassifying browser clamp as user scroll
New useEffect on follow state transition Clear residual collapse intent + compensation when entering follow+streaming Prevent stale intent from prior non-follow session

Why no new "sink-down" jitter

The original design added compensation to prevent "sink-down" jitter (content visually jumping when scrollHeight shrinks). However, the 60fps continuous follow loop corrects scrollTop within 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:

  1. Follow controller misclassification: Removing return after the shrink-clamp branch allowed the follow controller's handleScroll to see the browser-clamp delta. If explicitUserScrollIntentUntilMsRef had a residual value from a recent user gesture, the follow controller would incorrectly exit follow mode. Fixed by restoring return after updating refs.

  2. 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:web passes

…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.
@limityan limityan merged commit ed4717b into GCWing:main Jun 13, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant