Stop auto scrolling when agent responds and let scroll area handle scrolling to bottom (#4257)
This commit is contained in:
@@ -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 (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user