Skip to content

Commit f59e17c

Browse files
authored
🤖 Fix auto-scroll and TODO flickering (#347)
**Auto-scroll issue:** FileEditToolCall's DiffRenderer shows "Processing..." while highlighting, then expands after auto-scroll runs. **Fix:** Double RAF ensures scroll happens after async content finishes rendering. **TODO flickering issue:** created new array reference even when content identical. **Fix:** Deep equality check in StreamingMessageAggregator prevents unnecessary reference changes. _Generated with `cmux`_
1 parent 5b7692e commit f59e17c

File tree

2 files changed

+25
-6
lines changed

2 files changed

+25
-6
lines changed

src/hooks/useAutoScroll.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@ export function useAutoScroll() {
2424
const performAutoScroll = useCallback(() => {
2525
if (!contentRef.current) return;
2626

27+
// Double RAF: First frame for DOM updates (e.g., DiffRenderer async highlighting),
28+
// second frame to scroll after layout is complete
2729
requestAnimationFrame(() => {
28-
// Check ref.current not state - avoids race condition where queued frames
29-
// execute after user scrolls up but still see old autoScroll=true
30-
if (contentRef.current && autoScrollRef.current) {
31-
contentRef.current.scrollTop = contentRef.current.scrollHeight;
32-
}
30+
requestAnimationFrame(() => {
31+
// Check ref.current not state - avoids race condition where queued frames
32+
// execute after user scrolls up but still see old autoScroll=true
33+
if (contentRef.current && autoScrollRef.current) {
34+
contentRef.current.scrollTop = contentRef.current.scrollHeight;
35+
}
36+
});
3337
});
3438
}, []); // No deps - ref ensures we always check current value
3539

src/utils/messages/StreamingMessageAggregator.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,18 @@ export class StreamingMessageAggregator {
8484
return this.recencyTimestamp;
8585
}
8686

87+
/**
88+
* Check if two TODO lists are equal (deep comparison).
89+
* Prevents unnecessary re-renders when todo_write is called with identical content.
90+
*/
91+
private todosEqual(a: TodoItem[], b: TodoItem[]): boolean {
92+
if (a.length !== b.length) return false;
93+
return a.every((todoA, i) => {
94+
const todoB = b[i];
95+
return todoA.content === todoB.content && todoA.status === todoB.status;
96+
});
97+
}
98+
8799
/**
88100
* Get the current TODO list.
89101
* Updated whenever todo_write succeeds.
@@ -438,7 +450,10 @@ export class StreamingMessageAggregator {
438450
data.result.success
439451
) {
440452
const args = toolPart.input as { todos: TodoItem[] };
441-
this.currentTodos = args.todos;
453+
// Only update if todos actually changed (prevents flickering from reference changes)
454+
if (!this.todosEqual(this.currentTodos, args.todos)) {
455+
this.currentTodos = args.todos;
456+
}
442457
}
443458
}
444459
this.invalidateCache();

0 commit comments

Comments
 (0)