Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 20 additions & 52 deletions ui/desktop/src/components/BaseChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,50 +117,6 @@ function BaseChatContent({
const [currentRecipeTitle, setCurrentRecipeTitle] = React.useState<string | null>(null);
const { isCompacting, handleManualCompaction } = useContextManager();

// Timeout ref for debouncing auto-scroll
const autoScrollTimeoutRef = useRef<number | null>(null);
// Track if user was following when agent started responding
const wasFollowingRef = useRef<boolean>(true);

const isNearBottom = React.useCallback(() => {
if (!scrollRef.current) return false;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const viewport = scrollRef.current as any;
if (!viewport.viewportRef?.current) return false;

const viewportElement = viewport.viewportRef.current;
const { scrollHeight, scrollTop, clientHeight } = viewportElement;
const scrollBottom = scrollTop + clientHeight;
const distanceFromBottom = scrollHeight - scrollBottom;

return distanceFromBottom <= 100;
}, []);

// Function to auto-scroll if user was following when agent started
const conditionalAutoScroll = React.useCallback(() => {
// Clear any existing timeout
if (autoScrollTimeoutRef.current) {
clearTimeout(autoScrollTimeoutRef.current);
}

// Debounce the auto-scroll to prevent jumpy behavior and prevent multiple rapid scrolls
autoScrollTimeoutRef.current = window.setTimeout(() => {
// Only auto-scroll if user was following when the agent started responding
if (wasFollowingRef.current && scrollRef.current) {
scrollRef.current.scrollToBottom();
}
}, 150);
}, []);

useEffect(() => {
return () => {
if (autoScrollTimeoutRef.current) {
clearTimeout(autoScrollTimeoutRef.current);
}
};
}, []);

// Use shared chat engine
const {
messages,
Expand All @@ -187,14 +143,10 @@ function BaseChatContent({
chat,
setChat,
onMessageStreamFinish: () => {
conditionalAutoScroll();

// Call the original callback if provided
onMessageStreamFinish?.();
},
onMessageSent: () => {
wasFollowingRef.current = isNearBottom();

// Mark that user has started using the recipe
if (recipeConfig) {
setHasStartedUsingRecipe(true);
Expand Down Expand Up @@ -275,12 +227,23 @@ function BaseChatContent({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

// Track if this is the initial render for session resuming
const initialRenderRef = useRef(true);

// Auto-scroll when messages are loaded (for session resuming)
const handleRenderingComplete = React.useCallback(() => {
if (scrollRef.current?.scrollToBottom) {
scrollRef.current.scrollToBottom();
// Only force scroll on the very first render
if (initialRenderRef.current && messages.length > 0) {
initialRenderRef.current = false;
if (scrollRef.current?.scrollToBottom) {
scrollRef.current.scrollToBottom();
}
} else if (scrollRef.current?.isFollowing) {
if (scrollRef.current?.scrollToBottom) {
scrollRef.current.scrollToBottom();
}
}
}, []);
}, [messages.length]);

// Handle submit
const handleSubmit = (e: React.FormEvent) => {
Expand Down Expand Up @@ -441,7 +404,12 @@ function BaseChatContent({
onClick={async () => {
clearError();

await handleManualCompaction(messages, setMessages, append, chat.sessionId);
await handleManualCompaction(
messages,
setMessages,
append,
chat.sessionId
);
}}
>
Summarize Conversation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,12 @@ describe('ContextManager', () => {
const { result } = renderContextManager();

await act(async () => {
await result.current.handleAutoCompaction(mockMessages, mockSetMessages, mockAppend, 'test-session-id');
await result.current.handleAutoCompaction(
mockMessages,
mockSetMessages,
mockAppend,
'test-session-id'
);
});

expect(mockManageContextFromBackend).toHaveBeenCalledWith({
Expand Down Expand Up @@ -226,7 +231,12 @@ describe('ContextManager', () => {
const { result } = renderContextManager();

await act(async () => {
await result.current.handleAutoCompaction(mockMessages, mockSetMessages, mockAppend, "test-session-id");
await result.current.handleAutoCompaction(
mockMessages,
mockSetMessages,
mockAppend,
'test-session-id'
);
});

expect(result.current.compactionError).toBe('Backend error');
Expand Down Expand Up @@ -257,7 +267,12 @@ describe('ContextManager', () => {

// Start compaction
act(() => {
result.current.handleAutoCompaction(mockMessages, mockSetMessages, mockAppend, "test-session-id");
result.current.handleAutoCompaction(
mockMessages,
mockSetMessages,
mockAppend,
'test-session-id'
);
});

// Should be compacting
Expand Down Expand Up @@ -307,7 +322,12 @@ describe('ContextManager', () => {
const { result } = renderContextManager();

await act(async () => {
await result.current.handleAutoCompaction(messages, mockSetMessages, mockAppend, "test-session-id");
await result.current.handleAutoCompaction(
messages,
mockSetMessages,
mockAppend,
'test-session-id'
);
});

// No server messages -> setMessages called with empty list
Expand Down Expand Up @@ -370,7 +390,12 @@ describe('ContextManager', () => {
const { result } = renderContextManager();

await act(async () => {
await result.current.handleManualCompaction(mockMessages, mockSetMessages, mockAppend, 'test-session-id');
await result.current.handleManualCompaction(
mockMessages,
mockSetMessages,
mockAppend,
'test-session-id'
);
});

expect(mockManageContextFromBackend).toHaveBeenCalledWith({
Expand Down Expand Up @@ -483,7 +508,12 @@ describe('ContextManager', () => {
const { result } = renderContextManager();

await act(async () => {
await result.current.handleManualCompaction(mockMessages, mockSetMessages, mockAppend, 'test-session-id');
await result.current.handleManualCompaction(
mockMessages,
mockSetMessages,
mockAppend,
'test-session-id'
);
});

// Verify all three messages are set
Expand All @@ -510,7 +540,12 @@ describe('ContextManager', () => {
const { result } = renderContextManager();

await act(async () => {
await result.current.handleAutoCompaction(mockMessages, mockSetMessages, mockAppend, "test-session-id");
await result.current.handleAutoCompaction(
mockMessages,
mockSetMessages,
mockAppend,
'test-session-id'
);
});

expect(result.current.compactionError).toBe('Unknown error during compaction');
Expand Down Expand Up @@ -541,7 +576,12 @@ describe('ContextManager', () => {
const { result } = renderContextManager();

await act(async () => {
await result.current.handleAutoCompaction(mockMessages, mockSetMessages, mockAppend, "test-session-id");
await result.current.handleAutoCompaction(
mockMessages,
mockSetMessages,
mockAppend,
'test-session-id'
);
});

// Should complete without error even if content is not text
Expand Down
Loading
Loading