garrytan / garrytan/gstack

Windows: browse cold-start flashes focus-stealing console windows (Bun.spawnSync ignores windowsHide)

Open
#1,784 2 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
133k
Forks
19.9k
Avg merge
18h 46m
Merged PRs (30d)
26

Description

## Windows: `browse` cold-start flashes focus-stealing console windows (`Bun.spawnSync` ignores `windowsHide`)

### Summary
On Windows 11, every **cold start** of `browse` (i.e. when no healthy daemon is already running) briefly flashes **2+ focus-stealing console windows** on the interactive desktop. They steal foreground focus and interrupt typing in other apps. The cause is several console-subsystem child processes spawned without an *effective* `windowsHide`.

### Root cause (verified with an elevated `Win32_ProcessStartTrace`)
Tracing process creation during a cold `browse goto` shows the flashing `conhost.exe` windows are parented to:

| spawned process | spawned by | call site |
|---|---|---|
| `node.exe` (the detach launcher) | `browse.exe` | `browse/src/cli.ts` `startServer()` — `Bun.spawnSync(['node','-e',launcherCode], …)` |
| `git.exe` (`rev-parse --show-toplevel`) | `browse.exe` | `browse/src/config.ts` `getGitRoot()` and `browse/src/find-browse.ts` — `Bun.spawnSync(['git', …])` |
| `icacls.exe` ×2 | daemon `node` | `browse/src/file-permissions.ts` `restrictFilePermissions` / `restrictDirectoryPermissions` — `execFileSync('icacls', …, { stdio:'ignore' })` (no `windowsHide`) |

### The key finding
**`Bun.spawnSync` does not honor the `windowsHide` option on Windows** (tested on bun 1.3.11). Adding `windowsHide: true` to a `Bun.spawnSync(...)` call has no effect — the child still allocates a console window.

Node's `child_process` (`spawn` / `spawnSync` / `execFileSync`) **does** honor `windowsHide`. (Confirmed in the same trace: the detached daemon `node`, spawned via Node's `child_process.spawn({ detached:true, windowsHide:true })` inside `launcherCode`, did **not** get a console window — only the `Bun.spawnSync`-spawned launcher and git did.)

So the fix is: for Windows console spawns, use Node's `child_process` with `windowsHide:true` rather than `Bun.spawnSync`, and add `windowsHide:true` to the `execFileSync('icacls', …)` calls.

### Suggested fix
```diff
# browse/src/cli.ts
-import { spawn as nodeSpawn } from 'child_process';
+import { spawn as nodeSpawn, spawnSync as nodeSpawnSync } from 'child_process';
@@ startServer() (Windows branch)
- `{detached:true,stdio:['ignore','ignore','ignore'],env:Object.assign({},process.env,` +
+ `{detached:true,windowsHide:true,stdio:['ignore','ignore','ignore'],env:Object.assign({},process.env,` +
`${extraEnvStr})}).unref()`;
- Bun.spawnSync(['node', '-e', launcherCode], { stdio: ['ignore', 'ignore', 'ignore'] });
+ // Bun.spawnSync ignores windowsHide on Windows -> the launcher node flashes a
+ // console. Node's spawnSync honors it.
+ nodeSpawnSync('node', ['-e', launcherCode], { stdio: ['ignore', 'ignore', 'ignore'], windowsHide: true });

# browse/src/config.ts (and browse/src/find-browse.ts likewise)
+import { execFileSync } from 'child_process';
- const proc = Bun.spawnSync(['git', 'rev-parse', '--show-toplevel'], { stdout:'pipe', stderr:'pipe', timeout:2_000 });
- if (proc.exitCode !== 0) return null;
- return proc.stdout.toString().trim() || null;
+ // Node's execFileSync honors windowsHide; Bun.spawnSync does not.
+ const out = execFileSync('git', ['rev-parse', '--show-toplevel'],
+ { encoding:'utf-8', timeout:2_000, windowsHide:true, stdio:['ignore','pipe','ignore'] });
+ return out.trim() || null;

# browse/src/file-permissions.ts (both execFileSync('icacls', …) calls)
- { stdio: 'ignore' },
+ { stdio: 'ignore', windowsHide: true },
```

The detached daemon spawn just needs the `windowsHide:true` added to its options object (Node honors it).

### Repro
1. Windows 11, no `browse` daemon running.
2. `browse goto https://example.com`.
3. Observe 2+ console windows flash and steal focus. An elevated `Win32_ProcessStartTrace` shows the `conhost.exe` parents are `node` (launcher), `git`, and `icacls`.

### Environment
- Windows 11, gstack @ `920a13a`, bun 1.3.11, Node from `C:\Program Files\nodejs`.
- Default headless path. (Separately, `chrome-headless-shell.exe` is a console-subsystem binary; launching full chromium with `--headless=new` avoids that one too, but it was not the cause of these particular flickers.)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.