Windows: /browse daemon spawns bun without windowsHide → console window flashes and steals focus
- Dominant language
- TypeScript
- Stars
- 133k
- Forks
- 19.9k
- Avg merge
- 18h 46m
- Merged PRs (30d)
- 26
Description
## Summary
On **Windows**, the `/browse` daemon repeatedly pops a **console window that steals foreground focus** (interrupting typing and voice dictation). The window shows the path of the WinGet `bun.exe` shim, e.g. `...\WinGet\Links\bun.exe`.
## Environment
- Windows 11
- gstack `1.51.0.0`
- `bun` installed via WinGet (a console-subsystem exe on PATH as `...\Microsoft\WinGet\Links\bun.exe`)
- Browse daemon running under the Node fallback (`browse/dist/server-node.mjs` + `browse/dist/bun-polyfill.cjs`), which is the Windows path since Bun can't drive Playwright's Chromium on Windows.
## Root cause
On Windows the browse server runs under Node with `browse/src/bun-polyfill.cjs` shimming the Bun API. Its `Bun.spawn` / `Bun.spawnSync` implementations call Node's `child_process.spawn` / `spawnSync` **without `windowsHide: true`**:
```js
spawn(cmd, options = {}) {
const [command, ...args] = cmd;
const proc = spawn(command, args, {
stdio,
env: options.env,
cwd: options.cwd,
// no windowsHide <-- here
});
...
}
```
`spawnTerminalAgent` (`browse/src/terminal-agent-control.ts`) launches `bun run terminal-agent.ts` through this shim. Because `bun` resolves to a **console** executable, Windows allocates a fresh console window (conhost) for every spawn, which pops up and grabs foreground.
It recurs continuously because the agent-health **watchdog** in `browse/src/server.ts` (~line 1481, 60s tick) respawns the terminal-agent whenever its recorded PID dies. A daemon left over from an earlier `/browse` keeps flashing even with no browse session open.
## Impact
Constant focus theft breaks typing and speech-to-text on Windows. `HKCU\Control Panel\Desktop\ForegroundLockTimeout` only softens the focus grab; the window still pops and flickers over the terminal.
## Fix
Pass `windowsHide: true` in the options for both `spawn` and `spawnSync` in `browse/src/bun-polyfill.cjs` (then rebuild the `dist` bundle):
```diff
const proc = spawn(command, args, {
stdio,
env: options.env,
cwd: options.cwd,
+ windowsHide: true,
});
```
```diff
const result = spawnSync(command, args, {
stdio: [ ... ],
timeout: options.timeout,
env: options.env,
cwd: options.cwd,
+ windowsHide: true,
});
```
`windowsHide: true` sets `CREATE_NO_WINDOW`, so no console is ever allocated. This also silences the version-probe `spawnSync` calls (e.g. `chrome --version`). Verified locally: after the patch, restarting the browse daemon eliminates the flashing entirely with no functional change.
## Repro
1. On Windows, install `bun` via WinGet.
2. Run any `/browse`-backed skill (or just leave a browse daemon running).
3. Observe console windows citing `...\WinGet\Links\bun.exe` popping up and stealing focus, especially on the 60s watchdog respawn.
Contributor guide
Research direction
Start in browse/src/bun-polyfill.cjs and inspect the Bun.spawn and Bun.spawnSync implementations, then review browse/src/terminal-agent-control.ts for their use. Rebuild the browse dist bundle after the change and verify on Windows that /browse no longer flashes console windows during terminal-agent respawns or version probes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- bun, node.js, typescript
- Domain
- tooling
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100