diff --git a/src/tests/integration/web-mode-cli.test.ts b/src/tests/integration/web-mode-cli.test.ts index 249e17568..68b6c9c1b 100644 --- a/src/tests/integration/web-mode-cli.test.ts +++ b/src/tests/integration/web-mode-cli.test.ts @@ -164,6 +164,7 @@ test('launchWebMode prefers the packaged standalone host and opens the resolved cwd: standaloneRoot, detached: true, stdio: 'ignore', + windowsHide: true, env: { TEST_ENV: '1', HOSTNAME: '127.0.0.1', diff --git a/src/tests/integration/web-mode-windows-hide.test.ts b/src/tests/integration/web-mode-windows-hide.test.ts new file mode 100644 index 000000000..aeb6baeea --- /dev/null +++ b/src/tests/integration/web-mode-windows-hide.test.ts @@ -0,0 +1,120 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +const webMode = await import("../../web-mode.ts"); + +// --------------------------------------------------------------------------- +// #2628 — On Windows, child processes spawned by web-mode must set +// `windowsHide: true` to prevent console windows from flashing on screen. +// --------------------------------------------------------------------------- + +test("launchWebMode passes windowsHide: true in spawn options", async (t) => { + const tmp = mkdtempSync(join(tmpdir(), "gsd-web-winhide-")); + const standaloneRoot = join(tmp, "dist", "web", "standalone"); + const serverPath = join(standaloneRoot, "server.js"); + mkdirSync(standaloneRoot, { recursive: true }); + writeFileSync(serverPath, 'console.log("stub")\n'); + + const pidFilePath = join(tmp, "web-server.pid"); + const registryPath = join(tmp, "web-instances.json"); + + let capturedOptions: Record | undefined; + + t.after(() => { + rmSync(tmp, { recursive: true, force: true }); + }); + + const status = await webMode.launchWebMode( + { + cwd: "/tmp/winhide-project", + projectSessionsDir: "/tmp/.gsd/sessions/winhide", + agentDir: "/tmp/.gsd/agent", + packageRoot: tmp, + }, + { + initResources: () => {}, + resolvePort: async () => 46000, + execPath: "/custom/node", + env: { TEST_ENV: "1" }, + spawn: (_command, _args, options) => { + capturedOptions = options as Record; + return { + pid: 70001, + once: () => undefined, + unref: () => {}, + } as any; + }, + waitForBootReady: async () => undefined, + openBrowser: () => {}, + pidFilePath, + writePidFile: webMode.writePidFile, + registryPath, + stderr: { write: () => true }, + }, + ); + + assert.equal(status.ok, true, "launch should succeed"); + assert.ok(capturedOptions, "spawn must have been called"); + assert.equal( + capturedOptions!.windowsHide, + true, + "spawn options must include windowsHide: true to prevent console window flashing on Windows (#2628)", + ); +}); + +test("launchWebMode source-dev host also passes windowsHide: true", async (t) => { + const tmp = mkdtempSync(join(tmpdir(), "gsd-web-winhide-src-")); + const webRoot = join(tmp, "web"); + mkdirSync(webRoot, { recursive: true }); + writeFileSync(join(webRoot, "package.json"), '{"name":"web"}\n'); + + const pidFilePath = join(tmp, "web-server.pid"); + const registryPath = join(tmp, "web-instances.json"); + + let capturedOptions: Record | undefined; + + t.after(() => { + rmSync(tmp, { recursive: true, force: true }); + }); + + const status = await webMode.launchWebMode( + { + cwd: "/tmp/winhide-src-project", + projectSessionsDir: "/tmp/.gsd/sessions/winhide-src", + agentDir: "/tmp/.gsd/agent", + packageRoot: tmp, + }, + { + initResources: () => {}, + resolvePort: async () => 46001, + execPath: "/custom/node", + env: { TEST_ENV: "1" }, + platform: "win32", + spawn: (_command, _args, options) => { + capturedOptions = options as Record; + return { + pid: 70002, + once: () => undefined, + unref: () => {}, + } as any; + }, + waitForBootReady: async () => undefined, + openBrowser: () => {}, + pidFilePath, + writePidFile: webMode.writePidFile, + registryPath, + stderr: { write: () => true }, + }, + ); + + assert.equal(status.ok, true, "launch should succeed"); + assert.ok(capturedOptions, "spawn must have been called"); + assert.equal( + capturedOptions!.windowsHide, + true, + "source-dev spawn must also include windowsHide: true (#2628)", + ); +}); diff --git a/src/web-mode.ts b/src/web-mode.ts index 42683a667..665e0f5a8 100644 --- a/src/web-mode.ts +++ b/src/web-mode.ts @@ -14,7 +14,7 @@ const DEFAULT_PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '. function openBrowser(url: string): void { if (process.platform === 'win32') { // PowerShell's Start-Process handles URLs with '&' safely; cmd /c start does not. - execFile('powershell', ['-c', `Start-Process '${url.replace(/'/g, "''")}'`], () => {}) + execFile('powershell', ['-c', `Start-Process '${url.replace(/'/g, "''")}'`], { windowsHide: true }, () => {}) } else { const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open' execFile(cmd, [url], () => {}) @@ -635,6 +635,7 @@ export async function launchWebMode( cwd: spawnSpec.cwd, detached: true, stdio: 'ignore', + windowsHide: true, env, }, ) diff --git a/src/web/auto-dashboard-service.ts b/src/web/auto-dashboard-service.ts index e25bb7bff..972c7474f 100644 --- a/src/web/auto-dashboard-service.ts +++ b/src/web/auto-dashboard-service.ts @@ -153,6 +153,7 @@ export async function collectAuthoritativeAutoDashboardData( [AUTO_DASHBOARD_MODULE_ENV]: autoModulePath, }, maxBuffer: AUTO_DASHBOARD_MAX_BUFFER, + windowsHide: true, }, (error, stdout, stderr) => { if (error) { diff --git a/src/web/bridge-service.ts b/src/web/bridge-service.ts index 6f5ed530a..2f8a4f212 100644 --- a/src/web/bridge-service.ts +++ b/src/web/bridge-service.ts @@ -772,6 +772,7 @@ async function loadSessionBrowserSessionsViaChildProcess(config: BridgeRuntimeCo GSD_SESSION_BROWSER_DIR: config.projectSessionsDir, }, maxBuffer: 1024 * 1024, + windowsHide: true, }, (error, stdout, stderr) => { if (error) { @@ -833,6 +834,7 @@ async function appendSessionInfoViaChildProcess( GSD_TARGET_SESSION_NAME: name, }, maxBuffer: 1024 * 1024, + windowsHide: true, }, (error, _stdout, stderr) => { if (error) { @@ -1031,6 +1033,7 @@ async function loadWorkspaceIndexViaChildProcess(basePath: string, packageRoot: GSD_WORKSPACE_BASE: basePath, }, maxBuffer: 1024 * 1024, + windowsHide: true, }, (error, stdout, stderr) => { if (error) { @@ -1624,6 +1627,7 @@ export class BridgeService { cwd: cliEntry.cwd, env: childEnv, stdio: ["pipe", "pipe", "pipe"], + windowsHide: true, }) as SpawnedRpcChild; this.process = child; diff --git a/src/web/captures-service.ts b/src/web/captures-service.ts index 1f7cb1189..2a8b4c9b8 100644 --- a/src/web/captures-service.ts +++ b/src/web/captures-service.ts @@ -64,6 +64,7 @@ export async function collectCapturesData(projectCwdOverride?: string): Promise< GSD_CAPTURES_BASE: projectCwd, }, maxBuffer: CAPTURES_MAX_BUFFER, + windowsHide: true, }, (error, stdout, stderr) => { if (error) { @@ -136,6 +137,7 @@ export async function resolveCaptureAction(request: CaptureResolveRequest, proje GSD_CAPTURES_BASE: projectCwd, }, maxBuffer: CAPTURES_MAX_BUFFER, + windowsHide: true, }, (error, stdout, stderr) => { if (error) { diff --git a/src/web/cleanup-service.ts b/src/web/cleanup-service.ts index 145201f31..2ef778a4e 100644 --- a/src/web/cleanup-service.ts +++ b/src/web/cleanup-service.ts @@ -78,6 +78,7 @@ export async function collectCleanupData(projectCwdOverride?: string): Promise { if (error) { @@ -170,6 +171,7 @@ export async function executeCleanup( GSD_CLEANUP_SNAPSHOTS: JSON.stringify(pruneSnapshots), }, maxBuffer: CLEANUP_MAX_BUFFER, + windowsHide: true, }, (error, stdout, stderr) => { if (error) { diff --git a/src/web/doctor-service.ts b/src/web/doctor-service.ts index 8fac5b272..ec5bc4dac 100644 --- a/src/web/doctor-service.ts +++ b/src/web/doctor-service.ts @@ -41,6 +41,7 @@ function runDoctorChild( GSD_DOCTOR_SCOPE: scope ?? "", }, maxBuffer: DOCTOR_MAX_BUFFER, + windowsHide: true, }, (error, stdout, stderr) => { if (error) { diff --git a/src/web/export-service.ts b/src/web/export-service.ts index 431f31473..002c98a94 100644 --- a/src/web/export-service.ts +++ b/src/web/export-service.ts @@ -74,6 +74,7 @@ export async function collectExportData( GSD_EXPORT_FORMAT: format, }, maxBuffer: EXPORT_MAX_BUFFER, + windowsHide: true, }, (error, stdout, stderr) => { if (error) { diff --git a/src/web/forensics-service.ts b/src/web/forensics-service.ts index 445fa59e6..ac74855d6 100644 --- a/src/web/forensics-service.ts +++ b/src/web/forensics-service.ts @@ -94,6 +94,7 @@ export async function collectForensicsData(projectCwdOverride?: string): Promise GSD_FORENSICS_BASE: projectCwd, }, maxBuffer: FORENSICS_MAX_BUFFER, + windowsHide: true, }, (error, stdout, stderr) => { if (error) { diff --git a/src/web/history-service.ts b/src/web/history-service.ts index a2ee75c68..ac1808aa2 100644 --- a/src/web/history-service.ts +++ b/src/web/history-service.ts @@ -66,6 +66,7 @@ export async function collectHistoryData(projectCwdOverride?: string): Promise { if (error) { diff --git a/src/web/hooks-service.ts b/src/web/hooks-service.ts index 9eeac1276..5eebcf4d9 100644 --- a/src/web/hooks-service.ts +++ b/src/web/hooks-service.ts @@ -66,6 +66,7 @@ export async function collectHooksData(projectCwdOverride?: string): Promise { if (error) { diff --git a/src/web/recovery-diagnostics-service.ts b/src/web/recovery-diagnostics-service.ts index ee5abeb92..cc9c8b9e8 100644 --- a/src/web/recovery-diagnostics-service.ts +++ b/src/web/recovery-diagnostics-service.ts @@ -491,6 +491,7 @@ async function collectRecoveryDiagnosticsChildPayload( GSD_RECOVERY_FORENSICS_MODULE: sessionForensicsModulePath, }, maxBuffer: RECOVERY_DIAGNOSTICS_MAX_BUFFER, + windowsHide: true, }, (error, stdout, stderr) => { if (error) { diff --git a/src/web/settings-service.ts b/src/web/settings-service.ts index 8e1b5c6ea..7a2a8df24 100644 --- a/src/web/settings-service.ts +++ b/src/web/settings-service.ts @@ -142,6 +142,7 @@ export async function collectSettingsData(projectCwdOverride?: string): Promise< GSD_SETTINGS_BASE: projectCwd, }, maxBuffer: SETTINGS_MAX_BUFFER, + windowsHide: true, }, (error, stdout, stderr) => { if (error) { diff --git a/src/web/skill-health-service.ts b/src/web/skill-health-service.ts index 60834dc96..43d586884 100644 --- a/src/web/skill-health-service.ts +++ b/src/web/skill-health-service.ts @@ -61,6 +61,7 @@ export async function collectSkillHealthData(projectCwdOverride?: string): Promi GSD_SKILL_HEALTH_BASE: projectCwd, }, maxBuffer: SKILL_HEALTH_MAX_BUFFER, + windowsHide: true, }, (error, stdout, stderr) => { if (error) { diff --git a/src/web/undo-service.ts b/src/web/undo-service.ts index ad339a359..2a218cc54 100644 --- a/src/web/undo-service.ts +++ b/src/web/undo-service.ts @@ -195,6 +195,7 @@ export async function executeUndo(projectCwdOverride?: string): Promise { if (error) { diff --git a/src/web/update-service.ts b/src/web/update-service.ts index 1ec44aa1a..62c728161 100644 --- a/src/web/update-service.ts +++ b/src/web/update-service.ts @@ -73,6 +73,7 @@ export function triggerUpdate(targetVersion?: string): boolean { stdio: ["ignore", "ignore", "pipe"], // Detach so the child process is not killed if the parent exits detached: false, + windowsHide: true, }) let stderr = "" diff --git a/src/web/visualizer-service.ts b/src/web/visualizer-service.ts index 93b1fcdd0..11a21e8f8 100644 --- a/src/web/visualizer-service.ts +++ b/src/web/visualizer-service.ts @@ -98,6 +98,7 @@ export async function collectVisualizerData(projectCwdOverride?: string): Promis GSD_VISUALIZER_BASE: projectCwd, }, maxBuffer: VISUALIZER_MAX_BUFFER, + windowsHide: true, }, (error, stdout, stderr) => { if (error) {