diff --git a/packages/tui/src/terminal.ts b/packages/tui/src/terminal.ts index d5204fe9c..d420e41bf 100644 --- a/packages/tui/src/terminal.ts +++ b/packages/tui/src/terminal.ts @@ -717,10 +717,17 @@ export class ProcessTerminal implements Terminal { // stderr-guard in pi-utils (mirrors openai/codex#24459). suppressTerminalStderr(); - // Save previous state and enable raw mode + // A multiplexer or SSH disconnect can leave isTTY true after its pty has + // been revoked. Raw mode is then impossible, so take the normal terminal + // disconnect path rather than letting Bun abort startup with EIO. this.#wasRaw = process.stdin.isRaw || false; if (process.stdin.setRawMode) { - process.stdin.setRawMode(true); + try { + process.stdin.setRawMode(true); + } catch (err) { + this.#markTerminalDisconnected("stdin raw mode setup failed", err); + return; + } } process.stdin.setEncoding("utf8"); process.stdin.on("end", this.#stdinEndHandler); diff --git a/packages/tui/test/terminal-disconnect-raw-mode-throw.test.ts b/packages/tui/test/terminal-disconnect-raw-mode-throw.test.ts index 1f1ae01da..ef10eedf9 100644 --- a/packages/tui/test/terminal-disconnect-raw-mode-throw.test.ts +++ b/packages/tui/test/terminal-disconnect-raw-mode-throw.test.ts @@ -76,6 +76,25 @@ describe("ProcessTerminal disconnect with a revoked pty", () => { expect(signals).toContain("SIGHUP"); }); + it("treats an initial EIO enabling raw mode as a terminal disconnect", () => { + Object.defineProperty(process.stdin, "setRawMode", { + value: () => { + throw new Error("setRawMode failed with errno: 5"); + }, + configurable: true, + }); + + const terminal = new ProcessTerminal(); + expect(() => + terminal.start( + () => {}, + () => {}, + () => terminal.stop(), + ), + ).not.toThrow(); + expect(signals).toContain("SIGHUP"); + }); + it("propagates a raw-mode restore failure while the terminal is still live", () => { let started = false; Object.defineProperty(process.stdin, "setRawMode", {