diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 3df10e4e996..89fb168f1a7 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -163,7 +163,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction(null) const stickyFollowRef = useRef(false) const [showScrollToBottom, setShowScrollToBottom] = useState(false) - const [isAtBottom, setIsAtBottom] = useState(false) + const isAtBottomRef = useRef(false) const lastTtsRef = useRef("") const [wasStreaming, setWasStreaming] = useState(false) const [checkpointWarning, setCheckpointWarning] = useState< @@ -520,6 +520,23 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + virtuosoRef.current?.scrollTo({ top: Number.MAX_SAFE_INTEGER, behavior: "auto" }) + }) + } + return () => { + if (rafId !== undefined) { + cancelAnimationFrame(rafId) + } + } }, [task?.ts]) const taskTs = task?.ts @@ -1393,7 +1410,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - if (isAtBottom) { + if (isAtBottomRef.current) { if (isTaller) { scrollToBottomSmooth() } else { @@ -1401,7 +1418,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - const el = scrollContainerRef.current - if (!el) return - const onScroll = () => { - // Consider near-bottom within a small threshold consistent with Virtuoso settings - const nearBottom = Math.abs(el.scrollHeight - el.scrollTop - el.clientHeight) < 10 - if (!nearBottom) { - stickyFollowRef.current = false - } - // Keep UI button state in sync with scroll position - setShowScrollToBottom(!nearBottom) - } - el.addEventListener("scroll", onScroll, { passive: true }) - return () => el.removeEventListener("scroll", onScroll) - }, []) - // Effect to clear checkpoint warning when messages appear or task changes useEffect(() => { if (isHidden || !task) { @@ -1767,9 +1767,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction isAtBottom || stickyFollowRef.current} atBottomStateChange={(isAtBottom: boolean) => { - setIsAtBottom(isAtBottom) - // Only show the scroll-to-bottom button if not at bottom + isAtBottomRef.current = isAtBottom setShowScrollToBottom(!isAtBottom) + // Clear sticky follow when user scrolls away from bottom + if (!isAtBottom) { + stickyFollowRef.current = false + } }} atBottomThreshold={10} initialTopMostItemIndex={groupedMessages.length - 1} @@ -1898,7 +1901,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - if (isAtBottom) { + if (isAtBottomRef.current) { scrollToBottomAuto() } }} diff --git a/webview-ui/src/components/common/CodeBlock.tsx b/webview-ui/src/components/common/CodeBlock.tsx index b13a6ec24d8..042b764a9a8 100644 --- a/webview-ui/src/components/common/CodeBlock.tsx +++ b/webview-ui/src/components/common/CodeBlock.tsx @@ -299,9 +299,6 @@ const CodeBlock = memo( // potentially changes scrollHeight const wasScrolledUpRef = useRef(false) - // Ref to track if outer container was near bottom - const outerContainerNearBottomRef = useRef(false) - // Effect to listen to scroll events and update the ref useEffect(() => { const preElement = preRef.current @@ -323,28 +320,6 @@ const CodeBlock = memo( } }, []) // Empty dependency array: runs once on mount - // Effect to track outer container scroll position - useEffect(() => { - const scrollContainer = document.querySelector('[data-virtuoso-scroller="true"]') - if (!scrollContainer) return - - const handleOuterScroll = () => { - const isAtBottom = - Math.abs(scrollContainer.scrollHeight - scrollContainer.scrollTop - scrollContainer.clientHeight) < - SCROLL_SNAP_TOLERANCE - outerContainerNearBottomRef.current = isAtBottom - } - - scrollContainer.addEventListener("scroll", handleOuterScroll, { passive: true }) - - // Initial check - handleOuterScroll() - - return () => { - scrollContainer.removeEventListener("scroll", handleOuterScroll) - } - }, []) - // Store whether we should scroll after highlighting completes const shouldScrollAfterHighlightRef = useRef(false) @@ -471,14 +446,8 @@ const CodeBlock = memo( wasScrolledUpRef.current = false } - // Also scroll outer container if it was near bottom - if (outerContainerNearBottomRef.current) { - const scrollContainer = document.querySelector('[data-virtuoso-scroller="true"]') - if (scrollContainer) { - scrollContainer.scrollTop = scrollContainer.scrollHeight - outerContainerNearBottomRef.current = true - } - } + // Outer container scrolling is handled by Virtuoso's followOutput + // and ChatView's handleRowHeightChange — no direct DOM manipulation needed. // Reset the flag shouldScrollAfterHighlightRef.current = false