From fdfd99c63631845f339a5af13a920f8967d80441 Mon Sep 17 00:00:00 2001 From: xoknight Date: Sun, 29 Mar 2026 01:28:54 +0800 Subject: [PATCH] fix: chat auto-scrolls to top after thinking completes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The streaming auto-scroll effect (Effect 2) used scrollContainerRef.scrollTop to pin the viewport, but scrollContainerRef has no fixed height constraint and never overflows — the real scrollable ancestor is the parent div in chat-area. This made the effect a no-op, so after agent transitions (onAgentEnd removing empty messages + onAgentStart adding new ones) the scroll position was lost. Fix: use bottomRef.scrollIntoView({ behavior: 'instant', block: 'end' }) which correctly targets the nearest scrollable ancestor. Also change activeBubbleId scroll from block:'nearest' to block:'end' — the active bubble is always the latest message, and 'nearest' could leave the viewport mid-list after thinking transitions. --- components/chat/chat-session.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/components/chat/chat-session.tsx b/components/chat/chat-session.tsx index dd61bdb07..f14404501 100644 --- a/components/chat/chat-session.tsx +++ b/components/chat/chat-session.tsx @@ -189,23 +189,28 @@ export function ChatSessionComponent({ } }, [msgCount]); - // Auto-scroll: rAF-throttled instant scroll as text grows — only when user is at bottom + // Auto-scroll: rAF-throttled instant scroll as text grows — only when user is at bottom. + // Uses scrollIntoView on bottomRef because scrollContainerRef has no fixed height and + // never actually overflows — the real scrollable ancestor is in chat-area.tsx. const scrollRaf = useRef(0); useEffect(() => { if (!isAtBottomRef.current) return; cancelAnimationFrame(scrollRaf.current); scrollRaf.current = requestAnimationFrame(() => { - const el = scrollContainerRef.current; - if (el) el.scrollTop = el.scrollHeight; + if (bottomRef.current) { + bottomRef.current.scrollIntoView({ behavior: 'instant', block: 'end' }); + } }); }, [session.messages]); - // Scroll to active bubble when it changes + // Scroll to active bubble when it changes — use 'end' alignment so the bubble + // (which is always the latest message) lands at the bottom of the viewport. + // 'nearest' could leave the viewport mid-list after thinking transitions. useEffect(() => { if (activeBubbleId && activeBubbleRef.current) { activeBubbleRef.current.scrollIntoView({ behavior: 'smooth', - block: 'nearest', + block: 'end', }); isAtBottomRef.current = true; }