diff --git a/packages/coding-agent/src/config.ts b/packages/coding-agent/src/config.ts index b94452ec21..bfc0e58001 100644 --- a/packages/coding-agent/src/config.ts +++ b/packages/coding-agent/src/config.ts @@ -201,6 +201,7 @@ function readCommandOutput( encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], shell: shouldUseWindowsShell(command), + windowsHide: true, // avoid console window flash on Windows (console-less daemon workers) }); if (result.status === 0) return result.stdout.trim() || undefined; if (options.requireSuccess) { diff --git a/packages/coding-agent/src/core/autonomous.ts b/packages/coding-agent/src/core/autonomous.ts index 75930d415c..638c2e2ece 100644 --- a/packages/coding-agent/src/core/autonomous.ts +++ b/packages/coding-agent/src/core/autonomous.ts @@ -496,6 +496,9 @@ function runChildProcess( detached: process.platform !== "win32", shell: options.shell === true, stdio: ["ignore", "pipe", "pipe"], + // These run inside console-less daemon workers; avoid a console window + // flash on Windows (see runGit in utils/git.ts). + windowsHide: true, }); if (child.pid) { trackDetachedChildPid(child.pid); diff --git a/packages/coding-agent/src/core/exec.ts b/packages/coding-agent/src/core/exec.ts index bd62eb5a0e..d86a314340 100644 --- a/packages/coding-agent/src/core/exec.ts +++ b/packages/coding-agent/src/core/exec.ts @@ -62,6 +62,9 @@ export async function execCommand( cwd, shell: false, stdio: ["ignore", "pipe", "pipe"], + // Runs inside console-less daemon workers; avoid a console window + // flash on Windows (see runGit in utils/git.ts). + windowsHide: true, // Merge per-call env over the parent env so callers can scope vars // (e.g. herdr pane identity) without mutating the shared process.env. env: mergeExecEnv(options?.env), diff --git a/packages/coding-agent/src/core/footer-data-provider.ts b/packages/coding-agent/src/core/footer-data-provider.ts index 2aff8ac321..301606d57d 100644 --- a/packages/coding-agent/src/core/footer-data-provider.ts +++ b/packages/coding-agent/src/core/footer-data-provider.ts @@ -10,6 +10,8 @@ function resolveBranchWithGitSync(repoDir: string): string | null { cwd: repoDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], + // See runGit in utils/git.ts: avoid a console window flash on Windows. + windowsHide: true, }); const branch = result.status === 0 ? result.stdout.trim() : ""; return branch || null; @@ -24,6 +26,8 @@ function resolveBranchWithGitAsync(repoDir: string): Promise { { cwd: repoDir, encoding: "utf8", + // See runGit in utils/git.ts: avoid a console window flash on Windows. + windowsHide: true, }, (error: ExecFileException | null, stdout: string) => { if (error) { diff --git a/packages/coding-agent/src/core/kernel/bootstrap.ts b/packages/coding-agent/src/core/kernel/bootstrap.ts index 9b12b4b413..c656996ddf 100644 --- a/packages/coding-agent/src/core/kernel/bootstrap.ts +++ b/packages/coding-agent/src/core/kernel/bootstrap.ts @@ -376,6 +376,7 @@ function run(command: string, args: string[], options: { stdio?: "ignore" | "inh const child = spawn(command, args, { env: process.env, stdio: options.stdio ?? "ignore", + windowsHide: true, // avoid console window flash on Windows (console-less daemon workers) }); child.on("error", reject); child.on("exit", (code, signal) => { @@ -497,6 +498,12 @@ async function acquireBootstrapLock(venv: string): Promise<() => Promise> } } +// uv venv layouts differ by platform: POSIX puts the interpreter at bin/python, +// Windows at Scripts/python.exe. Hardcoding "bin/python" breaks bootstrap on Windows. +function venvPythonPath(venv: string): string { + return process.platform === "win32" ? path.join(venv, "Scripts", "python.exe") : path.join(venv, "bin", "python"); +} + async function findExecutable(name: string): Promise { const pathValue = process.env.PATH; if (!pathValue) return null; @@ -725,7 +732,7 @@ async function bootstrapVenv( ): Promise { await mkdir(path.dirname(venv), { recursive: true }); const uv = await ensureUv(options); - const python = path.join(venv, "bin", "python"); + const python = venvPythonPath(venv); const sourceDir = await resolveRuntimeSourceDir(); const runtimeRequirement = sourceDir ?? RUNTIME_REQUIREMENT; const runtimeIdentity = await resolveRuntimeIdentity(); @@ -886,7 +893,7 @@ async function ensureKernelPythonUncached( } const venv = await resolveWritableKernelVenvDir(); - const python = path.join(venv, "bin", "python"); + const python = venvPythonPath(venv); const runtimeIdentity = await resolveRuntimeIdentity(); if (await kernelReady(python, venv, runtimeIdentity, pythonSkills)) return python; diff --git a/packages/coding-agent/src/core/kernel/fork-server.ts b/packages/coding-agent/src/core/kernel/fork-server.ts index d5b5423ac2..9ce77026f3 100644 --- a/packages/coding-agent/src/core/kernel/fork-server.ts +++ b/packages/coding-agent/src/core/kernel/fork-server.ts @@ -175,6 +175,7 @@ class ForkServer { const proc = spawn(this.params.python, ["-c", FORK_SERVER_SCRIPT, socketPath], { env: this.launchEnv, stdio: ["ignore", "ignore", "pipe"], + windowsHide: true, // avoid console window flash on Windows (console-less daemon workers) }); this.proc = proc; proc.stderr?.on("data", (buf: Buffer) => { diff --git a/packages/coding-agent/src/core/kernel/index.ts b/packages/coding-agent/src/core/kernel/index.ts index b760a2e1e2..33f267f9f8 100644 --- a/packages/coding-agent/src/core/kernel/index.ts +++ b/packages/coding-agent/src/core/kernel/index.ts @@ -648,6 +648,7 @@ export class KernelManager { cwd: this.options.cwd, env: this.options.env ? { ...process.env, ...this.options.env } : process.env, stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, // avoid console window flash on Windows (console-less daemon workers) }); this.kernel = kernel; diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index fd818595f3..ecd227e375 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -2396,6 +2396,7 @@ export class DefaultPackageManager implements PackageManager { cwd: options?.cwd, stdio: ["ignore", "pipe", "pipe"], shell: shouldUseWindowsShell(command), + windowsHide: true, // avoid console window flash on Windows (console-less daemon workers) env: options?.env ? { ...baseEnv, ...options.env } : baseEnv, }); } @@ -2463,6 +2464,7 @@ export class DefaultPackageManager implements PackageManager { stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8", shell: shouldUseWindowsShell(command), + windowsHide: true, env: getEnv(), }); if (result.error || result.status !== 0) { diff --git a/packages/coding-agent/src/core/session-file-actions.ts b/packages/coding-agent/src/core/session-file-actions.ts index adcec4c012..94e706d57b 100644 --- a/packages/coding-agent/src/core/session-file-actions.ts +++ b/packages/coding-agent/src/core/session-file-actions.ts @@ -25,7 +25,7 @@ async function deleteSessionArtifacts(sessionPath: string): Promise { /** Remove the session `.jsonl`, trying the `trash` CLI first, then falling back to unlink. */ async function removeSessionFile(sessionPath: string): Promise { 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/core/session-lease.ts b/packages/coding-agent/src/core/session-lease.ts index 6c4e2975cf..4636794e00 100644 --- a/packages/coding-agent/src/core/session-lease.ts +++ b/packages/coding-agent/src/core/session-lease.ts @@ -116,6 +116,9 @@ function runProcessQuery(command: string, args: string[]): string { return execFileSync(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], + // Called from console-less daemon workers; avoid a console window flash + // on Windows (see runGit in utils/git.ts). + windowsHide: true, }); } diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index efc260eae1..10fb41e5a3 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -77,6 +77,7 @@ export function createLocalBashOperations(options?: { shellPath?: string }): Bas detached: process.platform !== "win32", env: env ?? getShellEnv(), stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, // avoid console window flash on Windows (console-less daemon workers) }); if (child.pid) trackDetachedChildPid(child.pid); let timedOut = false; diff --git a/packages/coding-agent/src/modes/daemon/command-recovery-journal.ts b/packages/coding-agent/src/modes/daemon/command-recovery-journal.ts index 05fcec0644..7a8f87978e 100644 --- a/packages/coding-agent/src/modes/daemon/command-recovery-journal.ts +++ b/packages/coding-agent/src/modes/daemon/command-recovery-journal.ts @@ -206,11 +206,16 @@ export class CommandRecoveryJournal { closeSync(descriptor); } renameSync(tempPath, this.path); - const directoryDescriptor = openSync(dirname(this.path), "r"); try { - fsyncSync(directoryDescriptor); - } finally { - closeSync(directoryDescriptor); + const directoryDescriptor = openSync(dirname(this.path), "r"); + try { + fsyncSync(directoryDescriptor); + } finally { + closeSync(directoryDescriptor); + } + } catch { + // Directory fsync is unavailable on some platforms (e.g. Windows raises + // EPERM); the atomic rename still protects readers. } this.recordCount = records.length; } diff --git a/packages/coding-agent/src/modes/daemon/daemon-catalog-process.ts b/packages/coding-agent/src/modes/daemon/daemon-catalog-process.ts index c841b5b79c..c0486f0b32 100644 --- a/packages/coding-agent/src/modes/daemon/daemon-catalog-process.ts +++ b/packages/coding-agent/src/modes/daemon/daemon-catalog-process.ts @@ -396,6 +396,7 @@ export class DaemonCatalogClient { cwd: process.cwd(), env: createCliSubprocessEnv({ ...process.env, [DAEMON_CATALOG_ROLE_ENV]: "1" }), stdio: ["ignore", "ignore", "ignore", "ipc"], + windowsHide: true, // avoid console window flash on Windows (console-less daemon workers) }); this.child = child; child.on("message", (value: unknown) => this.handleMessage(value)); diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index ccfef6a4d5..5fe262f11f 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -8665,7 +8665,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; @@ -8714,7 +8714,7 @@ 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]); + 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/rpc-client.ts b/packages/coding-agent/src/modes/rpc/rpc-client.ts index 7c32788164..262f1da283 100644 --- a/packages/coding-agent/src/modes/rpc/rpc-client.ts +++ b/packages/coding-agent/src/modes/rpc/rpc-client.ts @@ -107,6 +107,7 @@ export class RpcClient { cwd: this.options.cwd, env: { ...process.env, ...this.options.env }, stdio: ["pipe", "pipe", "pipe"], + windowsHide: true, // avoid console window flash on Windows (console-less daemon workers) }); // Collect stderr for debugging diff --git a/packages/coding-agent/src/utils/clipboard-image.ts b/packages/coding-agent/src/utils/clipboard-image.ts index 4cf44908f8..e6a892a12d 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, // avoid console window flash on Windows (console-less daemon workers) }); if (result.error) { diff --git a/packages/coding-agent/src/utils/git.ts b/packages/coding-agent/src/utils/git.ts index b60d98a003..a3d2b677d6 100644 --- a/packages/coding-agent/src/utils/git.ts +++ b/packages/coding-agent/src/utils/git.ts @@ -253,6 +253,9 @@ function runGit(cwd: string, args: string[]): string | null { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], + // Daemon workers run without a console; without this, each git spawn makes + // Windows allocate a fresh console window (visible flicker). + windowsHide: true, }); if (result.status !== 0 || typeof result.stdout !== "string") return null; return result.stdout.trim() || null; diff --git a/packages/coding-agent/src/utils/shell.ts b/packages/coding-agent/src/utils/shell.ts index cbf289d86a..6d60fb9419 100644 --- a/packages/coding-agent/src/utils/shell.ts +++ b/packages/coding-agent/src/utils/shell.ts @@ -16,7 +16,7 @@ function findBashOnPath(): string | null { if (process.platform === "win32") { // Windows: Use 'where' and verify file exists (where can return non-existent paths) try { - const result = spawnSync("where", ["bash.exe"], { encoding: "utf-8", timeout: 5000 }); + const result = spawnSync("where", ["bash.exe"], { encoding: "utf-8", timeout: 5000, windowsHide: true }); if (result.status === 0 && result.stdout) { const firstMatch = result.stdout.trim().split(/\r?\n/)[0]; if (firstMatch && existsSync(firstMatch)) { @@ -31,7 +31,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) { @@ -194,6 +194,7 @@ export function killProcessTree(pid: number): void { spawn("taskkill", ["/F", "/T", "/PID", String(pid)], { stdio: "ignore", detached: true, + windowsHide: true, }); } catch { // Ignore errors if taskkill fails diff --git a/packages/coding-agent/src/utils/tools-manager.ts b/packages/coding-agent/src/utils/tools-manager.ts index c3da7045ec..289cc220be 100644 --- a/packages/coding-agent/src/utils/tools-manager.ts +++ b/packages/coding-agent/src/utils/tools-manager.ts @@ -100,7 +100,7 @@ const TOOLS: Record = { // Check that a command both launches and reports a successful version. function commandWorks(cmd: string): boolean { try { - const result = spawnSync(cmd, ["--version"], { stdio: "pipe", timeout: COMMAND_TIMEOUT_MS }); + const result = spawnSync(cmd, ["--version"], { stdio: "pipe", timeout: COMMAND_TIMEOUT_MS, windowsHide: true }); return !result.error && result.status === 0; } catch { return false; @@ -224,7 +224,10 @@ async function downloadTool(tool: ManagedTool): Promise { try { if (assetName.endsWith(".tar.gz")) { - const extractResult = spawnSync("tar", ["xzf", archivePath, "-C", extractDir], { stdio: "pipe" }); + const extractResult = spawnSync("tar", ["xzf", archivePath, "-C", extractDir], { + stdio: "pipe", + windowsHide: true, + }); if (extractResult.error || extractResult.status !== 0) { const errMsg = extractResult.error?.message ?? extractResult.stderr?.toString().trim() ?? "unknown error"; throw new Error(`Failed to extract ${assetName}: ${errMsg}`); diff --git a/packages/tui/src/autocomplete.ts b/packages/tui/src/autocomplete.ts index bc11f530e4..5d7d7a2f71 100644 --- a/packages/tui/src/autocomplete.ts +++ b/packages/tui/src/autocomplete.ts @@ -164,6 +164,7 @@ async function walkDirectoryWithFd( const child = spawn(fdPath, args, { stdio: ["ignore", "pipe", "pipe"], + windowsHide: true, // avoid console window flash on Windows (console-less daemon workers) }); let stdout = ""; let resolved = false;