diff --git a/ui/desktop/src/components/BaseChat.tsx b/ui/desktop/src/components/BaseChat.tsx index e54f101a73..ccb48b0f61 100644 --- a/ui/desktop/src/components/BaseChat.tsx +++ b/ui/desktop/src/components/BaseChat.tsx @@ -42,7 +42,7 @@ * while remaining flexible enough to support different UI contexts (Hub vs Pair). */ -import React, { useEffect, useContext, createContext, useRef, useCallback } from 'react'; +import React, { useEffect, useContext, createContext, useRef } from 'react'; import { useLocation } from 'react-router-dom'; import { SearchView } from './conversation/SearchView'; import { AgentHeader } from './AgentHeader'; @@ -163,13 +163,6 @@ function BaseChatContent({ chat, setChat, onMessageStreamFinish: () => { - // Auto-scroll to bottom when message stream finishes - setTimeout(() => { - if (scrollRef.current?.scrollToBottom) { - scrollRef.current.scrollToBottom(); - } - }, 300); - // Call the original callback if provided onMessageStreamFinish?.(); }, @@ -303,11 +296,6 @@ function BaseChatContent({ scrollRef.current.scrollToBottom(); } }, 200); - } else { - // Immediate scroll for regular submit - if (scrollRef.current?.scrollToBottom) { - scrollRef.current.scrollToBottom(); - } } }; @@ -319,18 +307,6 @@ function BaseChatContent({ } append(text); }; - // Callback to handle scroll to bottom from ProgressiveMessageList - const handleScrollToBottom = useCallback(() => { - // Only auto-scroll if user is not actively typing - const isUserTyping = document.activeElement?.id === 'dynamic-textarea'; - if (!isUserTyping) { - setTimeout(() => { - if (scrollRef.current?.scrollToBottom) { - scrollRef.current.scrollToBottom(); - } - }, 100); - } - }, []); // Listen for global scroll-to-bottom requests (e.g., from MCP UI prompt actions) useEffect(() => { @@ -433,7 +409,6 @@ function BaseChatContent({ setMessages(updatedMessages); }} isUserMessage={isUserMessage} - onScrollToBottom={handleScrollToBottom} isStreamingMessage={chatState !== ChatState.Idle} onMessageUpdate={onMessageUpdate} /> @@ -450,7 +425,6 @@ function BaseChatContent({ setMessages(updatedMessages); }} isUserMessage={isUserMessage} - onScrollToBottom={handleScrollToBottom} isStreamingMessage={chatState !== ChatState.Idle} onMessageUpdate={onMessageUpdate} /> diff --git a/ui/desktop/src/components/ProgressiveMessageList.tsx b/ui/desktop/src/components/ProgressiveMessageList.tsx index e6b0d0321c..a01b8f9d68 100644 --- a/ui/desktop/src/components/ProgressiveMessageList.tsx +++ b/ui/desktop/src/components/ProgressiveMessageList.tsx @@ -30,7 +30,6 @@ interface ProgressiveMessageListProps { append?: (value: string) => void; // Make optional appendMessage?: (message: Message) => void; // Make optional isUserMessage: (message: Message) => boolean; - onScrollToBottom?: () => void; batchSize?: number; batchDelay?: number; showLoadingThreshold?: number; // Only show loading if more than X messages @@ -47,7 +46,6 @@ export default function ProgressiveMessageList({ append = () => {}, appendMessage = () => {}, isUserMessage, - onScrollToBottom, batchSize = 20, batchDelay = 20, showLoadingThreshold = 50, @@ -99,10 +97,6 @@ export default function ProgressiveMessageList({ if (nextCount >= messages.length) { setIsLoading(false); - // Trigger scroll to bottom - window.setTimeout(() => { - onScrollToBottom?.(); - }, 100); } else { // Schedule next batch timeoutRef.current = window.setTimeout(loadNextBatch, batchDelay); @@ -121,14 +115,7 @@ export default function ProgressiveMessageList({ timeoutRef.current = null; } }; - }, [ - messages.length, - batchSize, - batchDelay, - showLoadingThreshold, - onScrollToBottom, - renderedCount, - ]); + }, [messages.length, batchSize, batchDelay, showLoadingThreshold, renderedCount]); // Cleanup on unmount useEffect(() => { @@ -203,9 +190,6 @@ export default function ProgressiveMessageList({ chatId={chat.id} workingDir={window.appConfig.get('GOOSE_WORKING_DIR') as string} contextType={getContextHandlerType!(message)} - onSummaryComplete={() => { - window.setTimeout(() => onScrollToBottom?.(), 100); - }} /> ) : ( !hasOnlyToolResponses(message) && ( @@ -222,9 +206,6 @@ export default function ProgressiveMessageList({ chatId={chat.id} workingDir={window.appConfig.get('GOOSE_WORKING_DIR') as string} contextType={getContextHandlerType!(message)} - onSummaryComplete={() => { - window.setTimeout(() => onScrollToBottom?.(), 100); - }} /> ) : ( ( const { scrollHeight, scrollTop, clientHeight } = viewport; const scrollBottom = scrollTop + clientHeight; - // Allow a small tolerance (2px) for rounding errors - const isAtBottom = scrollHeight - scrollBottom <= 2; + const isAtBottom = scrollHeight - scrollBottom <= 10; setIsFollowing(isAtBottom); setIsScrolled(scrollTop > 0); }, []); - React.useEffect(() => { - if (!autoScroll || !isFollowing) return; + // Track previous scroll height to detect content changes + const prevScrollHeightRef = React.useRef(0); - scrollToBottom(); + React.useEffect(() => { + if (!autoScroll || !isFollowing || !viewportRef.current) return; + + const viewport = viewportRef.current; + const currentScrollHeight = viewport.scrollHeight; + + // Only auto-scroll if content has actually grown (new content added) + // and we were already following (at the bottom) + if (currentScrollHeight > prevScrollHeightRef.current) { + scrollToBottom(); + } + + prevScrollHeightRef.current = currentScrollHeight; }, [children, autoScroll, isFollowing, scrollToBottom]); // Add scroll event listener