diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 0683f2ebd03a..1651ce968906 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -187,7 +187,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction>({}) const prevExpandedRowsRef = useRef>() const scrollContainerRef = useRef(null) - const disableAutoScrollRef = useRef(false) + const stickyFollowRef = useRef(false) const [showScrollToBottom, setShowScrollToBottom] = useState(false) const [isAtBottom, setIsAtBottom] = useState(false) const lastTtsRef = useRef("") @@ -508,9 +508,11 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - if (!disableAutoScrollRef.current) { + if (isAtBottom) { if (isTaller) { scrollToBottomSmooth() } else { @@ -1393,34 +1394,35 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - let timer: ReturnType | undefined - if (!disableAutoScrollRef.current) { - timer = setTimeout(() => scrollToBottomSmooth(), 50) - } - return () => { - if (timer) { - clearTimeout(timer) - } - } - }, [groupedMessages.length, scrollToBottomSmooth]) - + // Disable sticky follow when user scrolls up inside the chat container const handleWheel = useCallback((event: Event) => { const wheelEvent = event as WheelEvent + if (wheelEvent.deltaY < 0 && scrollContainerRef.current?.contains(wheelEvent.target as Node)) { + stickyFollowRef.current = false + } + }, []) + useEvent("wheel", handleWheel, window, { passive: true }) - if (wheelEvent.deltaY && wheelEvent.deltaY < 0) { - if (scrollContainerRef.current?.contains(wheelEvent.target as Node)) { - // User scrolled up - disableAutoScrollRef.current = true + // Also disable sticky follow when the chat container is scrolled away from bottom + useEffect(() => { + 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) }, []) - useEvent("wheel", handleWheel, window, { passive: true }) // passive improves scrolling performance - // Effect to clear checkpoint warning when messages appear or task changes useEffect(() => { if (isHidden || !task) { @@ -1867,12 +1869,11 @@ const ChatViewComponent: React.ForwardRefRenderFunction isAtBottom || stickyFollowRef.current} atBottomStateChange={(isAtBottom: boolean) => { setIsAtBottom(isAtBottom) - if (isAtBottom) { - disableAutoScrollRef.current = false - } - setShowScrollToBottom(disableAutoScrollRef.current && !isAtBottom) + // Only show the scroll-to-bottom button if not at bottom + setShowScrollToBottom(!isAtBottom) }} atBottomThreshold={10} initialTopMostItemIndex={groupedMessages.length - 1} @@ -1893,8 +1894,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - scrollToBottomSmooth() - disableAutoScrollRef.current = false + // Engage sticky follow until user scrolls up + stickyFollowRef.current = true + // Pin immediately to avoid lag during fast streaming + scrollToBottomAuto() + // Hide button immediately to prevent flash + setShowScrollToBottom(false) }}>