Skip to content

Commit 773ffd8

Browse files
fix(tui): keep parent stdin paused after exit (#93159)
Keep the setup TUI parent stdin paused after its inherited-stdio child exits so Docker and PTY setup parents terminate cleanly. Align pre/post setup terminal cleanup with the cleanup-then-exit contract and add lifecycle regression coverage. Thanks @fuller-stack-dev.
1 parent 3add8af commit 773ffd8

4 files changed

Lines changed: 24 additions & 15 deletions

File tree

src/tui/tui-launch.test.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
// Covers TUI launch argument and environment construction.
22
import type { ChildProcess, SpawnOptions } from "node:child_process";
33
import { EventEmitter } from "node:events";
4-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4+
import { afterEach, beforeEach, describe, expect, it, type MockInstance, vi } from "vitest";
55

66
const spawnMock = vi.hoisted(() => vi.fn());
77
const detachMock = vi.hoisted(() => vi.fn());
8+
let pauseSpy: MockInstance;
9+
let resumeSpy: MockInstance;
810

911
vi.mock("node:child_process", () => ({
1012
spawn: spawnMock,
@@ -42,8 +44,8 @@ describe("launchTuiCli", () => {
4244
process.execArgv.length = 0;
4345
spawnMock.mockReset();
4446
detachMock.mockReset();
45-
vi.spyOn(process.stdin, "pause").mockImplementation(() => process.stdin);
46-
vi.spyOn(process.stdin, "resume").mockImplementation(() => process.stdin);
47+
pauseSpy = vi.spyOn(process.stdin, "pause").mockImplementation(() => process.stdin);
48+
resumeSpy = vi.spyOn(process.stdin, "resume").mockImplementation(() => process.stdin);
4749
vi.spyOn(process.stdin, "isPaused").mockReturnValue(false);
4850
});
4951

@@ -135,6 +137,19 @@ describe("launchTuiCli", () => {
135137
expect(options.stdio).toBe("inherit");
136138
});
137139

140+
it("keeps parent stdin paused after the relaunched TUI exits", async () => {
141+
const child = createChildProcess();
142+
spawnMock.mockImplementation((_cmd: string, _args: string[], _opts: SpawnOptions) => {
143+
queueMicrotask(() => child.emit("exit", 0, null));
144+
return child;
145+
});
146+
147+
await launchTuiCli({ deliver: false });
148+
149+
expect(pauseSpy).toHaveBeenCalledOnce();
150+
expect(resumeSpy).not.toHaveBeenCalled();
151+
});
152+
138153
it("launches compiled CLI shapes without repeating the current command", async () => {
139154
process.argv[1] = "setup";
140155
const child = createChildProcess();

src/tui/tui-launch.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,8 @@ export async function launchTuiCli(
9696
: {}),
9797
}
9898
: process.env;
99-
const stdinWasPaused =
100-
typeof process.stdin.isPaused === "function" ? process.stdin.isPaused() : false;
101-
102-
// Pause parent stdin while the child owns the terminal, then restore the previous state.
99+
// Pause parent stdin while the inherited-stdio child owns the terminal.
100+
// Keep it paused afterward so setup/container parents with stdin_open can exit.
103101
process.stdin.pause();
104102

105103
await new Promise<void>((resolve, reject) => {
@@ -126,9 +124,5 @@ export async function launchTuiCli(
126124
}
127125
resolve();
128126
});
129-
}).finally(() => {
130-
if (!stdinWasPaused) {
131-
process.stdin.resume();
132-
}
133127
});
134128
}

src/wizard/setup.finalize.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -541,10 +541,10 @@ describe("finalizeSetupWizard", () => {
541541
).rejects.toThrow("TUI exited with code 1");
542542

543543
expect(restoreTerminalState).toHaveBeenCalledWith("pre-setup tui", {
544-
resumeStdinIfPaused: true,
544+
resumeStdinIfPaused: false,
545545
});
546546
expect(restoreTerminalState).toHaveBeenCalledWith("post-setup tui", {
547-
resumeStdinIfPaused: true,
547+
resumeStdinIfPaused: false,
548548
});
549549
});
550550

src/wizard/setup.finalize.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,7 @@ export async function finalizeSetupWizard(
529529
});
530530

531531
if (hatchChoice === "tui") {
532-
restoreTerminalState("pre-setup tui", { resumeStdinIfPaused: true });
532+
restoreTerminalState("pre-setup tui", { resumeStdinIfPaused: false });
533533
try {
534534
await launchTuiCli({
535535
local: true,
@@ -538,7 +538,7 @@ export async function finalizeSetupWizard(
538538
timeoutMs: HATCH_TUI_TIMEOUT_MS,
539539
});
540540
} finally {
541-
restoreTerminalState("post-setup tui", { resumeStdinIfPaused: true });
541+
restoreTerminalState("post-setup tui", { resumeStdinIfPaused: false });
542542
}
543543
launchedTui = true;
544544
} else if (hatchChoice === "web") {

0 commit comments

Comments
 (0)