From e2fad0e10cde85c253133b1bceaabf144bde5c5f Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Sat, 10 Jan 2026 11:18:44 +0000 Subject: [PATCH] The TypeScript check passed with no errors. The debounce pattern from `VirtualizedProcessLogs.tsx` has been successfully rolled out to `ConversationList.tsx`. (#1909) ## Summary Added 100ms debounce to `ConversationList.tsx` matching the pattern in `VirtualizedProcessLogs.tsx`: 1. **Added refs** (lines 94-99): `pendingUpdateRef` to store pending updates and `debounceTimeoutRef` to track the timeout 2. **Modified `onEntriesUpdated`** (lines 115-149): Now stores updates in the ref and debounces state updates with a 100ms delay 3. **Added cleanup effect** (lines 107-113): Clears any pending timeout on unmount to prevent memory leaks --- .../components/ui-new/ConversationList.tsx | 49 +++++++++++++++---- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/ui-new/ConversationList.tsx b/frontend/src/components/ui-new/ConversationList.tsx index c323e45b..e84e5cef 100644 --- a/frontend/src/components/ui-new/ConversationList.tsx +++ b/frontend/src/components/ui-new/ConversationList.tsx @@ -91,6 +91,12 @@ export function ConversationList({ attempt, task }: ConversationListProps) { useState | null>(null); const [loading, setLoading] = useState(true); const { setEntries, reset } = useEntries(); + const pendingUpdateRef = useRef<{ + entries: PatchTypeWithKey[]; + addType: AddEntryType; + loading: boolean; + } | null>(null); + const debounceTimeoutRef = useRef | null>(null); useEffect(() => { setLoading(true); @@ -98,25 +104,48 @@ export function ConversationList({ attempt, task }: ConversationListProps) { reset(); }, [attempt.id, reset]); + useEffect(() => { + return () => { + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current); + } + }; + }, []); + const onEntriesUpdated = ( newEntries: PatchTypeWithKey[], addType: AddEntryType, newLoading: boolean ) => { - let scrollModifier: ScrollModifier = InitialDataScrollModifier; + pendingUpdateRef.current = { + entries: newEntries, + addType, + loading: newLoading, + }; - if (addType === 'plan' && !loading) { - scrollModifier = ScrollToTopOfLastItem; - } else if (addType === 'running' && !loading) { - scrollModifier = AutoScrollToBottom; + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current); } - setChannelData({ data: newEntries, scrollModifier }); - setEntries(newEntries); + debounceTimeoutRef.current = setTimeout(() => { + const pending = pendingUpdateRef.current; + if (!pending) return; - if (loading) { - setLoading(newLoading); - } + let scrollModifier: ScrollModifier = InitialDataScrollModifier; + + if (pending.addType === 'plan' && !loading) { + scrollModifier = ScrollToTopOfLastItem; + } else if (pending.addType === 'running' && !loading) { + scrollModifier = AutoScrollToBottom; + } + + setChannelData({ data: pending.entries, scrollModifier }); + setEntries(pending.entries); + + if (loading) { + setLoading(pending.loading); + } + }, 100); }; useConversationHistory({ attempt, onEntriesUpdated });