fix(terminal): write process input off the UI thread (#2183)

A child that stopped reading its input could freeze the whole window, because terminal input was written synchronously on the UI thread. Terminal, Run and debug stdin now share a per-session ordered writer queue bounded to 4 MiB and 1,024 writes. An overflow rejects the whole write and shows an error; closing or killing a session discards pending input without waiting for the child, and an explicit stdin EOF drains accepted writes first.

Refs #2160
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-11-09 03:06:46 +08:00
1 джерело 004da36bed
коміт 43bcd66b2b
13 files changed
+512 -36

No files matched your search

+7 -2
Переглянути файл
@@ -1257,7 +1257,7 @@ export async function sendStdin(sessionId: string, text: string): Promise<void>
} catch (e) {
enqueue(sessionId, {
kind: "system",
text: tx`Cannot write to stdin: ${e instanceof Error ? e.message : String(e)}\n`,
text: tx`Cannot write to stdin: ${e instanceof Error ? tx(e.message) : String(e)}\n`,
});
}
}
@@ -1266,7 +1266,12 @@ export async function sendStdin(sessionId: string, text: string): Promise<void>
export function writeTerminal(sessionId: string, data: string) {
const session = getSession(sessionId);
if (!session || session.runId === null || session.status !== "running") return;
void call(commands.runStdin(session.runId, data)).catch(() => undefined);
void call(commands.runStdin(session.runId, data)).catch((e) => {
enqueue(sessionId, {
kind: "system",
text: tx`Cannot write to stdin: ${e instanceof Error ? tx(e.message) : String(e)}\n`,
});
});
}
/** The terminal console's size in cells; the process's terminal follows it. */
+22
Переглянути файл
@@ -16,6 +16,7 @@ const ipc = vi.hoisted(() => ({
termKill: vi.fn(),
termResize: vi.fn(),
termIsBusy: vi.fn(),
termWrite: vi.fn(),
},
}));
const workbench = vi.hoisted(() => ({
@@ -122,6 +123,27 @@ describe("resolveStartCwd", () => {
describe("terminal start directory fallback", () => {
beforeEach(() => vi.clearAllMocks());
it("shows rejected terminal input instead of silently discarding it", async () => {
ipc.commands.termSpawn.mockResolvedValue({ id: 9, title: "zsh" });
ipc.commands.termWrite.mockRejectedValueOnce(new Error("input queue full"));
const session = new TerminalSession({
key: "s-input-error",
cwd: "/repo",
onChange: () => undefined,
onFocus: () => undefined,
});
await session.start();
session.input("paste");
await vi.waitFor(() =>
expect(workbench.notify).toHaveBeenCalledWith({
level: "error",
title: "Cannot write to terminal",
body: "input queue full",
}),
);
session.dispose();
});
it("spawns a restored session in the project root when its saved directory is gone", async () => {
ipc.commands.fsEntryKind.mockRejectedValue(new Error("gone"));
ipc.commands.termSpawn.mockResolvedValue({ id: 7, title: "zsh" });
+9 -2
Переглянути файл
@@ -9,6 +9,7 @@ import { SearchAddon, type ISearchOptions } from "@xterm/addon-search";
import "@xterm/xterm/css/xterm.css";
import { Channel } from "@tauri-apps/api/core";
import { call, commands, isTauri, type TerminalId } from "@/platform/ipc";
import { tx } from "@/platform/i18n";
import { editorService, notifications, registerFontZoomSurface } from "@/workbench/contributions";
import { fitScheduler, settledResize } from "./fit";
import { installTerminalDrop } from "./drop";
@@ -658,14 +659,20 @@ export class TerminalSession {
this.pendingInput += data;
return;
}
void commands.termWrite(this.ptyId, data);
void call(commands.termWrite(this.ptyId, data)).catch((error) => {
notifications().notify({
level: "error",
title: tx("Cannot write to terminal"),
body: error instanceof Error ? tx(error.message) : String(error),
});
});
}
/** Sends keystrokes buffered while the PTY was starting. */
private flushPendingInput() {
const queued = this.pendingInput;
this.pendingInput = "";
if (queued && this.ptyId !== null) void commands.termWrite(this.ptyId, queued);
if (queued && this.ptyId !== null) this.input(queued);
}
/** Writes input immediately (shortcuts translated outside xterm). */