From 529fdec81a4b8a589d6832443c088e5465183bde Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 19 Oct 2025 21:15:52 -0500 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=A4=96=20Fix=20auto-scroll=20with=20a?= =?UTF-8?q?sync-rendered=20content?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use double RAF in performAutoScroll to ensure scroll happens after async content (like DiffRenderer highlighting) finishes rendering. First frame allows DOM updates, second frame scrolls after layout. --- src/hooks/useAutoScroll.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/hooks/useAutoScroll.ts b/src/hooks/useAutoScroll.ts index 091b49aa9..540adf47b 100644 --- a/src/hooks/useAutoScroll.ts +++ b/src/hooks/useAutoScroll.ts @@ -24,12 +24,16 @@ export function useAutoScroll() { const performAutoScroll = useCallback(() => { if (!contentRef.current) return; + // Double RAF: First frame for DOM updates (e.g., DiffRenderer async highlighting), + // second frame to scroll after layout is complete requestAnimationFrame(() => { - // Check ref.current not state - avoids race condition where queued frames - // execute after user scrolls up but still see old autoScroll=true - if (contentRef.current && autoScrollRef.current) { - contentRef.current.scrollTop = contentRef.current.scrollHeight; - } + requestAnimationFrame(() => { + // Check ref.current not state - avoids race condition where queued frames + // execute after user scrolls up but still see old autoScroll=true + if (contentRef.current && autoScrollRef.current) { + contentRef.current.scrollTop = contentRef.current.scrollHeight; + } + }); }); }, []); // No deps - ref ensures we always check current value From c48c5025efff44667b043138e3190df1ce0f0a52 Mon Sep 17 00:00:00 2001 From: Ammar Date: Sun, 19 Oct 2025 21:20:38 -0500 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=A4=96=20Prevent=20TODO=20list=20flic?= =?UTF-8?q?kering=20with=20deep=20equality=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only update currentTodos reference when content actually changes. Prevents PinnedTodoList from flickering when todo_write is called with identical content. --- .../messages/StreamingMessageAggregator.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/utils/messages/StreamingMessageAggregator.ts b/src/utils/messages/StreamingMessageAggregator.ts index 40b6d1d24..1e8b2efb5 100644 --- a/src/utils/messages/StreamingMessageAggregator.ts +++ b/src/utils/messages/StreamingMessageAggregator.ts @@ -84,6 +84,18 @@ export class StreamingMessageAggregator { return this.recencyTimestamp; } + /** + * Check if two TODO lists are equal (deep comparison). + * Prevents unnecessary re-renders when todo_write is called with identical content. + */ + private todosEqual(a: TodoItem[], b: TodoItem[]): boolean { + if (a.length !== b.length) return false; + return a.every((todoA, i) => { + const todoB = b[i]; + return todoA.content === todoB.content && todoA.status === todoB.status; + }); + } + /** * Get the current TODO list. * Updated whenever todo_write succeeds. @@ -438,7 +450,10 @@ export class StreamingMessageAggregator { data.result.success ) { const args = toolPart.input as { todos: TodoItem[] }; - this.currentTodos = args.todos; + // Only update if todos actually changed (prevents flickering from reference changes) + if (!this.todosEqual(this.currentTodos, args.todos)) { + this.currentTodos = args.todos; + } } } this.invalidateCache();