Skip to content

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

@kaiwulff

Description

@kaiwulff

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.tsBun.spawnSync(['git', …])
icacls.exe ×2 daemon node browse/src/file-permissions.ts restrictFilePermissions / restrictDirectoryPermissionsexecFileSync('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

# 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.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions