From 7dc0ff156f1ec07e6379dcf55ff1fe8414cb1b9d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 28 Feb 2026 01:54:19 +0000 Subject: [PATCH] Handle terminal exit without hard write errors Co-authored-by: Julius Marminge --- .../src/components/ThreadTerminalDrawer.tsx | 41 +++++++++++-------- apps/web/src/terminal-errors.test.ts | 29 +++++++++++++ apps/web/src/terminal-errors.ts | 20 +++++++++ 3 files changed, 73 insertions(+), 17 deletions(-) create mode 100644 apps/web/src/terminal-errors.test.ts create mode 100644 apps/web/src/terminal-errors.ts diff --git a/apps/web/src/components/ThreadTerminalDrawer.tsx b/apps/web/src/components/ThreadTerminalDrawer.tsx index ab86780487a8..a2b499279fda 100644 --- a/apps/web/src/components/ThreadTerminalDrawer.tsx +++ b/apps/web/src/components/ThreadTerminalDrawer.tsx @@ -25,6 +25,7 @@ import { MAX_THREAD_TERMINAL_COUNT, type ThreadTerminalGroup, } from "../types"; +import { isIgnorableTerminalWriteError } from "../terminal-errors"; import { readNativeApi } from "~/nativeApi"; const MIN_DRAWER_HEIGHT = 180; @@ -112,6 +113,7 @@ interface TerminalViewportProps { terminalId: string; cwd: string; runtimeEnv?: Record; + onCloseTerminal: (terminalId: string) => void; focusRequestId: number; autoFocus: boolean; resizeEpoch: number; @@ -123,6 +125,7 @@ function TerminalViewport({ terminalId, cwd, runtimeEnv, + onCloseTerminal, focusRequestId, autoFocus, resizeEpoch, @@ -156,6 +159,12 @@ function TerminalViewport({ const api = readNativeApi(); if (!api) return; + let closeRequested = false; + const requestTerminalClose = () => { + if (closeRequested) return; + closeRequested = true; + onCloseTerminal(terminalId); + }; const sendTerminalInput = async (data: string, fallbackError: string) => { const activeTerminal = terminalRef.current; @@ -163,6 +172,10 @@ function TerminalViewport({ try { await api.terminal.write({ threadId, terminalId, data }); } catch (error) { + if (isIgnorableTerminalWriteError(error)) { + requestTerminalClose(); + return; + } writeSystemMessage(activeTerminal, error instanceof Error ? error.message : fallbackError); } }; @@ -243,12 +256,13 @@ function TerminalViewport({ const inputDisposable = terminal.onData((data) => { void api.terminal .write({ threadId, terminalId, data }) - .catch((err) => - writeSystemMessage( - terminal, - err instanceof Error ? err.message : "Terminal write failed", - ), - ); + .catch((err) => { + if (isIgnorableTerminalWriteError(err)) { + requestTerminalClose(); + return; + } + writeSystemMessage(terminal, err instanceof Error ? err.message : "Terminal write failed"); + }); }); const themeObserver = new MutationObserver(() => { @@ -325,16 +339,7 @@ function TerminalViewport({ } if (event.type === "exited") { - const details = [ - typeof event.exitCode === "number" ? `code ${event.exitCode}` : null, - typeof event.exitSignal === "number" ? `signal ${event.exitSignal}` : null, - ] - .filter((value): value is string => value !== null) - .join(", "); - writeSystemMessage( - activeTerminal, - details.length > 0 ? `Process exited (${details})` : "Process exited", - ); + requestTerminalClose(); } }); @@ -370,7 +375,7 @@ function TerminalViewport({ fitAddonRef.current = null; terminal.dispose(); }; - }, [cwd, runtimeEnv, terminalId, threadId]); + }, [cwd, onCloseTerminal, runtimeEnv, terminalId, threadId]); useEffect(() => { if (!autoFocus) return; @@ -783,6 +788,7 @@ export default function ThreadTerminalDrawer({ terminalId={terminalId} cwd={cwd} {...(runtimeEnv ? { runtimeEnv } : {})} + onCloseTerminal={onCloseTerminal} focusRequestId={focusRequestId} autoFocus={terminalId === resolvedActiveTerminalId} resizeEpoch={resizeEpoch} @@ -800,6 +806,7 @@ export default function ThreadTerminalDrawer({ terminalId={resolvedActiveTerminalId} cwd={cwd} {...(runtimeEnv ? { runtimeEnv } : {})} + onCloseTerminal={onCloseTerminal} focusRequestId={focusRequestId} autoFocus resizeEpoch={resizeEpoch} diff --git a/apps/web/src/terminal-errors.test.ts b/apps/web/src/terminal-errors.test.ts new file mode 100644 index 000000000000..76382b6c14cb --- /dev/null +++ b/apps/web/src/terminal-errors.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "vitest"; + +import { isIgnorableTerminalWriteError } from "./terminal-errors"; + +describe("isIgnorableTerminalWriteError", () => { + it("treats not-running terminal errors as ignorable", () => { + expect( + isIgnorableTerminalWriteError( + new Error("Terminal is not running for thread: thread-1, terminal: default"), + ), + ).toBe(true); + }); + + it("treats unknown terminal thread errors as ignorable", () => { + expect( + isIgnorableTerminalWriteError( + `TerminalError: Failed to write to terminal +├─ cause: Error: Unknown terminal thread: thread-1, terminal: default`, + ), + ).toBe(true); + }); + + it("does not ignore unrelated terminal write failures", () => { + expect(isIgnorableTerminalWriteError(new Error("Request timed out: terminal.write"))).toBe( + false, + ); + expect(isIgnorableTerminalWriteError(null)).toBe(false); + }); +}); diff --git a/apps/web/src/terminal-errors.ts b/apps/web/src/terminal-errors.ts new file mode 100644 index 000000000000..abc31fdd6abe --- /dev/null +++ b/apps/web/src/terminal-errors.ts @@ -0,0 +1,20 @@ +const BENIGN_TERMINAL_WRITE_ERROR_MARKERS = [ + "terminal is not running", + "unknown terminal thread", +] as const; + +function errorMessage(error: unknown): string | null { + if (error instanceof Error) { + return error.message; + } + if (typeof error === "string") { + return error; + } + return null; +} + +export function isIgnorableTerminalWriteError(error: unknown): boolean { + const message = errorMessage(error)?.toLowerCase(); + if (!message) return false; + return BENIGN_TERMINAL_WRITE_ERROR_MARKERS.some((marker) => message.includes(marker)); +}