diff --git a/dist/codex-hook-trust.js b/dist/codex-hook-trust.js index 7303275f..24771dcb 100644 --- a/dist/codex-hook-trust.js +++ b/dist/codex-hook-trust.js @@ -43,6 +43,7 @@ export async function detectCodexHookTrustState(codexHome, cwd, timeoutMs = 1000 const child = spawn('codex', ['app-server'], { env: { ...process.env, CODEX_HOME: codexHome }, stdio: ['pipe', 'pipe', 'ignore'], + windowsHide: true, }); const rl = readline.createInterface({ input: child.stdout }); const pending = new Map(); diff --git a/dist/summarizer.js b/dist/summarizer.js index 60719809..74d2c4e0 100644 --- a/dist/summarizer.js +++ b/dist/summarizer.js @@ -395,7 +395,8 @@ function readCommandOutput(command, args) { return new Promise((resolve, reject) => { const child = spawn(command, args, { env: getApiEnv(), - stdio: ['ignore', 'pipe', 'pipe'] + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true }); let output = ''; child.stdout.on('data', chunk => { @@ -444,7 +445,8 @@ export async function runCodexCommand(command) { return new Promise((resolve, reject) => { const child = spawn(command.command, command.args, { env: getApiEnv(), - stdio: ['pipe', 'pipe', 'pipe'] + stdio: ['pipe', 'pipe', 'pipe'], + windowsHide: true }); let stderr = ''; let answer = ''; diff --git a/dist/sync-cli.js b/dist/sync-cli.js index 26172c70..6cd06587 100644 --- a/dist/sync-cli.js +++ b/dist/sync-cli.js @@ -136,6 +136,7 @@ if (isBackground) { ...filteredArgs ], { detached: true, + windowsHide: true, stdio: ['ignore', logFd, logFd] }); child.unref(); // Allow parent to exit diff --git a/src/codex-hook-trust.ts b/src/codex-hook-trust.ts index 717a317e..26c11fcf 100644 --- a/src/codex-hook-trust.ts +++ b/src/codex-hook-trust.ts @@ -62,6 +62,7 @@ export async function detectCodexHookTrustState( const child = spawn('codex', ['app-server'], { env: { ...process.env, CODEX_HOME: codexHome }, stdio: ['pipe', 'pipe', 'ignore'], + windowsHide: true, }); const rl = readline.createInterface({ input: child.stdout }); const pending = new Map { return new Promise((resolve, reject) => { const child = spawn(command, args, { env: getApiEnv(), - stdio: ['ignore', 'pipe', 'pipe'] + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true }); let output = ''; @@ -511,7 +512,8 @@ export async function runCodexCommand(command: CodexSummarizerCommand): Promise< return new Promise((resolve, reject) => { const child = spawn(command.command, command.args, { env: getApiEnv(), - stdio: ['pipe', 'pipe', 'pipe'] + stdio: ['pipe', 'pipe', 'pipe'], + windowsHide: true }); let stderr = ''; diff --git a/src/sync-cli.ts b/src/sync-cli.ts index 297b20f8..bbc5fa3c 100644 --- a/src/sync-cli.ts +++ b/src/sync-cli.ts @@ -149,6 +149,7 @@ if (isBackground) { ...filteredArgs ], { detached: true, + windowsHide: true, stdio: ['ignore', logFd, logFd] }); diff --git a/test/spawn-windows-hide.test.ts b/test/spawn-windows-hide.test.ts new file mode 100644 index 00000000..4661f7ec --- /dev/null +++ b/test/spawn-windows-hide.test.ts @@ -0,0 +1,48 @@ +import { describe, it, expect } from 'vitest'; +import { readdirSync, readFileSync } from 'fs'; +import { join } from 'path'; + +// On Windows, a console program started from a process with no console of its +// own (the detached background sync, the summarizer it runs) gets a brand-new +// console window unless the spawn passes `windowsHide: true`. Users saw a +// terminal window flash for every background sync and every `codex app-server` +// summarizer call. Every spawn in src/ must hide its window. +const SRC_DIR = join(__dirname, '..', 'src'); + +function spawnCallOptions(source: string): string[] { + const calls: string[] = []; + const pattern = /\bspawn\(/g; + let match: RegExpExecArray | null; + while ((match = pattern.exec(source)) !== null) { + let depth = 0; + let end = match.index + match[0].length - 1; + for (; end < source.length; end++) { + const ch = source[end]; + if (ch === '(') depth++; + else if (ch === ')') { + depth--; + if (depth === 0) break; + } + } + calls.push(source.slice(match.index, end + 1)); + } + return calls; +} + +describe('child processes hide their console window on Windows', () => { + const files = readdirSync(SRC_DIR).filter(name => name.endsWith('.ts')); + const calls = files.flatMap(name => + spawnCallOptions(readFileSync(join(SRC_DIR, name), 'utf8')).map(call => ({ name, call })) + ); + + it('finds the spawn calls it checks', () => { + expect(calls.length).toBeGreaterThanOrEqual(4); + }); + + it.each(calls.map(({ name, call }) => [name, call.split('\n')[0], call]))( + '%s: %s', + (_name, _firstLine, call) => { + expect(call).toMatch(/windowsHide:\s*true/); + } + ); +});