Speculative fixes to try to fix react error. (#19508)

This commit is contained in:
Jacob Richman
2026-02-18 22:39:32 -08:00
committed by GitHub
parent c43500c50b
commit a804f99fe0
3 changed files with 35 additions and 18 deletions

View File

@@ -80,20 +80,24 @@ export function useFocusHint(
isThisShellFocused: boolean, isThisShellFocused: boolean,
resultDisplay: ToolResultDisplay | undefined, resultDisplay: ToolResultDisplay | undefined,
) { ) {
const [lastUpdateTime, setLastUpdateTime] = useState<Date | null>(null);
const [userHasFocused, setUserHasFocused] = useState(false); const [userHasFocused, setUserHasFocused] = useState(false);
// Derive a stable reset key for the inactivity timer. For strings and arrays
// (shell output), we use the length to capture updates without referential
// identity issues or expensive deep comparisons.
const resetKey =
typeof resultDisplay === 'string'
? resultDisplay.length
: Array.isArray(resultDisplay)
? resultDisplay.length
: !!resultDisplay;
const showFocusHint = useInactivityTimer( const showFocusHint = useInactivityTimer(
isThisShellFocusable, isThisShellFocusable,
lastUpdateTime ? lastUpdateTime.getTime() : 0, resetKey,
SHELL_FOCUS_HINT_DELAY_MS, SHELL_FOCUS_HINT_DELAY_MS,
); );
useEffect(() => {
if (resultDisplay) {
setLastUpdateTime(new Date());
}
}, [resultDisplay]);
useEffect(() => { useEffect(() => {
if (isThisShellFocused) { if (isThisShellFocused) {
setUserHasFocused(true); setUserHasFocused(true);

View File

@@ -96,7 +96,7 @@ describe('useConsoleMessages', () => {
}); });
await act(async () => { await act(async () => {
await vi.advanceTimersByTimeAsync(20); await vi.advanceTimersByTimeAsync(60);
}); });
expect(result.current.consoleMessages).toEqual([ expect(result.current.consoleMessages).toEqual([
@@ -114,7 +114,7 @@ describe('useConsoleMessages', () => {
}); });
await act(async () => { await act(async () => {
await vi.advanceTimersByTimeAsync(20); await vi.advanceTimersByTimeAsync(60);
}); });
expect(result.current.consoleMessages).toEqual([ expect(result.current.consoleMessages).toEqual([
@@ -131,7 +131,7 @@ describe('useConsoleMessages', () => {
}); });
await act(async () => { await act(async () => {
await vi.advanceTimersByTimeAsync(20); await vi.advanceTimersByTimeAsync(60);
}); });
expect(result.current.consoleMessages).toEqual([ expect(result.current.consoleMessages).toEqual([
@@ -148,7 +148,7 @@ describe('useConsoleMessages', () => {
}); });
await act(async () => { await act(async () => {
await vi.advanceTimersByTimeAsync(20); await vi.advanceTimersByTimeAsync(60);
}); });
expect(result.current.consoleMessages).toHaveLength(1); expect(result.current.consoleMessages).toHaveLength(1);

View File

@@ -9,7 +9,7 @@ import {
useEffect, useEffect,
useReducer, useReducer,
useRef, useRef,
useTransition, startTransition,
} from 'react'; } from 'react';
import type { ConsoleMessageItem } from '../types.js'; import type { ConsoleMessageItem } from '../types.js';
import { import {
@@ -71,10 +71,11 @@ export function useConsoleMessages(): UseConsoleMessagesReturn {
const [consoleMessages, dispatch] = useReducer(consoleMessagesReducer, []); const [consoleMessages, dispatch] = useReducer(consoleMessagesReducer, []);
const messageQueueRef = useRef<ConsoleMessageItem[]>([]); const messageQueueRef = useRef<ConsoleMessageItem[]>([]);
const timeoutRef = useRef<NodeJS.Timeout | null>(null); const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const [, startTransition] = useTransition(); const isProcessingRef = useRef(false);
const processQueue = useCallback(() => { const processQueue = useCallback(() => {
if (messageQueueRef.current.length > 0) { if (messageQueueRef.current.length > 0) {
isProcessingRef.current = true;
const messagesToProcess = messageQueueRef.current; const messagesToProcess = messageQueueRef.current;
messageQueueRef.current = []; messageQueueRef.current = [];
startTransition(() => { startTransition(() => {
@@ -87,15 +88,26 @@ export function useConsoleMessages(): UseConsoleMessagesReturn {
const handleNewMessage = useCallback( const handleNewMessage = useCallback(
(message: ConsoleMessageItem) => { (message: ConsoleMessageItem) => {
messageQueueRef.current.push(message); messageQueueRef.current.push(message);
if (!timeoutRef.current) { if (!isProcessingRef.current && !timeoutRef.current) {
// Batch updates using a timeout. 16ms is a reasonable delay to batch // Batch updates using a timeout. 50ms is a reasonable delay to batch
// rapid-fire messages without noticeable lag. // rapid-fire messages without noticeable lag while avoiding React update
timeoutRef.current = setTimeout(processQueue, 16); // queue flooding.
timeoutRef.current = setTimeout(processQueue, 50);
} }
}, },
[processQueue], [processQueue],
); );
// Once the updated consoleMessages have been committed to the screen,
// we can safely process the next batch of queued messages if any exist.
// This completely eliminates overlapping concurrent updates to this state.
useEffect(() => {
isProcessingRef.current = false;
if (messageQueueRef.current.length > 0 && !timeoutRef.current) {
timeoutRef.current = setTimeout(processQueue, 50);
}
}, [consoleMessages, processQueue]);
useEffect(() => { useEffect(() => {
const handleConsoleLog = (payload: ConsoleLogPayload) => { const handleConsoleLog = (payload: ConsoleLogPayload) => {
let content = payload.content; let content = payload.content;
@@ -149,6 +161,7 @@ export function useConsoleMessages(): UseConsoleMessagesReturn {
timeoutRef.current = null; timeoutRef.current = null;
} }
messageQueueRef.current = []; messageQueueRef.current = [];
isProcessingRef.current = true;
startTransition(() => { startTransition(() => {
dispatch({ type: 'CLEAR' }); dispatch({ type: 'CLEAR' });
}); });