From 76adc2632115a04bd344133dce90107ab48bf365 Mon Sep 17 00:00:00 2001 From: Aikiooo <78739437+Aikiooo@users.noreply.github.com> Date: Thu, 10 Sep 2026 17:32:12 +0200 Subject: [PATCH] fix(win): hide the console window for background child processes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On Windows, Node allocates a console window for a child process unless `windowsHide` is set. The window flashes on screen and can take focus away from whatever the user is doing — for the agent processes, which live for the whole session, on every start. `process-scanner.ts` already passed the option; the other background call sites did not. Set it on the ones whose output is captured, so the flag is consistent across the CLI. Left alone on purpose: - `daemon.ts` and `update-and-start.ts` when spawning with `stdio: "inherit"` — `pm2 startup` prints the `sudo` command the user has to read and re-run, so those windows must stay visible. - POSIX-only branches (`lsof`, `/bin/sh`, a login shell for `command -v`) and macOS `caffeinate`, where the option does nothing. - `terminal/index.ts` uses node-pty, which has no such option. `windowsHide` maps to CREATE_NO_WINDOW and is ignored off Windows. --- cli/src/agents/base.ts | 3 +++ cli/src/agents/codex-readonly.ts | 1 + cli/src/filesystem/index.ts | 1 + cli/src/notify.ts | 23 ++++++++++++++--------- cli/src/ports.ts | 10 ++++++++-- cli/src/update-and-start.ts | 1 + cli/src/update-runner.ts | 1 + cli/src/utils.ts | 1 + 8 files changed, 30 insertions(+), 11 deletions(-) diff --git a/cli/src/agents/base.ts b/cli/src/agents/base.ts index d6411d4..f672580 100644 --- a/cli/src/agents/base.ts +++ b/cli/src/agents/base.ts @@ -293,6 +293,9 @@ export class ACP { env: { ...process.env, ...(config.env ?? {}) }, stdio: ["pipe", "pipe", "pipe"], shell: useShell, + // A shell-backed spawn allocates its own console window on Windows, + // which flashes on screen and can steal focus. No-op elsewhere. + windowsHide: true, }, ); diff --git a/cli/src/agents/codex-readonly.ts b/cli/src/agents/codex-readonly.ts index 150b6b5..dd7fa37 100644 --- a/cli/src/agents/codex-readonly.ts +++ b/cli/src/agents/codex-readonly.ts @@ -64,6 +64,7 @@ export async function readCodexThread( const child = spawn(command, ["app-server", "--stdio"], { cwd, stdio: ["pipe", "pipe", "pipe"], + windowsHide: true, }); let stderr = ""; diff --git a/cli/src/filesystem/index.ts b/cli/src/filesystem/index.ts index 5b5f8df..ccf7327 100644 --- a/cli/src/filesystem/index.ts +++ b/cli/src/filesystem/index.ts @@ -711,6 +711,7 @@ export function initFilesystemHandler(conn: Connection, rootDir: string) { cwd: gitRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], // silence stderr + windowsHide: true, }); } catch { // File might not exist in HEAD (new file), return empty diff --git a/cli/src/notify.ts b/cli/src/notify.ts index f31b820..b6903a7 100644 --- a/cli/src/notify.ts +++ b/cli/src/notify.ts @@ -17,16 +17,21 @@ function escapePowerShell(s: string) { } function run(cmd: string, args: string[]) { - execFile(cmd, args, { timeout: 5000 }, (error, _stdout, stderr) => { - if (error) { - logger.debug(`${cmd} failed`, error); - return; - } + execFile( + cmd, + args, + { timeout: 5000, windowsHide: true }, + (error, _stdout, stderr) => { + if (error) { + logger.debug(`${cmd} failed`, error); + return; + } - if (stderr?.trim()) { - logger.debug(`${cmd} stderr: ${stderr.trim()}`); - } - }); + if (stderr?.trim()) { + logger.debug(`${cmd} stderr: ${stderr.trim()}`); + } + }, + ); } export function notify({ title, body }: NotifyArgs): boolean { diff --git a/cli/src/ports.ts b/cli/src/ports.ts index fccb8c5..a61d87d 100644 --- a/cli/src/ports.ts +++ b/cli/src/ports.ts @@ -111,6 +111,7 @@ export function initPortsHandler(conn: Connection) { output = execSync("netstat -ano | findstr LISTENING", { encoding: "utf-8", timeout: 5000, + windowsHide: true, }); const lines = output.trim().split("\n"); @@ -211,7 +212,10 @@ export function initPortsHandler(conn: Connection) { } } } else if (platform === "win32") { - const result = spawnSync("netstat", ["-ano"], { encoding: "utf-8" }); + const result = spawnSync("netstat", ["-ano"], { + encoding: "utf-8", + windowsHide: true, + }); const lines = (result.stdout || "").trim().split("\n"); for (const line of lines) { if (!line.includes("LISTENING")) continue; @@ -221,7 +225,9 @@ export function initPortsHandler(conn: Connection) { const p = parseInt(parts[4], 10); if (localAddr.endsWith(`:${portNum}`)) { if (pid === null) pid = p; - spawnSync("taskkill", ["/F", "/PID", String(p)]); + spawnSync("taskkill", ["/F", "/PID", String(p)], { + windowsHide: true, + }); } } } diff --git a/cli/src/update-and-start.ts b/cli/src/update-and-start.ts index 8ce2898..e93dc14 100644 --- a/cli/src/update-and-start.ts +++ b/cli/src/update-and-start.ts @@ -34,6 +34,7 @@ export function isShellularInstalledGlobally(): Promise { const child = spawn("npm", ["list", "-g", "shellular", "--depth=0"], { stdio: "ignore", shell: process.platform === "win32", + windowsHide: true, }); child.on("close", (code) => { diff --git a/cli/src/update-runner.ts b/cli/src/update-runner.ts index 71d711a..0bd0801 100644 --- a/cli/src/update-runner.ts +++ b/cli/src/update-runner.ts @@ -54,6 +54,7 @@ export async function runSelfUpdate(): Promise { stdio: ["ignore", logFd, logFd], cwd: config.SHELLULAR_DIR, env: process.env, + windowsHide: true, }); child.unref(); } else { diff --git a/cli/src/utils.ts b/cli/src/utils.ts index 0225d57..0c93fae 100644 --- a/cli/src/utils.ts +++ b/cli/src/utils.ts @@ -91,6 +91,7 @@ export function commandExists(command: string): boolean { const result = spawnSync("where", [command], { stdio: "ignore", shell: false, + windowsHide: true, }); return result.status === 0; }