From 7eb576c626f56d5d79d51ed862e4ffb1fe7c36a7 Mon Sep 17 00:00:00 2001 From: jnjpng Date: Thu, 22 Jan 2026 14:04:38 -0800 Subject: [PATCH] revert: PR #638 bash abort controller (#641) --- src/cli/App.tsx | 89 +++++++------------------------- src/cli/components/InputRich.tsx | 20 +++---- 2 files changed, 24 insertions(+), 85 deletions(-) diff --git a/src/cli/App.tsx b/src/cli/App.tsx index f288763..f3a0e11 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -759,9 +759,6 @@ export default function App({ const [commandRunning, setCommandRunning, commandRunningRef] = useSyncedState(false); - // Whether a bash mode command is running (for escape key cancellation) - const [bashRunning, setBashRunning] = useState(false); - // Profile load confirmation - when loading a profile and current agent is unsaved const [profileConfirmPending, setProfileConfirmPending] = useState<{ name: string; @@ -790,9 +787,6 @@ export default function App({ >(null); const toolAbortControllerRef = useRef(null); - // AbortController for bash mode command cancellation - const bashAbortControllerRef = useRef(null); - // Eager approval checking: only enabled when resuming a session (LET-7101) // After first successful message, we disable it since any new approvals are from our own turn const [needsEagerApprovalCheck, setNeedsEagerApprovalCheck] = useState( @@ -3436,14 +3430,6 @@ export default function App({ ); const handleInterrupt = useCallback(async () => { - // If we're running a bash mode command, abort it - if (bashAbortControllerRef.current) { - bashAbortControllerRef.current.abort(); - // Don't null the ref here - the finally block in handleBashSubmit will do that - // Just return since the bash command will handle its own cleanup - return; - } - // If we're executing client-side tools, abort them AND the main stream const hasTrackedTools = executingToolCallIdsRef.current.length > 0 || @@ -3917,11 +3903,6 @@ export default function App({ const cmdId = uid("bash"); const startTime = Date.now(); - // Create AbortController for this bash command - const bashAbortController = new AbortController(); - bashAbortControllerRef.current = bashAbortController; - setBashRunning(true); - // Add running bash_command line with streaming state buffersRef.current.byId.set(cmdId, { kind: "bash_command", @@ -3958,7 +3939,6 @@ export default function App({ cwd: process.cwd(), env: getShellEnv(), timeout: 30000, // 30 second timeout - signal: bashAbortController.signal, onOutput: (chunk, stream) => { const entry = buffersRef.current.byId.get(cmdId); if (entry && entry.kind === "bash_command") { @@ -3998,58 +3978,26 @@ export default function App({ output: output || (success ? "" : `Exit code: ${result.exitCode}`), }); } catch (error: unknown) { - // Check if this was an abort/interrupt - const err = error as { name?: string; code?: string; message?: string }; - const isAbort = - bashAbortController.signal.aborted || - err.code === "ABORT_ERR" || - err.name === "AbortError" || - err.message === "The operation was aborted"; + // Handle command errors (timeout, abort, etc.) + const errOutput = + error instanceof Error + ? (error as { stderr?: string; stdout?: string }).stderr || + (error as { stdout?: string }).stdout || + error.message + : String(error); - if (isAbort) { - // User interrupted the command - buffersRef.current.byId.set(cmdId, { - kind: "bash_command", - id: cmdId, - input: command, - output: INTERRUPTED_BY_USER, - phase: "finished", - success: false, - streaming: undefined, - }); - bashCommandCacheRef.current.push({ - input: command, - output: INTERRUPTED_BY_USER, - }); - } else { - // Handle other command errors (timeout, etc.) - const errOutput = - error instanceof Error - ? (error as { stderr?: string; stdout?: string }).stderr || - (error as { stdout?: string }).stdout || - error.message - : String(error); + buffersRef.current.byId.set(cmdId, { + kind: "bash_command", + id: cmdId, + input: command, + output: errOutput, + phase: "finished", + success: false, + streaming: undefined, + }); - buffersRef.current.byId.set(cmdId, { - kind: "bash_command", - id: cmdId, - input: command, - output: errOutput, - phase: "finished", - success: false, - streaming: undefined, - }); - - // Still cache for next user message (even failures are visible to agent) - bashCommandCacheRef.current.push({ - input: command, - output: errOutput, - }); - } - } finally { - // Clear the abort controller ref and state - bashAbortControllerRef.current = null; - setBashRunning(false); + // Still cache for next user message (even failures are visible to agent) + bashCommandCacheRef.current.push({ input: command, output: errOutput }); } refreshDerived(); @@ -8926,7 +8874,6 @@ Plan file path: ${planFilePath}`; streaming={ streaming && !abortControllerRef.current?.signal.aborted } - bashRunning={bashRunning} tokenCount={tokenCount} thinkingMessage={thinkingMessage} onSubmit={onSubmit} diff --git a/src/cli/components/InputRich.tsx b/src/cli/components/InputRich.tsx index ede4a08..591be64 100644 --- a/src/cli/components/InputRich.tsx +++ b/src/cli/components/InputRich.tsx @@ -118,7 +118,6 @@ EventEmitter.defaultMaxListeners = 20; export function Input({ visible = true, streaming, - bashRunning = false, tokenCount, thinkingMessage, onSubmit, @@ -146,7 +145,6 @@ export function Input({ }: { visible?: boolean; streaming: boolean; - bashRunning?: boolean; tokenCount: number; thinkingMessage: string; onSubmit: (message?: string) => Promise<{ submitted: boolean }>; @@ -277,22 +275,22 @@ export function Input({ onEscapeCancel(); }); - // Handle escape key for interrupt (when streaming or bash running) or double-escape-to-clear (when not) + // Handle escape key for interrupt (when streaming) or double-escape-to-clear (when not) useInput((_input, key) => { if (!visible) return; // Debug logging for escape key detection if (process.env.LETTA_DEBUG_KEYS === "1" && key.escape) { // eslint-disable-next-line no-console console.error( - `[debug:InputRich:escape] escape=${key.escape} visible=${visible} onEscapeCancel=${!!onEscapeCancel} streaming=${streaming} bashRunning=${bashRunning}`, + `[debug:InputRich:escape] escape=${key.escape} visible=${visible} onEscapeCancel=${!!onEscapeCancel} streaming=${streaming}`, ); } // Skip if onEscapeCancel is provided - handled by the confirmation handler above if (onEscapeCancel) return; if (key.escape) { - // When streaming or bash command running, use Esc to interrupt - if ((streaming || bashRunning) && onInterrupt && !interruptRequested) { + // When streaming, use Esc to interrupt + if (streaming && onInterrupt && !interruptRequested) { onInterrupt(); // Don't load queued messages into input - let the dequeue effect // in App.tsx process them automatically after the interrupt completes. @@ -321,15 +319,9 @@ export function Input({ useInput((input, key) => { if (!visible) return; - // Handle CTRL-C + // Handle CTRL-C for double-ctrl-c-to-exit + // In bash mode, CTRL-C wipes input but doesn't exit bash mode if (input === "c" && key.ctrl) { - // If bash command or streaming is running, interrupt it (like normal terminal) - if ((bashRunning || streaming) && onInterrupt && !interruptRequested) { - onInterrupt(); - return; - } - - // Otherwise, double-ctrl-c-to-exit behavior if (ctrlCPressed) { // Second CTRL-C - call onExit callback which handles stats and exit if (onExit) onExit();