diff --git a/tui/src/app.test.ts b/tui/src/app.test.ts index fa6f52834..8f055ca23 100644 --- a/tui/src/app.test.ts +++ b/tui/src/app.test.ts @@ -2379,7 +2379,7 @@ describe("NanobotTui layout", () => { expect(exited).toEqual(["chat"]) }) - test("exits immediately when Ctrl+C is pressed on an idle empty composer", async () => { + test("exits after Ctrl+C input dispatch completes on an idle empty composer", async () => { setup = await createRenderer({ width: 72, height: 20, screenMode: "alternate-screen" }) let closed = false const transport = client() @@ -2393,7 +2393,9 @@ describe("NanobotTui layout", () => { setup.mockInput.pressCtrlC() - expect(closed).toBe(true) + expect(closed).toBe(false) + expect(setup.renderer.isDestroyed).toBe(false) + await waitUntil(() => closed) expect(setup.renderer.isDestroyed).toBe(true) }) diff --git a/tui/src/app.ts b/tui/src/app.ts index a2a1d32e3..dcb530a69 100644 --- a/tui/src/app.ts +++ b/tui/src/app.ts @@ -1653,7 +1653,10 @@ export class NanobotTui { this.clearComposer() this.status.content = this.readyStatus() } else { - this.quit() + // Finish dispatching the raw ETX before destroy() restores terminal input. + // Releasing the terminal from inside this callback can leak the same Ctrl+C + // to the parent shell on Windows terminals. + setTimeout(() => this.quit(), 0) } return }