Skip to content

fix(browse): stop stray Windows console windows during browse#2060

Open
blueverse-hh wants to merge 1 commit into
garrytan:mainfrom
blueverse-hh:fix/windows-hide-detached-console
Open

fix(browse): stop stray Windows console windows during browse#2060
blueverse-hh wants to merge 1 commit into
garrytan:mainfrom
blueverse-hh:fix/windows-hide-detached-console

Conversation

@blueverse-hh

@blueverse-hh blueverse-hh commented Jun 20, 2026

Copy link
Copy Markdown

Problem

During browse use on Windows, stray terminal windows pop up — chrome-headless-shell.exe and bun.exe — some lingering for the browser's lifetime.

Root cause

The browse daemon is launched detached (cli.ts startServer), which on Windows means it has no console. When that console-less daemon spawns console children — chrome-headless-shell.exe (via Playwright), bun skill helpers, the taskkill cleanup — each finds no console to inherit, so Windows allocates a fresh visible console window.

This is not fixable via the user's "Default terminal application" setting (Console Host shows a window too), nor by setting windowsHide on the daemon itself — a detached process has no console regardless, so its children still allocate visible ones.

Verified on Windows 11: a child of a detached/console-less parent is visible without windowsHide and hidden with it. windowsHideCREATE_NO_WINDOW, which suppresses the window even when the parent has no console.

Fix

The daemon defaults windowsHide: true for every child_process spawn, via win-console-hide.ts imported first in server.ts. This covers Playwright's browser launch: playwright-core's processLauncher reads spawn through a live __toESM(require("child_process")).spawn getter at call time, so patching the child_process singleton before the first launch() applies — even though spawn options can't be passed through chromium.launch(). gstack's own Bun.spawn skill helpers and the detached launcher / attack-telemetry spawns set windowsHide directly.

windowsHide is a documented no-op on macOS/Linux.

Verification

Built on Windows 11 and ran browse goto https://example.com:

  • daemon + 4 chrome-headless-shell processes spawned
  • measured via Win32 EnumWindows/IsWindowVisible: zero visible windows for any gstack process (daemon, chrome, bun)

Before the fix the same processes each opened a visible console window.

Adds browse/test/windows-hide-detached-console.test.ts — a static-grep tripwire pinning the windowsHide defaults and the win-console-hide import order so this can't regress.

Note (out of scope)

Several existing source-scanning tests fail on Windows because they derive the source dir via new URL(import.meta.url).pathname (yields /C:/... → doubled-drive C:\C:\... → ENOENT), e.g. terminal-agent-pid-identity.test.ts (verified failing on main). The new test uses import.meta.dir. Happy to send a follow-up switching those over.

🤖 Generated with Claude Code

@trunk-io

trunk-io Bot commented Jun 20, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

The browse daemon is launched detached (cli.ts startServer), which on Windows
gives it NO console. Console children it then spawns -- chrome-headless-shell.exe
via Playwright, `bun` skill helpers, the taskkill cleanup -- find no console to
inherit, so Windows allocates a fresh VISIBLE console window for each. Users see
stray chrome-headless-shell / bun terminal windows during browse use; some linger
for the browser's lifetime. Neither the user's default-terminal choice nor a flag
on the daemon itself fixes it: a detached process has no console regardless.

Verified on Windows 11: a child of a detached/console-less parent is visible
without windowsHide and hidden with it (windowsHide => CREATE_NO_WINDOW, which
suppresses the window even when the parent has no console).

Fix: the daemon defaults windowsHide:true for every child_process spawn
(win-console-hide.ts, imported first in server.ts so it patches the
child_process singleton before Playwright's first launch()). playwright-core
reads spawn via a live `__toESM(require("child_process")).spawn` getter at call
time, so patching the singleton covers its browser launch even though spawn
options can't be passed through chromium.launch(). gstack's own Bun.spawn skill
helpers and the detached launcher / attack-telemetry spawns set windowsHide
directly.

End-to-end: `browse goto` now starts the daemon and 4 chrome-headless-shell
processes with zero visible windows.

Adds a static-grep tripwire so the flag and import order can't regress.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@blueverse-hh blueverse-hh force-pushed the fix/windows-hide-detached-console branch from 96caef8 to a8b5bc5 Compare June 20, 2026 03:05
@blueverse-hh blueverse-hh changed the title fix(browse): hide console window on Windows detached spawns fix(browse): stop stray Windows console windows during browse Jun 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant