diff --git a/packages/opencode/src/cli/cmd/tui.ts b/packages/opencode/src/cli/cmd/tui.ts index d5fa46bfb115..42e0b9327e58 100644 --- a/packages/opencode/src/cli/cmd/tui.ts +++ b/packages/opencode/src/cli/cmd/tui.ts @@ -14,6 +14,7 @@ import { writeHeapSnapshot } from "v8" import { ServerAuth } from "@/server/auth" import { validateSession } from "../tui/validate-session" import { win32InstallCtrlCGuard } from "@opencode-ai/tui/terminal-win32" +import { safeExit } from "@/util/exit" declare global { const OPENCODE_WORKER_PATH: string @@ -303,7 +304,7 @@ export const TuiThreadCommand = cmd({ unguard?.() } catch {} } - process.exit() + safeExit() }, }) // scratch diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 13540a73a36f..2ca59cc7646a 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -27,6 +27,7 @@ import { PrCommand } from "./cli/cmd/pr" import { SessionCommand } from "./cli/cmd/session" import { DbCommand } from "./cli/cmd/db" import { errorMessage } from "./util/error" +import { safeExit } from "./util/exit" import { PluginCommand } from "./cli/cmd/plug" import { Heap } from "./cli/heap" @@ -137,6 +138,6 @@ try { // Some subprocesses don't react properly to SIGTERM and similar signals. // Most notably, some docker-container-based MCP servers don't handle such signals unless // run using `docker run --init`. - // Explicitly exit to avoid any hanging subprocesses. - process.exit() + // Explicitly exit (on non-Windows) to avoid any hanging subprocesses. + safeExit() } diff --git a/packages/opencode/src/util/exit.ts b/packages/opencode/src/util/exit.ts new file mode 100644 index 000000000000..88bdd208afd8 --- /dev/null +++ b/packages/opencode/src/util/exit.ts @@ -0,0 +1,14 @@ +export function safeExit(code: number = Number(process.exitCode ?? 0)): void { + if (process.platform !== "win32") { + process.exit(code) + return + } + + // On Windows process.exit() calls ExitProcess(), which broadcasts + // CTRL_CLOSE_EVENT to the whole console process group. The parent shell + // (pwsh/cmd) is attached to the same console, so it is killed together with + // opencode when it exits from a TUI session (https://github.com/anomalyco/opencode/issues/28673). + // Setting the exit code and returning lets the process end naturally, which + // only detaches opencode from the console and leaves the shell alive. + process.exitCode = code +} \ No newline at end of file