Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 24 additions & 17 deletions apps/web/src/components/ThreadTerminalDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -112,6 +113,7 @@ interface TerminalViewportProps {
terminalId: string;
cwd: string;
runtimeEnv?: Record<string, string>;
onCloseTerminal: (terminalId: string) => void;
focusRequestId: number;
autoFocus: boolean;
resizeEpoch: number;
Expand All @@ -123,6 +125,7 @@ function TerminalViewport({
terminalId,
cwd,
runtimeEnv,
onCloseTerminal,
focusRequestId,
autoFocus,
resizeEpoch,
Expand Down Expand Up @@ -156,13 +159,23 @@ 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;
if (!activeTerminal) return;
try {
await api.terminal.write({ threadId, terminalId, data });
} catch (error) {
if (isIgnorableTerminalWriteError(error)) {
requestTerminalClose();
return;
}
writeSystemMessage(activeTerminal, error instanceof Error ? error.message : fallbackError);
}
};
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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();
}
});

Expand Down Expand Up @@ -370,7 +375,7 @@ function TerminalViewport({
fitAddonRef.current = null;
terminal.dispose();
};
}, [cwd, runtimeEnv, terminalId, threadId]);
}, [cwd, onCloseTerminal, runtimeEnv, terminalId, threadId]);

useEffect(() => {
if (!autoFocus) return;
Expand Down Expand Up @@ -783,6 +788,7 @@ export default function ThreadTerminalDrawer({
terminalId={terminalId}
cwd={cwd}
{...(runtimeEnv ? { runtimeEnv } : {})}
onCloseTerminal={onCloseTerminal}
focusRequestId={focusRequestId}
autoFocus={terminalId === resolvedActiveTerminalId}
resizeEpoch={resizeEpoch}
Expand All @@ -800,6 +806,7 @@ export default function ThreadTerminalDrawer({
terminalId={resolvedActiveTerminalId}
cwd={cwd}
{...(runtimeEnv ? { runtimeEnv } : {})}
onCloseTerminal={onCloseTerminal}
focusRequestId={focusRequestId}
autoFocus
resizeEpoch={resizeEpoch}
Expand Down
29 changes: 29 additions & 0 deletions apps/web/src/terminal-errors.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
20 changes: 20 additions & 0 deletions apps/web/src/terminal-errors.ts
Original file line number Diff line number Diff line change
@@ -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));
}