diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 6f07d4b..b77ad6e 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -862,8 +862,12 @@ export default function App({ clearCompletedSubagents(); while (true) { + // Capture the signal BEFORE any async operations + // This prevents a race where handleInterrupt nulls the ref during await + const signal = abortControllerRef.current?.signal; + // Check if cancelled before starting new stream - if (abortControllerRef.current?.signal.aborted) { + if (signal?.aborted) { setStreaming(false); return; } @@ -874,6 +878,12 @@ export default function App({ currentInput, ); + // Check again after network call - user may have pressed Escape during sendMessageStream + if (signal?.aborted) { + setStreaming(false); + return; + } + // Define callback to sync agent state on first message chunk // This ensures the UI shows the correct model as early as possible const syncAgentState = async () => { @@ -929,7 +939,7 @@ export default function App({ stream, buffersRef.current, refreshDerivedThrottled, - abortControllerRef.current?.signal, + signal, // Use captured signal, not ref (which may be nulled by handleInterrupt) syncAgentState, ); @@ -1510,6 +1520,9 @@ export default function App({ // If EAGER_CANCEL is enabled, immediately stop everything client-side first if (EAGER_CANCEL) { + // Prevent multiple handleInterrupt calls while state updates are pending + setInterruptRequested(true); + // Abort the stream via abort signal if (abortControllerRef.current) { abortControllerRef.current.abort(); @@ -1540,12 +1553,13 @@ export default function App({ // Silently ignore - cancellation already happened client-side }); - // Reset cancellation flag after cleanup is complete. + // Reset cancellation flags after cleanup is complete. // This allows the dequeue effect to process any queued messages. // We use setTimeout to ensure React state updates (setStreaming, etc.) // have been processed before the dequeue effect runs. setTimeout(() => { userCancelledRef.current = false; + setInterruptRequested(false); }, 0); return;