Symptom
While the browse daemon is alive, a console window titled C:\Users\<user>\.bun\bin\bun.exe
appears repeatedly and takes foreground focus. On Windows a newly created console window
steals keyboard input, so it interrupts whatever the user is typing in another application.
It recurs rather than happening once, which makes browse effectively unusable on an
attended desktop.
Environment
- Windows 11 Pro (26200), Git Bash / MSYS2
browse build 11de390be1be6849eb9a15f91ff4922dd16c589a
- Headless (
launched) mode — not --headed
Root cause
src/terminal-agent-control.ts, in spawnTerminalAgent():
const proc = (Bun as any).spawn(['bun', 'run', script], {
cwd: opts.cwd || process.cwd(),
env: { ...process.env, BROWSE_STATE_FILE: opts.stateFile, /* ... */ },
stdio: ['ignore', 'ignore', 'ignore'],
});
proc.unref?.();
The call already passes stdio: ['ignore','ignore','ignore'] and calls unref(), but there
is no windowsHide option — and on Windows, redirecting stdio does not stop the OS from
allocating a console window for the child. The watchdog then respawns the agent whenever it
exits, which reproduces the window each time and is why closing it doesn't help.
Confirmed by process inspection — the window belongs to a direct child of the daemon, not to
the daemon itself:
ProcessId : 55380
ParentProcessId : 51412
CommandLine : bun run <...>\browse\src\terminal-agent.ts
ProcessId : 51412
Name : node.exe
CommandLine : node.exe <...>\browse\dist\server-node.mjs
This matters for anyone debugging it: killing the visible bun.exe by name leaves the real
node.exe daemon alive to spawn another within seconds, which looks exactly like "it keeps
popping back up no matter how many times I close it."
Why this looks like an oversight rather than a design choice
src/cli.ts already handles precisely this class of problem for the server launch, with a
comment saying so (~lines 318–328): it notes that Bun.spawn() + proc.unref() doesn't
truly detach on Windows, and routes the launch through Node's child_process.spawn with
{detached: true, stdio: ['ignore','ignore','ignore']} instead. The terminal-agent spawn
never got the same treatment.
Suggested fix
Add windowsHide: true to the spawn options. If Bun.spawn does not honour windowsHide
on Windows, route this spawn through Node's child_process.spawn the same way cli.ts
already does for the server:
import { spawn as nodeSpawn } from 'child_process';
nodeSpawn(process.execPath, [script], {
detached: true,
stdio: ['ignore', 'ignore', 'ignore'],
windowsHide: true, // <-- suppresses the console window
env: { /* ... */ },
}).unref();
Two other Bun.spawn call sites spawn bun as a child and look like they'd have the same
behaviour on Windows, worth auditing in the same pass:
src/browser-skill-commands.ts:188 — Bun.spawn(['bun', 'test', testFile], …)
src/browser-skill-commands.ts:266 — Bun.spawn(['bun', 'run', scriptPath, …], …)
Workaround
browse stop tears down both the daemon and the bun child, which stops the popups.
Secondary, minor
browse stop prints:
[browse] Unable to connect. Is the computer able to access the url?
and still succeeds — both processes do exit (verified by checking for the server-node.mjs
and bun.exe processes immediately afterward; nothing matched). The message reads like a
failure and invites a pointless retry or a manual hunt for surviving processes. It looks like
stop is being routed through the goto path and treated as a URL. Suggest suppressing that
line, or printing an explicit stopped confirmation instead.
Symptom
While the
browsedaemon is alive, a console window titledC:\Users\<user>\.bun\bin\bun.exeappears repeatedly and takes foreground focus. On Windows a newly created console window
steals keyboard input, so it interrupts whatever the user is typing in another application.
It recurs rather than happening once, which makes
browseeffectively unusable on anattended desktop.
Environment
browsebuild11de390be1be6849eb9a15f91ff4922dd16c589alaunched) mode — not--headedRoot cause
src/terminal-agent-control.ts, inspawnTerminalAgent():The call already passes
stdio: ['ignore','ignore','ignore']and callsunref(), but thereis no
windowsHideoption — and on Windows, redirecting stdio does not stop the OS fromallocating a console window for the child. The watchdog then respawns the agent whenever it
exits, which reproduces the window each time and is why closing it doesn't help.
Confirmed by process inspection — the window belongs to a direct child of the daemon, not to
the daemon itself:
This matters for anyone debugging it: killing the visible
bun.exeby name leaves the realnode.exedaemon alive to spawn another within seconds, which looks exactly like "it keepspopping back up no matter how many times I close it."
Why this looks like an oversight rather than a design choice
src/cli.tsalready handles precisely this class of problem for the server launch, with acomment saying so (~lines 318–328): it notes that
Bun.spawn()+proc.unref()doesn'ttruly detach on Windows, and routes the launch through Node's
child_process.spawnwith{detached: true, stdio: ['ignore','ignore','ignore']}instead. The terminal-agent spawnnever got the same treatment.
Suggested fix
Add
windowsHide: trueto the spawn options. IfBun.spawndoes not honourwindowsHideon Windows, route this spawn through Node's
child_process.spawnthe same waycli.tsalready does for the server:
Two other
Bun.spawncall sites spawnbunas a child and look like they'd have the samebehaviour on Windows, worth auditing in the same pass:
src/browser-skill-commands.ts:188—Bun.spawn(['bun', 'test', testFile], …)src/browser-skill-commands.ts:266—Bun.spawn(['bun', 'run', scriptPath, …], …)Workaround
browse stoptears down both the daemon and thebunchild, which stops the popups.Secondary, minor
browse stopprints:and still succeeds — both processes do exit (verified by checking for the
server-node.mjsand
bun.exeprocesses immediately afterward; nothing matched). The message reads like afailure and invites a pointless retry or a manual hunt for surviving processes. It looks like
stopis being routed through thegotopath and treated as a URL. Suggest suppressing thatline, or printing an explicit
stoppedconfirmation instead.