diff --git a/browse/src/bun-polyfill.cjs b/browse/src/bun-polyfill.cjs index 9bc1bb453..89e16c4fd 100644 --- a/browse/src/bun-polyfill.cjs +++ b/browse/src/bun-polyfill.cjs @@ -75,9 +75,11 @@ globalThis.Bun = { timeout: options.timeout, env: options.env, cwd: options.cwd, - // Bun never shows a console window; node's spawn defaults windowsHide to - // false, so without this every console child pops a window on Windows. - windowsHide: true, + // Node defaults windowsHide to false; Bun.spawn hides the console + // window. Without this the shim silently inverts the behavior on the + // one platform it exists to serve — every console child pops a window. + // Forwarded (not hardcoded) so an explicit windowsHide:false survives. + windowsHide: options.windowsHide !== false, }); return { @@ -94,10 +96,12 @@ globalThis.Bun = { stdio, env: options.env, cwd: options.cwd, - // See spawnSync. This is the one users notice: spawnTerminalAgent() launches - // `bun run terminal-agent.ts` through here and the daemon respawns it on a - // watchdog, so an empty console window reappears every few minutes. - windowsHide: true, + // stdio:'ignore' silences a child's output but does not suppress its + // console window on Windows. The terminal-agent respawn (server.ts + // watchdog, 60s ticker) popped a visible bun.exe window on every + // respawn until this was forwarded. Forwarded, not hardcoded, so an + // explicit windowsHide:false survives. + windowsHide: options.windowsHide !== false, }); return { diff --git a/browse/src/terminal-agent-control.ts b/browse/src/terminal-agent-control.ts index 094ba668f..9d96a73a6 100644 --- a/browse/src/terminal-agent-control.ts +++ b/browse/src/terminal-agent-control.ts @@ -77,6 +77,10 @@ export function spawnTerminalAgent(opts: { ...(opts.extraEnv || {}), }, stdio: ['ignore', 'ignore', 'ignore'], + // Explicit for the Node fallback path (dist/bun-polyfill.cjs), where the + // host default is the opposite of Bun's. A visible console window on every + // watchdog respawn is the symptom when this is missing. + windowsHide: true, }); proc.unref?.(); return proc.pid ?? null; diff --git a/browse/test/bun-polyfill.test.ts b/browse/test/bun-polyfill.test.ts index f0bb05286..152ed3181 100644 --- a/browse/test/bun-polyfill.test.ts +++ b/browse/test/bun-polyfill.test.ts @@ -70,24 +70,47 @@ describe('bun-polyfill', () => { expect(lines[1]).toBe('HAS_PORT'); }); - // Regression: node defaults windowsHide to false, so a console child spawned - // through this shim pops a visible window on Windows — bun never does. Asserted - // by stubbing child_process before the polyfill destructures it, so the check is - // deterministic and runs on every platform. - test('spawn and spawnSync pass windowsHide so Windows shows no console window', () => { + // windowsHide is the one option where Node's default is the opposite of + // Bun's: Node shows the child's console window, Bun hides it. Dropping it + // in translation makes every spawned child pop a window on Windows, which + // is the platform this whole file exists for. Both shims are covered, and + // an explicit windowsHide:false must survive forwarding (#2523 + #2539). + test('Bun.spawn defaults windowsHide to true', () => { const result = Bun.spawnSync(['node', '-e', ` const cp = require('child_process'); - const seen = []; - cp.spawn = (c, a, o) => { seen.push(o); return { pid: 1, stdout: null, stderr: null, stdin: null, unref() {}, kill() {} }; }; - cp.spawnSync = (c, a, o) => { seen.push(o); return { status: 0, stdout: Buffer.from(''), stderr: Buffer.from('') }; }; + const orig = cp.spawn; + let seen; + cp.spawn = (c, a, o) => { seen = o; return orig(c, a, o); }; require(${JSON.stringify(polyfillPath)}); - Bun.spawn(['echo', 'x'], { stdio: ['pipe', 'pipe', 'pipe'] }); - Bun.spawnSync(['echo', 'x'], { stdout: 'pipe' }); - console.log(seen.length === 2 ? 'BOTH' : 'GOT_' + seen.length); - console.log(seen.every((o) => o && o.windowsHide === true) ? 'HIDDEN' : 'VISIBLE'); + Bun.spawn(['node', '-e', ''], { stdio: ['ignore', 'ignore', 'ignore'] }); + console.log('windowsHide:' + seen.windowsHide); `], { stdout: 'pipe', stderr: 'pipe' }); - const lines = result.stdout.toString().trim().split('\n'); - expect(lines[0]).toBe('BOTH'); - expect(lines[1]).toBe('HIDDEN'); + expect(result.stdout.toString().trim()).toBe('windowsHide:true'); + }); + + test('Bun.spawnSync defaults windowsHide to true', () => { + const result = Bun.spawnSync(['node', '-e', ` + const cp = require('child_process'); + const orig = cp.spawnSync; + let seen; + cp.spawnSync = (c, a, o) => { seen = o; return orig(c, a, o); }; + require(${JSON.stringify(polyfillPath)}); + Bun.spawnSync(['node', '-e', '']); + console.log('windowsHide:' + seen.windowsHide); + `], { stdout: 'pipe', stderr: 'pipe' }); + expect(result.stdout.toString().trim()).toBe('windowsHide:true'); + }); + + test('an explicit windowsHide:false is honored', () => { + const result = Bun.spawnSync(['node', '-e', ` + const cp = require('child_process'); + const orig = cp.spawn; + let seen; + cp.spawn = (c, a, o) => { seen = o; return orig(c, a, o); }; + require(${JSON.stringify(polyfillPath)}); + Bun.spawn(['node', '-e', ''], { stdio: ['ignore', 'ignore', 'ignore'], windowsHide: false }); + console.log('windowsHide:' + seen.windowsHide); + `], { stdout: 'pipe', stderr: 'pipe' }); + expect(result.stdout.toString().trim()).toBe('windowsHide:false'); }); });