Stop auto scrolling when agent responds and let scroll area handle scrolling to bottom (#4257)

This commit is contained in:
Zane
2025-08-22 07:15:09 -07:00
committed by GitHub
parent 7f6177b268
commit 69ad4cad63
3 changed files with 18 additions and 53 deletions
+1 -27
View File
@@ -42,7 +42,7 @@
* while remaining flexible enough to support different UI contexts (Hub vs Pair). * 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 { useLocation } from 'react-router-dom';
import { SearchView } from './conversation/SearchView'; import { SearchView } from './conversation/SearchView';
import { AgentHeader } from './AgentHeader'; import { AgentHeader } from './AgentHeader';
@@ -163,13 +163,6 @@ function BaseChatContent({
chat, chat,
setChat, setChat,
onMessageStreamFinish: () => { onMessageStreamFinish: () => {
// Auto-scroll to bottom when message stream finishes
setTimeout(() => {
if (scrollRef.current?.scrollToBottom) {
scrollRef.current.scrollToBottom();
}
}, 300);
// Call the original callback if provided // Call the original callback if provided
onMessageStreamFinish?.(); onMessageStreamFinish?.();
}, },
@@ -303,11 +296,6 @@ function BaseChatContent({
scrollRef.current.scrollToBottom(); scrollRef.current.scrollToBottom();
} }
}, 200); }, 200);
} else {
// Immediate scroll for regular submit
if (scrollRef.current?.scrollToBottom) {
scrollRef.current.scrollToBottom();
}
} }
}; };
@@ -319,18 +307,6 @@ function BaseChatContent({
} }
append(text); 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) // Listen for global scroll-to-bottom requests (e.g., from MCP UI prompt actions)
useEffect(() => { useEffect(() => {
@@ -433,7 +409,6 @@ function BaseChatContent({
setMessages(updatedMessages); setMessages(updatedMessages);
}} }}
isUserMessage={isUserMessage} isUserMessage={isUserMessage}
onScrollToBottom={handleScrollToBottom}
isStreamingMessage={chatState !== ChatState.Idle} isStreamingMessage={chatState !== ChatState.Idle}
onMessageUpdate={onMessageUpdate} onMessageUpdate={onMessageUpdate}
/> />
@@ -450,7 +425,6 @@ function BaseChatContent({
setMessages(updatedMessages); setMessages(updatedMessages);
}} }}
isUserMessage={isUserMessage} isUserMessage={isUserMessage}
onScrollToBottom={handleScrollToBottom}
isStreamingMessage={chatState !== ChatState.Idle} isStreamingMessage={chatState !== ChatState.Idle}
onMessageUpdate={onMessageUpdate} onMessageUpdate={onMessageUpdate}
/> />
@@ -30,7 +30,6 @@ interface ProgressiveMessageListProps {
append?: (value: string) => void; // Make optional append?: (value: string) => void; // Make optional
appendMessage?: (message: Message) => void; // Make optional appendMessage?: (message: Message) => void; // Make optional
isUserMessage: (message: Message) => boolean; isUserMessage: (message: Message) => boolean;
onScrollToBottom?: () => void;
batchSize?: number; batchSize?: number;
batchDelay?: number; batchDelay?: number;
showLoadingThreshold?: number; // Only show loading if more than X messages showLoadingThreshold?: number; // Only show loading if more than X messages
@@ -47,7 +46,6 @@ export default function ProgressiveMessageList({
append = () => {}, append = () => {},
appendMessage = () => {}, appendMessage = () => {},
isUserMessage, isUserMessage,
onScrollToBottom,
batchSize = 20, batchSize = 20,
batchDelay = 20, batchDelay = 20,
showLoadingThreshold = 50, showLoadingThreshold = 50,
@@ -99,10 +97,6 @@ export default function ProgressiveMessageList({
if (nextCount >= messages.length) { if (nextCount >= messages.length) {
setIsLoading(false); setIsLoading(false);
// Trigger scroll to bottom
window.setTimeout(() => {
onScrollToBottom?.();
}, 100);
} else { } else {
// Schedule next batch // Schedule next batch
timeoutRef.current = window.setTimeout(loadNextBatch, batchDelay); timeoutRef.current = window.setTimeout(loadNextBatch, batchDelay);
@@ -121,14 +115,7 @@ export default function ProgressiveMessageList({
timeoutRef.current = null; timeoutRef.current = null;
} }
}; };
}, [ }, [messages.length, batchSize, batchDelay, showLoadingThreshold, renderedCount]);
messages.length,
batchSize,
batchDelay,
showLoadingThreshold,
onScrollToBottom,
renderedCount,
]);
// Cleanup on unmount // Cleanup on unmount
useEffect(() => { useEffect(() => {
@@ -203,9 +190,6 @@ export default function ProgressiveMessageList({
chatId={chat.id} chatId={chat.id}
workingDir={window.appConfig.get('GOOSE_WORKING_DIR') as string} workingDir={window.appConfig.get('GOOSE_WORKING_DIR') as string}
contextType={getContextHandlerType!(message)} contextType={getContextHandlerType!(message)}
onSummaryComplete={() => {
window.setTimeout(() => onScrollToBottom?.(), 100);
}}
/> />
) : ( ) : (
!hasOnlyToolResponses(message) && ( !hasOnlyToolResponses(message) && (
@@ -222,9 +206,6 @@ export default function ProgressiveMessageList({
chatId={chat.id} chatId={chat.id}
workingDir={window.appConfig.get('GOOSE_WORKING_DIR') as string} workingDir={window.appConfig.get('GOOSE_WORKING_DIR') as string}
contextType={getContextHandlerType!(message)} contextType={getContextHandlerType!(message)}
onSummaryComplete={() => {
window.setTimeout(() => onScrollToBottom?.(), 100);
}}
/> />
) : ( ) : (
<GooseMessage <GooseMessage
@@ -265,7 +246,6 @@ export default function ProgressiveMessageList({
onMessageUpdate, onMessageUpdate,
hasContextHandlerContent, hasContextHandlerContent,
getContextHandlerType, getContextHandlerType,
onScrollToBottom,
]); ]);
return ( return (
+16 -5
View File
@@ -67,17 +67,28 @@ const ScrollArea = React.forwardRef<ScrollAreaHandle, ScrollAreaProps>(
const { scrollHeight, scrollTop, clientHeight } = viewport; const { scrollHeight, scrollTop, clientHeight } = viewport;
const scrollBottom = scrollTop + clientHeight; const scrollBottom = scrollTop + clientHeight;
// Allow a small tolerance (2px) for rounding errors const isAtBottom = scrollHeight - scrollBottom <= 10;
const isAtBottom = scrollHeight - scrollBottom <= 2;
setIsFollowing(isAtBottom); setIsFollowing(isAtBottom);
setIsScrolled(scrollTop > 0); setIsScrolled(scrollTop > 0);
}, []); }, []);
React.useEffect(() => { // Track previous scroll height to detect content changes
if (!autoScroll || !isFollowing) return; const prevScrollHeightRef = React.useRef<number>(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]); }, [children, autoScroll, isFollowing, scrollToBottom]);
// Add scroll event listener // Add scroll event listener