From e700489bf11dc44f942f503c1e9ab92241f6bea7 Mon Sep 17 00:00:00 2001 From: Richardson Gunde Date: Sun, 26 Jul 2026 16:25:44 +0530 Subject: [PATCH 1/3] Fix Windows console-window flash on browser/runtime auto-launch (#470) Add windowsHide:true to every spawn() that launches a browser via cmd.exe (shell:true on win32) or starts the Docker/Podman engine. Without it, Node's spawn defaults windowsHide to false, so the intermediate console-subsystem process briefly flashes a visible window before handing off. win32-only option, no behavior change elsewhere. --- src/core/harness/sandbox.js | 5 ++++- src/hooks/auto-launch.js | 10 +++++++++- src/integrations/pi/rstack-sdlc.ts | 9 ++++++++- src/observability/dashboard/server.js | 9 ++++++++- 4 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/core/harness/sandbox.js b/src/core/harness/sandbox.js index 97ecd7c9..e992bd72 100644 --- a/src/core/harness/sandbox.js +++ b/src/core/harness/sandbox.js @@ -114,7 +114,10 @@ export async function startContainerRuntime({ return { runtime, started: false, ready: false, message: `${runtime} engine is not running and RStack cannot auto-start it on ${platform} — start it manually` }; } try { - const child = spawnImpl(startCmd.cmd, startCmd.args, { stdio: 'ignore' }); + // windowsHide suppresses the console window a freshly-spawned console + // subsystem process (cmd.exe on win32's docker path, podman.exe) would + // otherwise briefly flash — harmless elsewhere, since only win32 honors it. + const child = spawnImpl(startCmd.cmd, startCmd.args, { stdio: 'ignore', windowsHide: true }); child?.on?.('error', () => {}); child?.unref?.(); } catch (err) { diff --git a/src/hooks/auto-launch.js b/src/hooks/auto-launch.js index 922e0e74..0e1e889e 100644 --- a/src/hooks/auto-launch.js +++ b/src/hooks/auto-launch.js @@ -53,6 +53,14 @@ function openBrowser(url) { const cmd = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open'; try { - spawn(cmd, [url], { stdio: 'ignore', detached: true, shell: process.platform === 'win32' }).unref(); + // On win32 this runs through `cmd.exe /c start ` (shell: true); without + // windowsHide, that intermediate cmd.exe briefly flashes a visible console + // window before handing off to the actual browser process. + spawn(cmd, [url], { + stdio: 'ignore', + detached: true, + shell: process.platform === 'win32', + windowsHide: true, + }).unref(); } catch { /* best-effort */ } } diff --git a/src/integrations/pi/rstack-sdlc.ts b/src/integrations/pi/rstack-sdlc.ts index 2f077dba..109d8e59 100644 --- a/src/integrations/pi/rstack-sdlc.ts +++ b/src/integrations/pi/rstack-sdlc.ts @@ -99,7 +99,14 @@ function openUrl(url: string): void { const cmd = process.platform === "win32" ? "start" : process.platform === "darwin" ? "open" : "xdg-open"; try { - const cp = spawn(cmd, [url], { stdio: "ignore", detached: true, shell: process.platform === "win32" }); + // windowsHide suppresses the cmd.exe console window that `shell: true` + // would otherwise briefly flash on win32 before handing off to the browser. + const cp = spawn(cmd, [url], { + stdio: "ignore", + detached: true, + shell: process.platform === "win32", + windowsHide: true, + }); cp.on("error", () => {}); cp.unref(); } catch { /* best-effort */ } diff --git a/src/observability/dashboard/server.js b/src/observability/dashboard/server.js index 65ce6f44..a0ce2719 100644 --- a/src/observability/dashboard/server.js +++ b/src/observability/dashboard/server.js @@ -125,7 +125,14 @@ function openBrowser(url) { const cmd = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open'; try { - spawn(cmd, [url], { stdio: 'ignore', detached: true, shell: process.platform === 'win32' }).unref(); + // windowsHide suppresses the cmd.exe console window that `shell: true` + // would otherwise briefly flash on win32 before handing off to the browser. + spawn(cmd, [url], { + stdio: 'ignore', + detached: true, + shell: process.platform === 'win32', + windowsHide: true, + }).unref(); } catch { // best effort only } From 728099dfd8a5a38e1ed5254304fe448ff81bce5c Mon Sep 17 00:00:00 2001 From: Richardson Gunde Date: Sun, 26 Jul 2026 16:47:09 +0530 Subject: [PATCH 2/3] Add test coverage for the windowsHide fix (Qodo review, PR #471) Qodo flagged that the windowsHide:true change shipped with no tests. Export openBrowser (auto-launch.js, dashboard/server.js) and openUrl (rstack-sdlc.ts) with an injectable platform/spawnImpl (mirrors sandbox.js's existing spawnImpl convention) so the fix is testable without actually spawning a browser. Pins windowsHide:true + shell selection per-platform for auto-launch.js, rstack-sdlc.ts, and sandbox.js's startContainerRuntime. dashboard/server.js's identical openBrowser copy isn't covered by a direct import test: that module calls server.listen() unguarded at the top level (pre-existing, out of scope here), so importing it for the export would start a real HTTP server as a side effect. Covered by code-review parity with the other two call sites instead. --- src/hooks/auto-launch.js | 13 +++-- src/integrations/pi/rstack-sdlc.ts | 25 +++++--- src/observability/dashboard/server.js | 13 +++-- tests/windows-console-flash-470.test.js | 78 +++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 18 deletions(-) create mode 100644 tests/windows-console-flash-470.test.js diff --git a/src/hooks/auto-launch.js b/src/hooks/auto-launch.js index 0e1e889e..aadba125 100644 --- a/src/hooks/auto-launch.js +++ b/src/hooks/auto-launch.js @@ -49,17 +49,20 @@ export async function autoLaunchBusinessHub(projectRoot, opts = {}) { if (!opts.noBrowser && process.env.RSTACK_NO_BROWSER !== '1') openBrowser(url); } -function openBrowser(url) { - const cmd = process.platform === 'win32' ? 'start' - : process.platform === 'darwin' ? 'open' : 'xdg-open'; +// platform/spawnImpl are injectable (mirrors sandbox.js's spawnImpl convention) +// so #470's windowsHide behavior is testable per-platform without actually +// spawning a browser. +export function openBrowser(url, { platform = process.platform, spawnImpl = spawn } = {}) { + const cmd = platform === 'win32' ? 'start' + : platform === 'darwin' ? 'open' : 'xdg-open'; try { // On win32 this runs through `cmd.exe /c start ` (shell: true); without // windowsHide, that intermediate cmd.exe briefly flashes a visible console // window before handing off to the actual browser process. - spawn(cmd, [url], { + spawnImpl(cmd, [url], { stdio: 'ignore', detached: true, - shell: process.platform === 'win32', + shell: platform === 'win32', windowsHide: true, }).unref(); } catch { /* best-effort */ } diff --git a/src/integrations/pi/rstack-sdlc.ts b/src/integrations/pi/rstack-sdlc.ts index 109d8e59..0a255d79 100644 --- a/src/integrations/pi/rstack-sdlc.ts +++ b/src/integrations/pi/rstack-sdlc.ts @@ -1,7 +1,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { StringEnum } from "@earendil-works/pi-ai"; import { Type } from "typebox"; -import { spawn } from "node:child_process"; +import { spawn, type ChildProcess } from "node:child_process"; import { createHash, randomUUID } from "node:crypto"; import { request as httpRequest } from "node:http"; import { createConnection } from "node:net"; @@ -94,17 +94,26 @@ function safeOpen(filePath: string): void { } } -function openUrl(url: string): void { +// platform/spawnImpl are injectable (mirrors sandbox.js's spawnImpl convention) +// so #470's windowsHide behavior is testable per-platform without actually +// spawning a browser. +export function openUrl( + url: string, + opts: { platform?: NodeJS.Platform; spawnImpl?: typeof spawn } = {}, +): void { if (process.env.CI) return; - const cmd = process.platform === "win32" ? "start" - : process.platform === "darwin" ? "open" : "xdg-open"; + const platform = opts.platform ?? process.platform; + const spawnImpl = opts.spawnImpl ?? spawn; + const cmd = platform === "win32" ? "start" + : platform === "darwin" ? "open" : "xdg-open"; try { - // windowsHide suppresses the cmd.exe console window that `shell: true` - // would otherwise briefly flash on win32 before handing off to the browser. - const cp = spawn(cmd, [url], { + // On win32 this runs through `cmd.exe /c start ` (shell: true); without + // windowsHide, that intermediate cmd.exe briefly flashes a visible console + // window before handing off to the actual browser process. + const cp: ChildProcess = spawnImpl(cmd, [url], { stdio: "ignore", detached: true, - shell: process.platform === "win32", + shell: platform === "win32", windowsHide: true, }); cp.on("error", () => {}); diff --git a/src/observability/dashboard/server.js b/src/observability/dashboard/server.js index a0ce2719..450759ee 100644 --- a/src/observability/dashboard/server.js +++ b/src/observability/dashboard/server.js @@ -121,16 +121,19 @@ function safeJson(str) { try { return JSON.parse(str); } catch { return null; } } -function openBrowser(url) { - const cmd = process.platform === 'win32' ? 'start' - : process.platform === 'darwin' ? 'open' : 'xdg-open'; +// platform/spawnImpl are injectable (mirrors sandbox.js's spawnImpl convention) +// so #470's windowsHide behavior is testable per-platform without actually +// spawning a browser. +export function openBrowser(url, { platform = process.platform, spawnImpl = spawn } = {}) { + const cmd = platform === 'win32' ? 'start' + : platform === 'darwin' ? 'open' : 'xdg-open'; try { // windowsHide suppresses the cmd.exe console window that `shell: true` // would otherwise briefly flash on win32 before handing off to the browser. - spawn(cmd, [url], { + spawnImpl(cmd, [url], { stdio: 'ignore', detached: true, - shell: process.platform === 'win32', + shell: platform === 'win32', windowsHide: true, }).unref(); } catch { diff --git a/tests/windows-console-flash-470.test.js b/tests/windows-console-flash-470.test.js new file mode 100644 index 00000000..7bb03c52 --- /dev/null +++ b/tests/windows-console-flash-470.test.js @@ -0,0 +1,78 @@ +/** + * #470: on win32, spawning a browser/runtime process via `cmd.exe` (shell: + * true) or directly (cmd.exe/podman.exe) briefly flashes a visible console + * window unless `windowsHide: true` is passed — Node's spawn() defaults it + * to false. Pins that every one of the four affected spawn call sites + * passes windowsHide on win32, and that non-win32 platforms are unaffected + * (the option is harmless there, but shell must stay false). + * + * owner: RStack developed by Richardson Gunde + */ + +// src/observability/dashboard/server.js has an identical openBrowser copy, +// but that module calls server.listen() unguarded at the top level (a +// pre-existing design predating this PR, out of scope to refactor here) — +// importing it for its openBrowser export would start a real HTTP server as +// a side effect, so it's covered by code-review parity with the two +// call sites below instead of a direct import. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { openBrowser as openBrowserAutoLaunch } from '../src/hooks/auto-launch.js'; +import { openUrl } from '../src/integrations/pi/rstack-sdlc.ts'; +import { startContainerRuntime } from '../src/core/harness/sandbox.js'; + +function fakeChild() { + return { on() {}, unref() {} }; +} + +function recordingSpawn(calls) { + return (cmd, args, options) => { + calls.push({ cmd, args, options }); + return fakeChild(); + }; +} + +test('auto-launch.js openBrowser: windowsHide:true + shell:true on win32', () => { + const calls = []; + openBrowserAutoLaunch('http://localhost:3008', { platform: 'win32', spawnImpl: recordingSpawn(calls) }); + assert.equal(calls.length, 1); + assert.equal(calls[0].cmd, 'start'); + assert.equal(calls[0].options.shell, true); + assert.equal(calls[0].options.windowsHide, true); +}); + +test('auto-launch.js openBrowser: shell:false on macOS/Linux, windowsHide harmless', () => { + const calls = []; + openBrowserAutoLaunch('http://localhost:3008', { platform: 'darwin', spawnImpl: recordingSpawn(calls) }); + assert.equal(calls[0].cmd, 'open'); + assert.equal(calls[0].options.shell, false); +}); + +test('pi/rstack-sdlc.ts openUrl: windowsHide:true + shell:true on win32', () => { + const calls = []; + openUrl('http://localhost:3008', { platform: 'win32', spawnImpl: recordingSpawn(calls) }); + assert.equal(calls[0].cmd, 'start'); + assert.equal(calls[0].options.shell, true); + assert.equal(calls[0].options.windowsHide, true); +}); + +test('pi/rstack-sdlc.ts openUrl: linux uses xdg-open, shell:false', () => { + const calls = []; + openUrl('http://localhost:3008', { platform: 'linux', spawnImpl: recordingSpawn(calls) }); + assert.equal(calls[0].cmd, 'xdg-open'); + assert.equal(calls[0].options.shell, false); +}); + +test('sandbox.js startContainerRuntime: windowsHide:true on the engine auto-start spawn', async () => { + const calls = []; + const result = await startContainerRuntime({ + installedProbe: () => 'docker', + readyProbe: () => false, + spawnImpl: (cmd, args, options) => { calls.push({ cmd, args, options }); return fakeChild(); }, + platform: 'win32', + timeoutMs: 0, + }); + assert.equal(calls.length, 1); + assert.equal(calls[0].options.windowsHide, true); + assert.equal(result.started, true); +}); From 179947fc74240e0adc45436148425a010aeb1a9e Mon Sep 17 00:00:00 2001 From: Richardson Gunde Date: Sun, 26 Jul 2026 16:54:02 +0530 Subject: [PATCH 3/3] Fix CI-only test failure: openUrl no-ops under process.env.CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit openUrl (rstack-sdlc.ts) intentionally early-returns when process.env.CI is set, to avoid popping a browser during automated runs. GitHub Actions always sets CI=true, so the two openUrl tests added in the last commit never actually reached spawnImpl there, while passing locally (CI unset) — caught by PR #471's real CI run, not by local verification. Clear process.env.CI for the duration of those two tests (restored after) so they exercise the real spawn path in CI too, and add a dedicated test pinning the CI no-op behavior itself so it stays covered rather than just being an env quirk the other tests dodge. Verified locally both with and without CI=true set. --- tests/windows-console-flash-470.test.js | 47 ++++++++++++++++++++----- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/tests/windows-console-flash-470.test.js b/tests/windows-console-flash-470.test.js index 7bb03c52..92126599 100644 --- a/tests/windows-console-flash-470.test.js +++ b/tests/windows-console-flash-470.test.js @@ -48,19 +48,50 @@ test('auto-launch.js openBrowser: shell:false on macOS/Linux, windowsHide harmle assert.equal(calls[0].options.shell, false); }); +// openUrl early-returns when process.env.CI is set (intentional — avoids +// popping a browser during a CI run), which GitHub Actions always sets. Clear +// it for the duration of these two tests so they exercise the real spawn +// path there too, restoring it afterward so the guard itself stays covered. +function withoutCiEnv(fn) { + const had = Object.prototype.hasOwnProperty.call(process.env, 'CI'); + const prior = process.env.CI; + delete process.env.CI; + try { + fn(); + } finally { + if (had) process.env.CI = prior; + } +} + test('pi/rstack-sdlc.ts openUrl: windowsHide:true + shell:true on win32', () => { - const calls = []; - openUrl('http://localhost:3008', { platform: 'win32', spawnImpl: recordingSpawn(calls) }); - assert.equal(calls[0].cmd, 'start'); - assert.equal(calls[0].options.shell, true); - assert.equal(calls[0].options.windowsHide, true); + withoutCiEnv(() => { + const calls = []; + openUrl('http://localhost:3008', { platform: 'win32', spawnImpl: recordingSpawn(calls) }); + assert.equal(calls[0].cmd, 'start'); + assert.equal(calls[0].options.shell, true); + assert.equal(calls[0].options.windowsHide, true); + }); }); test('pi/rstack-sdlc.ts openUrl: linux uses xdg-open, shell:false', () => { + withoutCiEnv(() => { + const calls = []; + openUrl('http://localhost:3008', { platform: 'linux', spawnImpl: recordingSpawn(calls) }); + assert.equal(calls[0].cmd, 'xdg-open'); + assert.equal(calls[0].options.shell, false); + }); +}); + +test('pi/rstack-sdlc.ts openUrl: no-ops under CI (never pops a browser during automated runs)', () => { const calls = []; - openUrl('http://localhost:3008', { platform: 'linux', spawnImpl: recordingSpawn(calls) }); - assert.equal(calls[0].cmd, 'xdg-open'); - assert.equal(calls[0].options.shell, false); + const original = process.env.CI; + process.env.CI = '1'; + try { + openUrl('http://localhost:3008', { platform: 'win32', spawnImpl: recordingSpawn(calls) }); + } finally { + if (original === undefined) delete process.env.CI; else process.env.CI = original; + } + assert.equal(calls.length, 0, 'openUrl must not spawn anything when CI is set'); }); test('sandbox.js startContainerRuntime: windowsHide:true on the engine auto-start spawn', async () => {