From 4d9fea64eedcd9fb9cc2243964ac58041b270e0b Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Thu, 1 Jan 2026 21:33:49 -0800 Subject: [PATCH] fix: patch flakey interrupt (#446) --- src/cli/App.tsx | 26 ++++++++++++++----- src/cli/components/AgentInfoBar.tsx | 16 +++++------- src/cli/components/InlineQuestionApproval.tsx | 2 +- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 75b24e3..fac6255 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -136,7 +136,6 @@ import { } from "./helpers/toolNameMapping"; import { alwaysRequiresUserInput, - isFancyUITool, isTaskTool, } from "./helpers/toolNameMapping.js"; import { useSuspend } from "./hooks/useSuspend/useSuspend.ts"; @@ -1162,6 +1161,8 @@ export default function App({ sessionStatsRef.current.updateUsageFromBuffers(buffersRef.current); const wasInterrupted = !!buffersRef.current.interrupted; + const wasAborted = !!signal?.aborted; + let stopReasonToHandle = wasAborted ? "cancelled" : stopReason; // Immediate refresh after stream completes to show final state unless // the user already cancelled (handleInterrupt rendered the UI). @@ -1169,8 +1170,15 @@ export default function App({ refreshDerived(); } + // If the turn was interrupted client-side but the backend had already emitted + // requires_approval, treat it as a cancel. This avoids re-entering approval flow + // and keeps queue-cancel flags consistent with the normal cancel branch below. + if (wasInterrupted && stopReasonToHandle === "requires_approval") { + stopReasonToHandle = "cancelled"; + } + // Case 1: Turn ended normally - if (stopReason === "end_turn") { + if (stopReasonToHandle === "end_turn") { setStreaming(false); llmApiErrorRetriesRef.current = 0; // Reset retry counter on success @@ -1208,7 +1216,7 @@ export default function App({ } // Case 1.5: Stream was cancelled by user - if (stopReason === "cancelled") { + if (stopReasonToHandle === "cancelled") { setStreaming(false); // Check if this cancel was triggered by queue threshold @@ -1244,7 +1252,7 @@ export default function App({ } // Case 2: Requires approval - if (stopReason === "requires_approval") { + if (stopReasonToHandle === "requires_approval") { // Clear stale state immediately to prevent ID mismatch bugs setAutoHandledResults([]); setAutoDeniedApprovals([]); @@ -1658,7 +1666,10 @@ export default function App({ // Unexpected stop reason (error, llm_api_error, etc.) // Check if this is a retriable error (transient LLM API error) - const retriable = await isRetriableError(stopReason, lastRunId); + const retriable = await isRetriableError( + stopReasonToHandle, + lastRunId, + ); if ( retriable && @@ -1716,8 +1727,9 @@ export default function App({ telemetry.trackError( fallbackError ? "FallbackError" - : stopReason || "unknown_stop_reason", - fallbackError || `Stream stopped with reason: ${stopReason}`, + : stopReasonToHandle || "unknown_stop_reason", + fallbackError || + `Stream stopped with reason: ${stopReasonToHandle}`, "message_stream", { modelId: currentModelId || undefined, diff --git a/src/cli/components/AgentInfoBar.tsx b/src/cli/components/AgentInfoBar.tsx index faf91a1..4c9bf7c 100644 --- a/src/cli/components/AgentInfoBar.tsx +++ b/src/cli/components/AgentInfoBar.tsx @@ -54,20 +54,16 @@ export const AgentInfoBar = memo(function AgentInfoBar({ {isCloudUser && ( - <> - - Open in ADE ↗ - - + + Open in ADE ↗ + )} {isCloudUser && ( - <> - - View usage ↗ - - + + View usage ↗ + )} {!isCloudUser && · {serverUrl}} diff --git a/src/cli/components/InlineQuestionApproval.tsx b/src/cli/components/InlineQuestionApproval.tsx index 0b61d86..501b55a 100644 --- a/src/cli/components/InlineQuestionApproval.tsx +++ b/src/cli/components/InlineQuestionApproval.tsx @@ -130,7 +130,7 @@ export const InlineQuestionApproval = memo( }); } // Always insert the space character - setCustomText((prev) => prev + " "); + setCustomText((prev) => `${prev} `); return; } if (key.escape) {