diff --git a/apps/server/src/terminal/Layers/Manager.test.ts b/apps/server/src/terminal/Layers/Manager.test.ts index 304b49d42b79..21ab1e7d5cc7 100644 --- a/apps/server/src/terminal/Layers/Manager.test.ts +++ b/apps/server/src/terminal/Layers/Manager.test.ts @@ -639,6 +639,38 @@ describe("TerminalManager", () => { manager.dispose(); }); + it("silently drops write to exited terminal instead of throwing", async () => { + const { manager, ptyAdapter } = makeManager(); + await manager.open(openInput()); + const process = ptyAdapter.processes[0]; + expect(process).toBeDefined(); + if (!process) return; + + process.emitExit({ exitCode: 0, signal: 0 }); + + await expect( + manager.write({ threadId: "thread-1", data: "hello\n" }), + ).resolves.toBeUndefined(); + + manager.dispose(); + }); + + it("silently drops resize to exited terminal instead of throwing", async () => { + const { manager, ptyAdapter } = makeManager(); + await manager.open(openInput()); + const process = ptyAdapter.processes[0]; + expect(process).toBeDefined(); + if (!process) return; + + process.emitExit({ exitCode: 0, signal: 0 }); + + await expect( + manager.resize({ threadId: "thread-1", cols: 200, rows: 50 }), + ).resolves.toBeUndefined(); + + manager.dispose(); + }); + it("starts zsh with prompt spacer disabled to avoid `%` end markers", async () => { if (process.platform === "win32") return; const { manager, ptyAdapter } = makeManager(5, { diff --git a/apps/server/src/terminal/Layers/Manager.ts b/apps/server/src/terminal/Layers/Manager.ts index fa766da570f1..13d88cedbba5 100644 --- a/apps/server/src/terminal/Layers/Manager.ts +++ b/apps/server/src/terminal/Layers/Manager.ts @@ -438,25 +438,29 @@ export class TerminalManagerRuntime extends EventEmitter const input = decodeTerminalWriteInput(raw); const session = this.requireSession(input.threadId, input.terminalId); if (!session.process || session.status !== "running") { - throw new Error( - `Terminal is not running for thread: ${input.threadId}, terminal: ${input.terminalId}`, - ); + return; + } + try { + session.process.write(input.data); + } catch { + // Process may have exited between the status check and the write call. } - session.process.write(input.data); } async resize(raw: TerminalResizeInput): Promise { const input = decodeTerminalResizeInput(raw); const session = this.requireSession(input.threadId, input.terminalId); if (!session.process || session.status !== "running") { - throw new Error( - `Terminal is not running for thread: ${input.threadId}, terminal: ${input.terminalId}`, - ); + return; } session.cols = input.cols; session.rows = input.rows; session.updatedAt = new Date().toISOString(); - session.process.resize(input.cols, input.rows); + try { + session.process.resize(input.cols, input.rows); + } catch { + // Process may have exited between the status check and the resize call. + } } async clear(raw: TerminalClearInput): Promise {