Skip to content

Commit a804f99

Browse files
authored
Speculative fixes to try to fix react error. (#19508)
1 parent c43500c commit a804f99

File tree

3 files changed

+35
-18
lines changed

3 files changed

+35
-18
lines changed

packages/cli/src/ui/components/messages/ToolShared.tsx

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,20 +80,24 @@ export function useFocusHint(
8080
isThisShellFocused: boolean,
8181
resultDisplay: ToolResultDisplay | undefined,
8282
) {
83-
const [lastUpdateTime, setLastUpdateTime] = useState<Date | null>(null);
8483
const [userHasFocused, setUserHasFocused] = useState(false);
84+
85+
// Derive a stable reset key for the inactivity timer. For strings and arrays
86+
// (shell output), we use the length to capture updates without referential
87+
// identity issues or expensive deep comparisons.
88+
const resetKey =
89+
typeof resultDisplay === 'string'
90+
? resultDisplay.length
91+
: Array.isArray(resultDisplay)
92+
? resultDisplay.length
93+
: !!resultDisplay;
94+
8595
const showFocusHint = useInactivityTimer(
8696
isThisShellFocusable,
87-
lastUpdateTime ? lastUpdateTime.getTime() : 0,
97+
resetKey,
8898
SHELL_FOCUS_HINT_DELAY_MS,
8999
);
90100

91-
useEffect(() => {
92-
if (resultDisplay) {
93-
setLastUpdateTime(new Date());
94-
}
95-
}, [resultDisplay]);
96-
97101
useEffect(() => {
98102
if (isThisShellFocused) {
99103
setUserHasFocused(true);

packages/cli/src/ui/hooks/useConsoleMessages.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ describe('useConsoleMessages', () => {
9696
});
9797

9898
await act(async () => {
99-
await vi.advanceTimersByTimeAsync(20);
99+
await vi.advanceTimersByTimeAsync(60);
100100
});
101101

102102
expect(result.current.consoleMessages).toEqual([
@@ -114,7 +114,7 @@ describe('useConsoleMessages', () => {
114114
});
115115

116116
await act(async () => {
117-
await vi.advanceTimersByTimeAsync(20);
117+
await vi.advanceTimersByTimeAsync(60);
118118
});
119119

120120
expect(result.current.consoleMessages).toEqual([
@@ -131,7 +131,7 @@ describe('useConsoleMessages', () => {
131131
});
132132

133133
await act(async () => {
134-
await vi.advanceTimersByTimeAsync(20);
134+
await vi.advanceTimersByTimeAsync(60);
135135
});
136136

137137
expect(result.current.consoleMessages).toEqual([
@@ -148,7 +148,7 @@ describe('useConsoleMessages', () => {
148148
});
149149

150150
await act(async () => {
151-
await vi.advanceTimersByTimeAsync(20);
151+
await vi.advanceTimersByTimeAsync(60);
152152
});
153153

154154
expect(result.current.consoleMessages).toHaveLength(1);

packages/cli/src/ui/hooks/useConsoleMessages.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
useEffect,
1010
useReducer,
1111
useRef,
12-
useTransition,
12+
startTransition,
1313
} from 'react';
1414
import type { ConsoleMessageItem } from '../types.js';
1515
import {
@@ -71,10 +71,11 @@ export function useConsoleMessages(): UseConsoleMessagesReturn {
7171
const [consoleMessages, dispatch] = useReducer(consoleMessagesReducer, []);
7272
const messageQueueRef = useRef<ConsoleMessageItem[]>([]);
7373
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
74-
const [, startTransition] = useTransition();
74+
const isProcessingRef = useRef(false);
7575

7676
const processQueue = useCallback(() => {
7777
if (messageQueueRef.current.length > 0) {
78+
isProcessingRef.current = true;
7879
const messagesToProcess = messageQueueRef.current;
7980
messageQueueRef.current = [];
8081
startTransition(() => {
@@ -87,15 +88,26 @@ export function useConsoleMessages(): UseConsoleMessagesReturn {
8788
const handleNewMessage = useCallback(
8889
(message: ConsoleMessageItem) => {
8990
messageQueueRef.current.push(message);
90-
if (!timeoutRef.current) {
91-
// Batch updates using a timeout. 16ms is a reasonable delay to batch
92-
// rapid-fire messages without noticeable lag.
93-
timeoutRef.current = setTimeout(processQueue, 16);
91+
if (!isProcessingRef.current && !timeoutRef.current) {
92+
// Batch updates using a timeout. 50ms is a reasonable delay to batch
93+
// rapid-fire messages without noticeable lag while avoiding React update
94+
// queue flooding.
95+
timeoutRef.current = setTimeout(processQueue, 50);
9496
}
9597
},
9698
[processQueue],
9799
);
98100

101+
// Once the updated consoleMessages have been committed to the screen,
102+
// we can safely process the next batch of queued messages if any exist.
103+
// This completely eliminates overlapping concurrent updates to this state.
104+
useEffect(() => {
105+
isProcessingRef.current = false;
106+
if (messageQueueRef.current.length > 0 && !timeoutRef.current) {
107+
timeoutRef.current = setTimeout(processQueue, 50);
108+
}
109+
}, [consoleMessages, processQueue]);
110+
99111
useEffect(() => {
100112
const handleConsoleLog = (payload: ConsoleLogPayload) => {
101113
let content = payload.content;
@@ -149,6 +161,7 @@ export function useConsoleMessages(): UseConsoleMessagesReturn {
149161
timeoutRef.current = null;
150162
}
151163
messageQueueRef.current = [];
164+
isProcessingRef.current = true;
152165
startTransition(() => {
153166
dispatch({ type: 'CLEAR' });
154167
});

0 commit comments

Comments
 (0)