/** * ProgressiveMessageList Component * * A performance-optimized message list that renders messages progressively * to prevent UI blocking when loading long chat sessions. This component * renders messages in batches with a loading indicator, maintaining full * compatibility with the search functionality. * * Key Features: * - Progressive rendering in configurable batches * - Loading indicator during batch processing * - Maintains search functionality compatibility * - Smooth user experience with responsive UI * - Configurable batch size and delay */ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Message } from '../api'; import GooseMessage from './GooseMessage'; import UserMessage from './UserMessage'; import { SystemNotificationInline } from './context_management/SystemNotificationInline'; import { NotificationEvent } from '../types/message'; import LoadingGoose from './LoadingGoose'; import { ChatType } from '../types/chat'; import { identifyConsecutiveToolCalls, isInChain } from '../utils/toolCallChaining'; interface ProgressiveMessageListProps { messages: Message[]; chat: Pick; toolCallNotifications?: Map; // Make optional append?: (value: string) => void; // Make optional isUserMessage: (message: Message) => boolean; batchSize?: number; batchDelay?: number; showLoadingThreshold?: number; // Only show loading if more than X messages // Custom render function for messages renderMessage?: (message: Message, index: number) => React.ReactNode | null; isStreamingMessage?: boolean; // Whether messages are currently being streamed onMessageUpdate?: (messageId: string, newContent: string) => void; onRenderingComplete?: () => void; // Callback when all messages are rendered submitElicitationResponse?: ( elicitationId: string, userData: Record ) => Promise; } export default function ProgressiveMessageList({ messages, chat, toolCallNotifications = new Map(), append = () => {}, isUserMessage, batchSize = 20, batchDelay = 20, showLoadingThreshold = 50, renderMessage, // Custom render function isStreamingMessage = false, // Whether messages are currently being streamed onMessageUpdate, onRenderingComplete, submitElicitationResponse, }: ProgressiveMessageListProps) { const [renderedCount, setRenderedCount] = useState(() => { // Initialize with either all messages (if small) or first batch (if large) return messages.length <= showLoadingThreshold ? messages.length : Math.min(batchSize, messages.length); }); const [isLoading, setIsLoading] = useState(() => messages.length > showLoadingThreshold); const timeoutRef = useRef(null); const mountedRef = useRef(true); const hasOnlyToolResponses = (message: Message) => message.content.every((c) => c.type === 'toolResponse'); const hasInlineSystemNotification = (message: Message): boolean => { return message.content.some( (content) => content.type === 'systemNotification' && content.notificationType === 'inlineMessage' ); }; // Simple progressive loading - start immediately when component mounts if needed useEffect(() => { if (messages.length <= showLoadingThreshold) { setRenderedCount(messages.length); setIsLoading(false); // For small lists, call completion callback immediately if (onRenderingComplete) { setTimeout(() => onRenderingComplete(), 50); } return; } // Large list - start progressive loading const loadNextBatch = () => { setRenderedCount((current) => { const nextCount = Math.min(current + batchSize, messages.length); if (nextCount >= messages.length) { setIsLoading(false); // Call the completion callback after a brief delay to ensure DOM is updated if (onRenderingComplete) { setTimeout(() => onRenderingComplete(), 50); } } else { // Schedule next batch timeoutRef.current = window.setTimeout(loadNextBatch, batchDelay); } return nextCount; }); }; // Start loading after a short delay timeoutRef.current = window.setTimeout(loadNextBatch, batchDelay); return () => { if (timeoutRef.current) { window.clearTimeout(timeoutRef.current); timeoutRef.current = null; } }; }, [ messages.length, batchSize, batchDelay, showLoadingThreshold, renderedCount, onRenderingComplete, ]); // Cleanup on unmount useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; if (timeoutRef.current) { window.clearTimeout(timeoutRef.current); } }; }, []); // Force complete rendering when search is active useEffect(() => { // Only add listener if we're actually loading if (!isLoading) { return; } const handleKeyDown = (e: KeyboardEvent) => { const isMac = window.electron.platform === 'darwin'; const isSearchShortcut = (isMac ? e.metaKey : e.ctrlKey) && e.key === 'f'; if (isSearchShortcut) { // Immediately render all messages when search is triggered setRenderedCount(messages.length); setIsLoading(false); if (timeoutRef.current) { window.clearTimeout(timeoutRef.current); timeoutRef.current = null; } } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [isLoading, messages.length]); // Detect tool call chains const toolCallChains = useMemo(() => identifyConsecutiveToolCalls(messages), [messages]); // Render messages up to the current rendered count const renderMessages = useCallback(() => { const messagesToRender = messages.slice(0, renderedCount); return messagesToRender .map((message, index) => { if (!message.metadata.userVisible) { return null; } if (renderMessage) { return renderMessage(message, index); } // Default rendering logic (for BaseChat) if (!chat) { console.warn( 'ProgressiveMessageList: chat prop is required when not using custom renderMessage' ); return null; } // System notifications are never user messages, handle them first if (hasInlineSystemNotification(message)) { return (
); } const isUser = isUserMessage(message); const messageIsInChain = isInChain(index, toolCallChains); return (
{isUser ? ( !hasOnlyToolResponses(message) && ( ) ) : ( )}
); }) .filter(Boolean); }, [ messages, renderedCount, renderMessage, isUserMessage, chat, append, toolCallNotifications, isStreamingMessage, onMessageUpdate, toolCallChains, submitElicitationResponse, ]); return ( <> {renderMessages()} {/* Loading indicator when progressively rendering */} {isLoading && (
Press Cmd/Ctrl+F to load all messages immediately for search
)} ); }