diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a644e1ec54..0d4aa88197 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -317,6 +317,8 @@ ### Fixed +- Windows spawns no longer flash a console: every `spawn`/`spawnSync`/`execFile` site now passes `windowsHide:true`, with `utils/child-process.ts` forcing it on win32 for all current and future `spawnProcess` callers ([#927](https://github.com/code-yeongyu/senpi/pull/927)). + - Goals no longer stall after a settings hot-reload: a reload `session_start` now re-engages an active goal (re-arming the monitor backstop while wake sources are live, or queueing a continuation through the existing sessionStart admission) instead of parking it until the next user message; stopped goals still never auto-start on reload ([#936](https://github.com/code-yeongyu/senpi/pull/936)). - Cursor CLI OAuth is now available by default when its real prerequisites exist: with `cursor-agent` installed and no managed CLI account, a native `cursor` OAuth credential is copied automatically into one canonical `native` slot without modifying the primary credential; explicit `enabled: false` remains a hard opt-out, repeated/concurrent startup is idempotent, and `/login cursor` refreshes the CLI fallback in the same session ([#931](https://github.com/code-yeongyu/senpi/pull/931)). @@ -7614,4 +7616,4 @@ Initial public release. - Git branch display in footer - Message queueing during streaming responses - OAuth integration for Gmail and Google Calendar access -- HTML export with syntax highlighting and collapsible sections +- HTML export with syntax highlighting and collapsible sections \ No newline at end of file diff --git a/packages/coding-agent/src/beta/omo-local-update-worker.ts b/packages/coding-agent/src/beta/omo-local-update-worker.ts index 04e5153281..cc3a968601 100644 --- a/packages/coding-agent/src/beta/omo-local-update-worker.ts +++ b/packages/coding-agent/src/beta/omo-local-update-worker.ts @@ -48,6 +48,7 @@ export const defaultSpawnWorker: OmoLocalSpawnWorker = (request) => { try { const child = spawn(process.execPath, workerCommandArgs(request.force), { detached: true, + windowsHide: true, env: process.env, stdio: ["ignore", logFd, logFd], }); diff --git a/packages/coding-agent/src/changes.md b/packages/coding-agent/src/changes.md index daf50db3ad..a5006cd1c1 100644 --- a/packages/coding-agent/src/changes.md +++ b/packages/coding-agent/src/changes.md @@ -240,6 +240,29 @@ - `main.ts` model/thinking option resolution block. +## Windows console hide for top-level entry and config helpers (2026-08-18) + +### What changed + +- `packages/coding-agent/src/cli.ts`: main entry re-spawn now passes `windowsHide:true`. +- `packages/coding-agent/src/config.ts`: `readCommandOutput` via `spawnProcessSync` now passes `windowsHide:true`. +- `packages/coding-agent/src/package-manager-cli.ts`: self-update step via `spawnProcess` now passes `windowsHide:true`. +- `packages/coding-agent/src/self-update-bootstrap.ts`: npm update step via `spawn` now passes `windowsHide:true`. +- `packages/coding-agent/src/beta/omo-local-update-worker.ts`: detached worker spawn now passes `windowsHide:true` alongside `detached:true`. + +### Why + +- Windows console less parents briefly show a conhost window for every spawn without `windowsHide:true`. Detached/background helpers run periodically, so flashes recur until all sites are hidden. + +### Why an extension could not handle it + +- Spawn options live inside the caller (helper, daemon, runner). Extensions cannot inject `windowsHide` from outside; the spawn site itself must set it. + +### Expected merge conflict zones + +- LOW: the spawn options literal in the patched file(s). + + ## Repository audit baseline for the src tracker (2026-08-17) ### What changed diff --git a/packages/coding-agent/src/cli.ts b/packages/coding-agent/src/cli.ts index 30d09fe53b..c88dac8b21 100644 --- a/packages/coding-agent/src/cli.ts +++ b/packages/coding-agent/src/cli.ts @@ -64,6 +64,7 @@ async function spawnFullCli(): Promise { const child = spawn(process.execPath, [...process.execArgv, fullCliPath, ...args], { env: process.env, stdio: "inherit", + windowsHide: true, }); child.on("error", (error) => { reject(error); diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index aca5709435..df404a8fa2 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -209,6 +209,7 @@ function readCommandOutput( const result = spawnProcessSync(command, args, { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, }); if (result.status === 0) return result.stdout.trim() || undefined; if (options.requireSuccess) { diff --git a/packages/coding-agent/src/core/changes.md b/packages/coding-agent/src/core/changes.md index dedf4e5936..de366cb59b 100644 --- a/packages/coding-agent/src/core/changes.md +++ b/packages/coding-agent/src/core/changes.md @@ -551,6 +551,27 @@ Conflict zone: `cursor-exec-bridge.ts` `executeTool`, `cursor-exec-bridge-sessio - `model-resolver.ts` pattern matching and partial-match ordering, `agent-session.ts` thinking-level setters, `session-manager.ts` entry schema. +## Windows console hide for core exec and package helpers (2026-08-18) + +### What changed + +- `packages/coding-agent/src/core/exec.ts`: `execCommand` via `spawn` now passes `windowsHide:true`. +- `packages/coding-agent/src/core/footer-data-provider.ts`: git `spawnSync`/`execFile` probes now pass `windowsHide:true`. +- `packages/coding-agent/src/core/package-manager.ts`: `spawnCommand`/`spawnCaptureCommand`/`runCommandSync` now pass `windowsHide:true` (central wrapper also forces it). + +### Why + +- Windows console less parents briefly show a conhost window for every spawn without `windowsHide:true`. Detached/background helpers run periodically, so flashes recur until all sites are hidden. + +### Why an extension could not handle it + +- Spawn options live inside the caller (helper, daemon, runner). Extensions cannot inject `windowsHide` from outside; the spawn site itself must set it. + +### Expected merge conflict zones + +- LOW: the spawn options literal in the patched file(s). + + ## Cursor bridge lifecycle events retain run ownership (2026-08-18) ### What changed diff --git a/packages/coding-agent/src/core/exec.ts b/packages/coding-agent/src/core/exec.ts index 5afe9ec367..4e7bdc2021 100644 --- a/packages/coding-agent/src/core/exec.ts +++ b/packages/coding-agent/src/core/exec.ts @@ -42,6 +42,7 @@ export async function execCommand( cwd, shell: false, stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, }); let stdout = ""; diff --git a/packages/coding-agent/src/core/extensions/builtin/cursor-cli-oauth/changes.md b/packages/coding-agent/src/core/extensions/builtin/cursor-cli-oauth/changes.md index e9513b55f3..4efe80a606 100644 --- a/packages/coding-agent/src/core/extensions/builtin/cursor-cli-oauth/changes.md +++ b/packages/coding-agent/src/core/extensions/builtin/cursor-cli-oauth/changes.md @@ -1,3 +1,23 @@ + +## Windows console hide for Cursor CLI probes (2026-08-18) + +### What changed + +- `packages/coding-agent/src/core/extensions/builtin/cursor-cli-oauth/executable.ts`: `VersionProbeOptions` now allows `windowsHide` and probe `execFile` passes `windowsHide:true`. +- `packages/coding-agent/src/core/extensions/builtin/cursor-cli-oauth/models.ts`: model probe `spawn` now passes `windowsHide:true`. + +### Why + +- Windows console less parents briefly show a conhost window for every spawn without `windowsHide:true`. Detached/background helpers run periodically, so flashes recur until all sites are hidden. + +### Why an extension could not handle it + +- Spawn options live inside the caller (helper, daemon, runner). Extensions cannot inject `windowsHide` from outside; the spawn site itself must set it. + +### Expected merge conflict zones + +- LOW: the spawn options literal in the patched file(s). + # cursor-cli-oauth extension changes ## 2026-08-21 - Cache provider settings loads by mtime+size to cut lock convoy diff --git a/packages/coding-agent/src/core/extensions/builtin/cursor-cli-oauth/executable.ts b/packages/coding-agent/src/core/extensions/builtin/cursor-cli-oauth/executable.ts index f432d32725..f703899411 100644 --- a/packages/coding-agent/src/core/extensions/builtin/cursor-cli-oauth/executable.ts +++ b/packages/coding-agent/src/core/extensions/builtin/cursor-cli-oauth/executable.ts @@ -20,6 +20,7 @@ export type CursorAgentExecutableDeps = { export type VersionProbeOptions = { encoding: "utf8"; timeout: number; + windowsHide?: boolean; }; export type VersionProbeCallback = (error: Error | null, stdout: string, stderr: string) => void; @@ -116,7 +117,7 @@ export function probeCursorAgentVersion( deps: VersionProbeDeps = defaultVersionProbeDeps, ): Promise { return new Promise((resolve, reject) => { - deps.execFile(executable, ["--version"], { encoding: "utf8", timeout: 10_000 }, (error, stdout) => { + deps.execFile(executable, ["--version"], { encoding: "utf8", timeout: 10_000, windowsHide: true }, (error, stdout) => { if (error) { reject(error); return; diff --git a/packages/coding-agent/src/core/extensions/builtin/cursor-cli-oauth/models.ts b/packages/coding-agent/src/core/extensions/builtin/cursor-cli-oauth/models.ts index 87c6857e26..ed3598ebc1 100644 --- a/packages/coding-agent/src/core/extensions/builtin/cursor-cli-oauth/models.ts +++ b/packages/coding-agent/src/core/extensions/builtin/cursor-cli-oauth/models.ts @@ -125,6 +125,7 @@ async function runModelsProbe(executable: string, stdoutPath: string, timeoutMs: await new Promise((resolve, reject) => { const child = spawn(executable, ["models"], { stdio: ["ignore", output.fd, "ignore"], + windowsHide: true, }); let timedOut = false; let settled = false; diff --git a/packages/coding-agent/src/core/extensions/builtin/mcp/changes.md b/packages/coding-agent/src/core/extensions/builtin/mcp/changes.md index 0945fbd5bd..1846afc11c 100644 --- a/packages/coding-agent/src/core/extensions/builtin/mcp/changes.md +++ b/packages/coding-agent/src/core/extensions/builtin/mcp/changes.md @@ -1,3 +1,22 @@ + +## Windows console hide for MCP diagnostics (2026-08-18) + +### What changed + +- `packages/coding-agent/src/core/extensions/builtin/mcp/diagnose.ts`: `execFile` diagnostics now pass `windowsHide:true`. + +### Why + +- Windows console less parents briefly show a conhost window for every spawn without `windowsHide:true`. Detached/background helpers run periodically, so flashes recur until all sites are hidden. + +### Why an extension could not handle it + +- Spawn options live inside the caller (helper, daemon, runner). Extensions cannot inject `windowsHide` from outside; the spawn site itself must set it. + +### Expected merge conflict zones + +- LOW: the spawn options literal in the patched file(s). + # mcp Extension Changes ## Explicit pgrep match-all pattern for process-tree collection (2026-08-12) diff --git a/packages/coding-agent/src/core/extensions/builtin/mcp/diagnose.ts b/packages/coding-agent/src/core/extensions/builtin/mcp/diagnose.ts index 688c97a461..d27efd53d1 100644 --- a/packages/coding-agent/src/core/extensions/builtin/mcp/diagnose.ts +++ b/packages/coding-agent/src/core/extensions/builtin/mcp/diagnose.ts @@ -121,6 +121,7 @@ function execFileForDiagnostics( cwd: options.cwd, env: options.env, killSignal: "SIGKILL", + windowsHide: true, maxBuffer: MCP_STDIO_DIAGNOSTIC_MAX_BYTES * 4, timeout: MCP_STDIO_DIAGNOSTIC_TIMEOUT_MS, }, diff --git a/packages/coding-agent/src/core/footer-data-provider.ts b/packages/coding-agent/src/core/footer-data-provider.ts index e56a3eb6bc..1434fc35d2 100644 --- a/packages/coding-agent/src/core/footer-data-provider.ts +++ b/packages/coding-agent/src/core/footer-data-provider.ts @@ -53,6 +53,7 @@ function resolveBranchWithGitSync(repoDir: string): string | null { cwd: repoDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], + windowsHide: true, }); const branch = result.status === 0 ? result.stdout.trim() : ""; return branch || null; @@ -67,6 +68,7 @@ function resolveBranchWithGitAsync(repoDir: string): Promise { { cwd: repoDir, encoding: "utf8", + windowsHide: true, }, (error: ExecFileException | null, stdout: string) => { if (error) { diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index 495dc3152c..84a663eb07 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -2668,6 +2668,7 @@ export class DefaultPackageManager implements PackageManager { cwd: options?.cwd, stdio: isStdoutTakenOver() ? ["ignore", 2, 2] : "inherit", env, + windowsHide: true, }); } @@ -2682,6 +2683,7 @@ export class DefaultPackageManager implements PackageManager { cwd: options?.cwd, stdio: ["ignore", "pipe", "pipe"], env, + windowsHide: true, }); } @@ -2749,6 +2751,7 @@ export class DefaultPackageManager implements PackageManager { stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8", env, + windowsHide: true, }); if (result.error || result.status !== 0) { throw new Error( diff --git a/packages/coding-agent/src/core/tools/changes.md b/packages/coding-agent/src/core/tools/changes.md index 4d1e91fa56..3f15cfdc32 100644 --- a/packages/coding-agent/src/core/tools/changes.md +++ b/packages/coding-agent/src/core/tools/changes.md @@ -1,3 +1,23 @@ + +## Windows console hide for fd/rg helpers (2026-08-18) + +### What changed + +- `packages/coding-agent/src/core/tools/find.ts`: fd helper `spawn` now passes `windowsHide:true`. +- `packages/coding-agent/src/core/tools/grep.ts`: rg helper `spawn` now passes `windowsHide:true`. + +### Why + +- Windows console less parents briefly show a conhost window for every spawn without `windowsHide:true`. Detached/background helpers run periodically, so flashes recur until all sites are hidden. + +### Why an extension could not handle it + +- Spawn options live inside the caller (helper, daemon, runner). Extensions cannot inject `windowsHide` from outside; the spawn site itself must set it. + +### Expected merge conflict zones + +- LOW: the spawn options literal in the patched file(s). + # core/tools changes ## Edit tool keeps filesystem policy and themed diff rendering after the 59a71b23 pin (2026-08-19) diff --git a/packages/coding-agent/src/core/tools/find.ts b/packages/coding-agent/src/core/tools/find.ts index ce373dc100..ec97f3f70c 100644 --- a/packages/coding-agent/src/core/tools/find.ts +++ b/packages/coding-agent/src/core/tools/find.ts @@ -282,7 +282,7 @@ export function createFindToolDefinition( } args.push("--", effectivePattern, searchPath); - const child = spawn(fdPath, args, { stdio: ["ignore", "pipe", "pipe"] }); + const child = spawn(fdPath, args, { stdio: ["ignore", "pipe", "pipe"], windowsHide: true }); const rl = createInterface({ input: child.stdout }); let stderr = ""; const lines: string[] = []; diff --git a/packages/coding-agent/src/core/tools/grep.ts b/packages/coding-agent/src/core/tools/grep.ts index 91a44ed1df..47e055b21c 100644 --- a/packages/coding-agent/src/core/tools/grep.ts +++ b/packages/coding-agent/src/core/tools/grep.ts @@ -240,7 +240,7 @@ export function createGrepToolDefinition( if (glob) args.push("--glob", glob); args.push("--", pattern, searchPath); - const child = spawn(rgPath, args, { stdio: ["ignore", "pipe", "pipe"] }); + const child = spawn(rgPath, args, { stdio: ["ignore", "pipe", "pipe"], windowsHide: true }); const rl = createInterface({ input: child.stdout }); let stderr = ""; let matchCount = 0; diff --git a/packages/coding-agent/src/modes/app-server/changes.md b/packages/coding-agent/src/modes/app-server/changes.md index c746447808..cfd9174b4e 100644 --- a/packages/coding-agent/src/modes/app-server/changes.md +++ b/packages/coding-agent/src/modes/app-server/changes.md @@ -1,5 +1,24 @@ # changes +## Windows console hide for app-server daemon (2026-08-18) + +### What changed + +- `packages/coding-agent/src/modes/app-server/daemon.ts`: daemon `spawn` with `detached:true` now also passes `windowsHide:true`. + +### Why + +- Windows console less parents briefly show a conhost window for every spawn without `windowsHide:true`. Detached/background helpers run periodically, so flashes recur until all sites are hidden. + +### Why an extension could not handle it + +- Spawn options live inside the caller (helper, daemon, runner). Extensions cannot inject `windowsHide` from outside; the spawn site itself must set it. + +### Expected merge conflict zones + +- LOW: the daemon spawn options literal. + + ## Registry-owned thread teardown (2026-08-13) ### What changed diff --git a/packages/coding-agent/src/modes/app-server/daemon.ts b/packages/coding-agent/src/modes/app-server/daemon.ts index 0dc861cdb2..e1bda0a1f1 100644 --- a/packages/coding-agent/src/modes/app-server/daemon.ts +++ b/packages/coding-agent/src/modes/app-server/daemon.ts @@ -176,6 +176,7 @@ async function spawnDaemon(paths: DaemonPaths, listen: AppServerListen): Promise [...process.execArgv, resolveCliMainPath(), "app-server", "--listen", listen.url], { detached: true, + windowsHide: true, env: process.env, stdio: ["ignore", "ignore", stderr.fd], }, diff --git a/packages/coding-agent/src/modes/interactive/changes.md b/packages/coding-agent/src/modes/interactive/changes.md index 27fddd9275..9f303b90b0 100644 --- a/packages/coding-agent/src/modes/interactive/changes.md +++ b/packages/coding-agent/src/modes/interactive/changes.md @@ -276,6 +276,27 @@ Conflict zone: `interactive-mode.ts` `message_update` / `message_end`. `interactive-mode.ts` (adjacent to the paste handler wiring), and `queueCompactionSubmission()` next to `queueCompactionMessage()`. +## Windows console hide for interactive helpers (2026-08-18) + +### What changed + +- `packages/coding-agent/src/modes/interactive/external-editor.ts`: both editor `spawn` calls now pass `windowsHide:true`. +- `packages/coding-agent/src/modes/interactive/interactive-mode.ts`: tmux `spawn` and `gh` `spawnSync`/`spawn` helpers now pass `windowsHide:true`. +- `packages/coding-agent/src/modes/interactive/components/session-selector.ts`: `trash` `spawnSync` now passes `windowsHide:true`. + +### Why + +- Windows console less parents briefly show a conhost window for every spawn without `windowsHide:true`. Detached/background helpers run periodically, so flashes recur until all sites are hidden. + +### Why an extension could not handle it + +- Spawn options live inside the caller (helper, daemon, runner). Extensions cannot inject `windowsHide` from outside; the spawn site itself must set it. + +### Expected merge conflict zones + +- LOW: the spawn options literal in the patched file(s). + + ## Native Cursor login refreshes the CLI fallback lane in the same session (2026-08-18) ### What changed diff --git a/packages/coding-agent/src/modes/interactive/components/session-selector.ts b/packages/coding-agent/src/modes/interactive/components/session-selector.ts index 4949eee512..5a583585ea 100644 --- a/packages/coding-agent/src/modes/interactive/components/session-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/session-selector.ts @@ -647,7 +647,7 @@ async function deleteSessionFile( ): Promise<{ ok: boolean; method: "trash" | "unlink"; error?: string }> { // Try `trash` first (if installed) const trashArgs = sessionPath.startsWith("-") ? ["--", sessionPath] : [sessionPath]; - const trashResult = spawnSync("trash", trashArgs, { encoding: "utf-8" }); + const trashResult = spawnSync("trash", trashArgs, { encoding: "utf-8", windowsHide: true }); const getTrashErrorHint = (): string | null => { const parts: string[] = []; diff --git a/packages/coding-agent/src/modes/interactive/external-editor.ts b/packages/coding-agent/src/modes/interactive/external-editor.ts index 76edd6ee78..b9b6746985 100644 --- a/packages/coding-agent/src/modes/interactive/external-editor.ts +++ b/packages/coding-agent/src/modes/interactive/external-editor.ts @@ -31,6 +31,7 @@ export async function editInExternalEditor(options: ExternalEditorOptions): Prom const child = spawn(editor, [...editorArgs, filePath], { stdio: "inherit", shell: process.platform === "win32", + windowsHide: true, }); child.on("error", () => resolve({ launched: false })); child.on("close", (code) => resolve({ launched: true, code })); @@ -64,6 +65,7 @@ export async function editFileInExternalEditor(options: { command: string; path: const child = spawn(editor, [...editorArgs, options.path], { stdio: "inherit", shell: process.platform === "win32", + windowsHide: true, }); child.on("error", () => resolve({ launched: false })); child.on("close", (code) => resolve({ launched: true, code })); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 6141da00c1..d08435de4b 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -1660,6 +1660,7 @@ export class InteractiveMode { return new Promise((resolve) => { const proc = spawn("tmux", args, { stdio: ["ignore", "pipe", "ignore"], + windowsHide: true, }); let stdout = ""; const timer = setTimeout(() => { @@ -7895,9 +7896,7 @@ export class InteractiveMode { private async handleShareCommand(): Promise { // Check if gh is available and logged in try { - const authResult = spawnSync("gh", ["auth", "status"], { - encoding: "utf-8", - }); + const authResult = spawnSync("gh", ["auth", "status"], { encoding: "utf-8", windowsHide: true }); if (authResult.status !== 0) { this.showError("GitHub CLI is not logged in. Run 'gh auth login' first."); return; @@ -7945,12 +7944,8 @@ export class InteractiveMode { }; try { - const result = await new Promise<{ - stdout: string; - stderr: string; - code: number | null; - }>((resolve) => { - proc = spawn("gh", ["gist", "create", "--public=false", tmpFile]); + const result = await new Promise<{ stdout: string; stderr: string; code: number | null }>((resolve) => { + proc = spawn("gh", ["gist", "create", "--public=false", tmpFile], { windowsHide: true }); let stdout = ""; let stderr = ""; proc.stdout?.on("data", (data) => { diff --git a/packages/coding-agent/src/modes/rpc/changes.md b/packages/coding-agent/src/modes/rpc/changes.md index 9bd5ea9210..be72e97027 100644 --- a/packages/coding-agent/src/modes/rpc/changes.md +++ b/packages/coding-agent/src/modes/rpc/changes.md @@ -1,5 +1,24 @@ # changes +## Windows console hide for RPC child (2026-08-18) + +### What changed + +- `packages/coding-agent/src/modes/rpc/rpc-client.ts`: RPC child `spawn` now passes `windowsHide:true`. + +### Why + +- Windows console less parents briefly show a conhost window for every spawn without `windowsHide:true`. Detached/background helpers run periodically, so flashes recur until all sites are hidden. + +### Why an extension could not handle it + +- Spawn options live inside the caller (helper, daemon, runner). Extensions cannot inject `windowsHide` from outside; the spawn site itself must set it. + +### Expected merge conflict zones + +- LOW: the spawn options literal in the patched file(s). + + ## Suppress initial command-surface invalidation events (2026-08-17) ### What changed diff --git a/packages/coding-agent/src/modes/rpc/rpc-client.ts b/packages/coding-agent/src/modes/rpc/rpc-client.ts index 10d0218393..28797e2ab7 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-client.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-client.ts @@ -112,6 +112,7 @@ export class RpcClient { cwd: this.options.cwd, env: { ...process.env, ...this.options.env }, stdio: ["pipe", "pipe", "pipe"], + windowsHide: true, }); this.process = childProcess; diff --git a/packages/coding-agent/src/package-manager-cli.ts b/packages/coding-agent/src/package-manager-cli.ts index da0078a4b3..897bb11b6c 100644 --- a/packages/coding-agent/src/package-manager-cli.ts +++ b/packages/coding-agent/src/package-manager-cli.ts @@ -537,6 +537,7 @@ async function runSelfUpdate(command: SelfUpdateCommand): Promise { await new Promise((resolve, reject) => { const child = spawnProcess(step.command, step.args, { stdio: "inherit", + windowsHide: true, }); child.on("error", (error) => { reject(error); diff --git a/packages/coding-agent/src/self-update-bootstrap.ts b/packages/coding-agent/src/self-update-bootstrap.ts index 0da4229718..b66fb83b95 100644 --- a/packages/coding-agent/src/self-update-bootstrap.ts +++ b/packages/coding-agent/src/self-update-bootstrap.ts @@ -118,7 +118,7 @@ function readConfiguredNpmCommand(): string[] | undefined { async function runCommand(step: SelfUpdateBootstrapCommand): Promise { await new Promise((resolve, reject) => { - const child = spawn(step.command, step.args, { stdio: "inherit" }); + const child = spawn(step.command, step.args, { stdio: "inherit", windowsHide: true }); child.on("error", (error) => { reject(error); }); diff --git a/packages/coding-agent/src/utils/changes.md b/packages/coding-agent/src/utils/changes.md index 9c1a8439dc..e679c55b80 100644 --- a/packages/coding-agent/src/utils/changes.md +++ b/packages/coding-agent/src/utils/changes.md @@ -1,5 +1,25 @@ # changes +## Windows console hide central wrapper and utility spawns (2026-08-18) + +### What changed + +- `packages/coding-agent/src/utils/child-process.ts`: `spawnProcess`/`spawnProcessSync` now force `windowsHide:true` on win32 unless caller opts out (`windowsHide:false`), with `cross-spawn` propagation. +- `packages/coding-agent/src/utils/clipboard-image.ts`, `clipboard.ts`, `paths.ts`, `shell.ts`, `tools-manager.ts`, `open-browser.ts`: auxiliary `spawnSync`/`spawnProcessSync`/`spawn` calls (`xattr`/`setfattr`/`which`/`--version`/`wl-copy`/`rundll32`/`xdg-open`) now pass `windowsHide:true`. + +### Why + +- Same as above; central wrapper is the durable fix so future call sites are hidden by default, direct sites remain explicitly marked. + +### Why an extension could not handle it + +- Central wrapper is the single enforcement point; utilities cannot be fixed externally without touching the wrapper. + +### Expected merge conflict zones + +- LOW: the wrapper `effective` merge or the utility spawn literals. + + ## Repository audit baseline for the utils tracker (2026-08-17) ### What changed diff --git a/packages/coding-agent/src/utils/child-process.ts b/packages/coding-agent/src/utils/child-process.ts index f946fc1c20..065f45240c 100644 --- a/packages/coding-agent/src/utils/child-process.ts +++ b/packages/coding-agent/src/utils/child-process.ts @@ -35,7 +35,15 @@ export function spawnProcess( ): ChildProcessByStdio; export function spawnProcess(command: string, args: string[], options: SpawnOptions): ChildProcess; export function spawnProcess(command: string, args: string[], options: SpawnOptions): ChildProcess { - return process.platform === "win32" ? crossSpawn(command, args, options) : nodeSpawn(command, args, options); + const windowsHideOptions = + options && typeof (options as { windowsHide?: boolean }).windowsHide !== "undefined" + ? options + : { ...options, windowsHide: true }; + const effective = + process.platform === "win32" && (windowsHideOptions as { windowsHide?: boolean }).windowsHide !== false + ? { ...windowsHideOptions, windowsHide: true } + : options; + return process.platform === "win32" ? crossSpawn(command, args, effective) : nodeSpawn(command, args, effective); } export function spawnProcessSync( @@ -43,9 +51,18 @@ export function spawnProcessSync( args: string[], options: SpawnSyncOptionsWithStringEncoding, ): SpawnSyncReturns { + const windowsHideOptions = + options && typeof (options as { windowsHide?: boolean }).windowsHide !== "undefined" + ? (options as unknown as Record) + : { ...(options as unknown as Record), windowsHide: true }; + const effective = + process.platform === "win32" && + (windowsHideOptions as { windowsHide?: boolean }).windowsHide !== false + ? { ...(windowsHideOptions as Record), windowsHide: true } + : options; return process.platform === "win32" - ? crossSpawn.sync(command, args, options) - : nodeSpawnSync(command, args, options); + ? crossSpawn.sync(command, args, effective as SpawnSyncOptionsWithStringEncoding) + : nodeSpawnSync(command, args, effective as SpawnSyncOptionsWithStringEncoding); } /** diff --git a/packages/coding-agent/src/utils/clipboard-image.ts b/packages/coding-agent/src/utils/clipboard-image.ts index ea46f89d5a..687eee522e 100644 --- a/packages/coding-agent/src/utils/clipboard-image.ts +++ b/packages/coding-agent/src/utils/clipboard-image.ts @@ -98,6 +98,7 @@ function runCommand( timeout: timeoutMs, maxBuffer: maxBufferBytes, env: options?.env, + windowsHide: true, }); if (result.error) { diff --git a/packages/coding-agent/src/utils/clipboard.ts b/packages/coding-agent/src/utils/clipboard.ts index c53a382bc9..e4868fbc01 100644 --- a/packages/coding-agent/src/utils/clipboard.ts +++ b/packages/coding-agent/src/utils/clipboard.ts @@ -132,7 +132,7 @@ export async function copyToClipboard(text: string): Promise { // Await the exit code and only claim success on a clean exit, so a // failed wl-copy falls through to the xclip/OSC 52 fallbacks. const wlCopyExit = await new Promise((resolve) => { - const proc = spawn("wl-copy", [], { stdio: ["pipe", "ignore", "ignore"] }); + const proc = spawn("wl-copy", [], { stdio: ["pipe", "ignore", "ignore"], windowsHide: true }); proc.on("error", () => resolve(1)); proc.on("close", (code) => resolve(code ?? 1)); proc.stdin.on("error", () => { diff --git a/packages/coding-agent/src/utils/open-browser.ts b/packages/coding-agent/src/utils/open-browser.ts index 435e23f977..4a255e472a 100644 --- a/packages/coding-agent/src/utils/open-browser.ts +++ b/packages/coding-agent/src/utils/open-browser.ts @@ -18,7 +18,7 @@ export function openBrowser(target: string): void { // spawn reports launcher failures (for example, missing xdg-open) via an // error event. Browser launch is best-effort: callers still present the target // to the user, so keep the launcher failure from becoming a process crash. - spawn(cmd, args, { stdio: "ignore", detached: true }) + spawn(cmd, args, { stdio: "ignore", detached: true, windowsHide: true }) .on("error", () => {}) .unref(); } diff --git a/packages/coding-agent/src/utils/paths.ts b/packages/coding-agent/src/utils/paths.ts index fd22cb9ac0..b042fc87e6 100644 --- a/packages/coding-agent/src/utils/paths.ts +++ b/packages/coding-agent/src/utils/paths.ts @@ -138,9 +138,9 @@ export function markPathIgnoredByCloudSync(path: string): void { for (const attr of attrs) { if (process.platform === "darwin") { - spawnProcessSync("xattr", ["-w", attr, "1", path], { encoding: "utf-8", stdio: "ignore" }); + spawnProcessSync("xattr", ["-w", attr, "1", path], { encoding: "utf-8", stdio: "ignore", windowsHide: true }); } else { - spawnProcessSync("setfattr", ["-n", attr, "-v", "1", path], { encoding: "utf-8", stdio: "ignore" }); + spawnProcessSync("setfattr", ["-n", attr, "-v", "1", path], { encoding: "utf-8", stdio: "ignore", windowsHide: true }); } } } diff --git a/packages/coding-agent/src/utils/shell.ts b/packages/coding-agent/src/utils/shell.ts index 76a0b594ab..c7d6f44a33 100644 --- a/packages/coding-agent/src/utils/shell.ts +++ b/packages/coding-agent/src/utils/shell.ts @@ -87,7 +87,7 @@ function findBashOnPath(): string | null { // Unix: Use 'which' and trust its output (handles Termux and special filesystems) try { - const result = spawnSync("which", ["bash"], { encoding: "utf-8", timeout: 5000 }); + const result = spawnSync("which", ["bash"], { encoding: "utf-8", timeout: 5000, windowsHide: true }); if (result.status === 0 && result.stdout) { const firstMatch = result.stdout.trim().split(/\r?\n/)[0]; if (firstMatch) { diff --git a/packages/coding-agent/src/utils/tools-manager.ts b/packages/coding-agent/src/utils/tools-manager.ts index f229d4106b..64c8b2e11b 100644 --- a/packages/coding-agent/src/utils/tools-manager.ts +++ b/packages/coding-agent/src/utils/tools-manager.ts @@ -75,7 +75,7 @@ const TOOLS: Record = { // Check if a command exists in PATH by trying to run it function commandExists(cmd: string): boolean { try { - const result = spawnSync(cmd, ["--version"], { stdio: "pipe" }); + const result = spawnSync(cmd, ["--version"], { stdio: "pipe", windowsHide: true }); // Check for ENOENT error (command not found) return result.error === undefined || result.error === null; } catch { @@ -177,7 +177,7 @@ function formatSpawnFailure(result: SpawnSyncReturns): string { } function runExtractionCommand(command: string, args: string[]): string | null { - const result = spawnSync(command, args, { stdio: "pipe" }); + const result = spawnSync(command, args, { stdio: "pipe", windowsHide: true }); if (!result.error && result.status === 0) { return null; }