From 92d987e600ad3600346be9d11e9c521a0c00d433 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Wed, 3 Jun 2026 23:04:22 -0700 Subject: [PATCH 1/3] fix: suppress Windows console flashes from browse cold-start child spawns --- browse/src/cli.ts | 19 ++++++--- browse/src/config.ts | 28 +++++++++++++ browse/src/file-permissions.ts | 4 +- browse/test/cli-windows-hide.test.ts | 63 ++++++++++++++++++++++++++++ 4 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 browse/test/cli-windows-hide.test.ts diff --git a/browse/src/cli.ts b/browse/src/cli.ts index 59327b7923..9803c2c3b2 100644 --- a/browse/src/cli.ts +++ b/browse/src/cli.ts @@ -11,7 +11,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import { spawn as nodeSpawn } from 'child_process'; +import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from 'child_process'; import { safeUnlink, safeUnlinkQuiet, safeKill, isProcessAlive } from './error-handling'; import { writeSecureFile, mkdirSecure } from './file-permissions'; import { resolveConfig, ensureStateDir, readVersionHash } from './config'; @@ -141,10 +141,13 @@ async function killServer(pid: number): Promise { if (IS_WINDOWS) { // taskkill /T /F kills the process tree (Node + Chromium) try { - Bun.spawnSync( - ['taskkill', '/PID', String(pid), '/T', '/F'], - { stdout: 'pipe', stderr: 'pipe', timeout: 5000 } - ); + const proc = nodeSpawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { + stdout: 'pipe', + stderr: 'pipe', + timeout: 5000, + windowsHide: true, + }); + if (proc.error) throw proc.error; } catch (err: any) { if (err?.code !== 'ENOENT') throw err; } @@ -325,7 +328,11 @@ async function startServer(extraEnv?: Record): Promise { + test('Windows detach launcher uses hidden Node spawnSync', () => { + const body = read(CLI); + expect(body).toMatch(/spawnSync as nodeSpawnSync/); + expect(body).toMatch( + /nodeSpawnSync\(\s*['"]node['"],\s*\[\s*['"]-e['"],\s*launcherCode\s*\][\s\S]{0,250}windowsHide:\s*true/, + ); + expect(body).not.toMatch(/Bun\.spawnSync\(\s*\[\s*['"]node['"]/); + }); + + test('Windows taskkill helper uses hidden Node spawnSync', () => { + const body = read(CLI); + expect(body).toMatch( + /nodeSpawnSync\(\s*['"]taskkill['"][\s\S]{0,250}windowsHide:\s*true/, + ); + expect(body).not.toMatch(/Bun\.spawnSync\(\s*\[\s*['"]taskkill['"]/); + }); + + test('git probes use hidden Node spawnSync on Windows and keep Bun on POSIX', () => { + const body = read(CONFIG); + const hiddenGitSpawns = body.match( + /nodeSpawnSync\(\s*['"]git['"][\s\S]{0,250}windowsHide:\s*true/g, + ) || []; + expect(body).toContain("process.platform === 'win32'"); + expect(hiddenGitSpawns).toHaveLength(2); + expect(body).toMatch( + /Bun\.spawnSync\(\s*\[\s*['"]git['"],\s*['"]rev-parse['"],\s*['"]--show-toplevel['"]/, + ); + expect(body).toMatch( + /Bun\.spawnSync\(\s*\[\s*['"]git['"],\s*['"]remote['"],\s*['"]get-url['"],\s*['"]origin['"]/, + ); + }); + + test('icacls ACL helpers pass windowsHide to execFileSync', () => { + const body = read(FILE_PERMISSIONS); + const hiddenIcaclsCalls = body.match( + /execFileSync\(\s*['"]icacls['"][\s\S]{0,250}windowsHide:\s*true/g, + ) || []; + expect(hiddenIcaclsCalls).toHaveLength(2); + }); +}); From 4d30746a1b83caded12f60acff685956111fb293 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Thu, 4 Jun 2026 07:35:28 -0700 Subject: [PATCH 2/3] fix: hide the detached server windows too (folds in #1863) --- browse/src/cli.ts | 3 ++- browse/test/cli-windows-hide.test.ts | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/browse/src/cli.ts b/browse/src/cli.ts index 9803c2c3b2..516fb6eb35 100644 --- a/browse/src/cli.ts +++ b/browse/src/cli.ts @@ -326,7 +326,7 @@ async function startServer(extraEnv?: Record): Promise): Promise { expect(hiddenIcaclsCalls).toHaveLength(2); }); }); + +describe('detached server spawns carry windowsHide (#1863 fold-in)', () => { + test('Windows Node launcher inner spawn carries windowsHide:true', () => { + const body = read(CLI); + expect(body).toMatch(/spawn\(process\.execPath,[\s\S]{0,500}detached:true,windowsHide:true/); + }); + + test('non-Windows server nodeSpawn carries windowsHide:true', () => { + const body = read(CLI); + expect(body).toMatch(/nodeSpawn\('bun',[\s\S]{0,500}detached:\s*true[\s\S]{0,100}windowsHide:\s*true/); + }); + + test('every detached spawn site in cli.ts carries windowsHide:true', () => { + const body = read(CLI); + const detachedSpawns = body.match(/detached:\s*true/g)?.length ?? 0; + const windowsHideFlags = body.match(/windowsHide:\s*true/g)?.length ?? 0; + expect(windowsHideFlags).toBeGreaterThanOrEqual(detachedSpawns); + }); +}); From 77ebc1489ef4f5acce0147b3d7d732a8dc4f63e2 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Thu, 4 Jun 2026 07:36:13 -0700 Subject: [PATCH 3/3] fix: hide the osascript raise spawn too; exclude comments from the spawn invariant --- browse/src/cli.ts | 1 + browse/test/cli-windows-hide.test.ts | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/browse/src/cli.ts b/browse/src/cli.ts index 516fb6eb35..fbd90a848e 100644 --- a/browse/src/cli.ts +++ b/browse/src/cli.ts @@ -288,6 +288,7 @@ function raiseHeadedWindowMacOS(): void { nodeSpawn('osascript', ['-e', 'tell application "Google Chrome for Testing" to activate'], { stdio: 'ignore', detached: true, + windowsHide: true, }).unref(); } catch { // osascript missing or app not present — non-fatal diff --git a/browse/test/cli-windows-hide.test.ts b/browse/test/cli-windows-hide.test.ts index cd08adfad6..73e377ec4f 100644 --- a/browse/test/cli-windows-hide.test.ts +++ b/browse/test/cli-windows-hide.test.ts @@ -74,7 +74,10 @@ describe('detached server spawns carry windowsHide (#1863 fold-in)', () => { }); test('every detached spawn site in cli.ts carries windowsHide:true', () => { - const body = read(CLI); + const body = read(CLI) + .split('\n') + .filter((line) => !line.trim().startsWith('//') && !line.trim().startsWith('*')) + .join('\n'); const detachedSpawns = body.match(/detached:\s*true/g)?.length ?? 0; const windowsHideFlags = body.match(/windowsHide:\s*true/g)?.length ?? 0; expect(windowsHideFlags).toBeGreaterThanOrEqual(detachedSpawns);