Skip to content

fix(desktop): avoid uv venv pythonw launcher console flash on Windows - #53344

Closed
luntion wants to merge 1 commit into
NousResearch:mainfrom
luntion:fix/desktop-uv-venv-pythonw-shim-flash
Closed

fix(desktop): avoid uv venv pythonw launcher console flash on Windows#53344
luntion wants to merge 1 commit into
NousResearch:mainfrom
luntion:fix/desktop-uv-venv-pythonw-shim-flash

Conversation

@luntion

@luntion luntion commented Jun 27, 2026

Copy link
Copy Markdown

fix(desktop): avoid uv venv pythonw launcher console flash on Windows

Summary

On Windows, the Hermes desktop app spawns the dashboard backend via
venv\Scripts\pythonw.exe. When the venv is created by uv (which is
how every standard install.ps1 install builds it), that path is a uv
launcher shim — not a real pythonw.exe. The shim re-execs the base
console
python.exe, so a cmd.exe window flashes every time the
desktop spawns or respawns a backend. Setting windowsHide: true on the
parent spawn can't suppress this — the second process allocates its own
conhost.

This PR teaches apps/desktop/electron/main.cjs to detect uv venvs and
fall back to the base interpreter's pythonw.exe directly, matching
what hermes_cli/gateway_windows.py::_resolve_detached_python already
does for the CLI gateway path (landed in #41028).

Symptoms

  • After updating to recent versions, users see brief cmd.exe windows
    flashing every few minutes during normal use.
  • Especially visible with multiple profiles: the desktop's idle reaper
    evicts each profile backend after 600s, so every profile switch /
    reactivation re-spawns and re-flashes.

desktop.log shows the spawn loop clearly:

[hermes] Starting Hermes backend for profile "jiadao" via Hermes at ...
[hermes] HERMES_DASHBOARD_READY port=63759
[hermes] Reaping idle profile backend "nanchong" (idle > 600s)
[hermes] Starting Hermes backend for profile "nanchong" via ...

Each Starting Hermes backend line = one console flash.

Root cause

getNoConsoleVenvPython(venvRoot) returned venv\Scripts\pythonw.exe
unconditionally if the file existed. On a uv-managed venv that file is
a ~44KB launcher shim that internally spawns the base python.exe
(console subsystem), not the base pythonw.exe. That second process
opens a visible conhost regardless of CREATE_NO_WINDOW /
windowsHide on the parent.

Confirmed via pyvenv.cfg:

home = C:\Users\<u>\AppData\Roaming\uv\python\cpython-3.11.13-windows-x86_64-none
uv = 0.11.21

The base pythonw.exe exists at <home>\pythonw.exe (~90KB, real GUI-
subsystem binary).

The CLI gateway hit the exact same bug and was fixed in #41028
_resolve_detached_python() detects uv via pyvenv.cfg and switches
to the base pythonw.exe, then adds the venv's Lib\site-packages
to PYTHONPATH so imports still resolve. This PR ports that fix to the
desktop spawn path.

Changes

apps/desktop/electron/main.cjs

  1. Refactor readVenvHome() to share a common readPyvenvCfg()
    helper (no behaviour change).

  2. Add isUvVenv(venvRoot) — checks for uv = line in pyvenv.cfg
    (matches the same detection used by the CLI side).

  3. getNoConsoleVenvPython(): when the venv is uv-created, prefer
    the base interpreter's pythonw.exe (read from pyvenv.cfg.home)
    instead of venv\Scripts\pythonw.exe. Non-uv venvs (stdlib venv,
    virtualenv, etc.) keep the old behaviour — they ship a real
    pythonw.exe.

  4. createPythonBackend() and createActiveBackend(): include
    getVenvSitePackagesEntries(venvRoot) in pythonPathEntries.
    When we bypass the uv launcher, the base interpreter has no venv
    awareness, so we must put the venv's Lib\site-packages on
    PYTHONPATH for imports to work. For non-uv venvs this is a
    harmless duplicate entry (the venv's own pythonw.exe adds
    site-packages on startup, and appendUniquePathEntries dedupes).

    This matches what unwrapWindowsVenvHermesCommand() already does
    for the Windows venv hermes.exe path — the new behaviour brings
    createPythonBackend / createActiveBackend into parity.

apps/desktop/electron/windows-child-process.test.cjs

Lock in the new uv-detection logic and PYTHONPATH wiring via
source-pattern assertions, consistent with the existing test style.

Why source-pattern tests (not a behaviour test)

Existing helper modules in electron/*.cjs are independent CommonJS
modules with their own behaviour tests (backend-env.test.cjs,
update-relaunch.test.cjs, etc.). The Windows-spawn helpers
(getNoConsoleVenvPython, applyWindowsNoConsoleSpawnHints,
createPythonBackend, createActiveBackend) currently live inline in
main.cjs and are not exported, so the existing
windows-child-process.test.cjs covers them via source-string
matching.

A follow-up could extract these helpers to a venv-resolver.cjs module
with a real fixture-based behaviour test (build a fake uv pyvenv.cfg

  • fake base pythonw.exe, assert which path is returned). Happy to do
    that in this PR if reviewers prefer — kept it minimal here to match
    the surrounding test conventions and minimise unrelated diff.

Testing

Locally on Windows 10 (the reporter's environment):

cd apps/desktop
node --test electron/windows-child-process.test.cjs

The two relevant tests pass:

✔ desktop background child processes opt into hidden Windows consoles
✔ intentional or interactive desktop child processes stay documented

(bootstrap PowerShell runner hides Windows console children fails
on plain main too — pre-existing and unrelated to this PR.)

Relationship to other issues / PRs

Risk / blast radius

  • Windows-only code path (IS_WINDOWS guard preserved).
  • For non-uv venvs: no behaviour change.
  • For uv venvs (the default install layout): switches command from
    venv\Scripts\pythonw.exe (shim) to base pythonw.exe, and adds
    the venv Lib\site-packages to PYTHONPATH. The CLI gateway has
    been doing exactly this since fix(gateway): avoid Windows uv pythonw launcher console #41028 in production without
    regressions, so the import resolution side of it is proven.
  • Falls back to the old behaviour if pyvenv.cfg is missing or
    unreadable, or if the resolved base pythonw.exe doesn't exist.

The desktop spawns the dashboard backend via venv\Scripts\pythonw.exe.
For uv-created venvs that path is a launcher shim that re-execs the
base CONSOLE python.exe, flashing a cmd window on every spawn even
with windowsHide / CREATE_NO_WINDOW on the parent. The CLI gateway
hit the same bug and was fixed in NousResearch#41028 by detecting uv via
pyvenv.cfg and using the base pythonw.exe directly with venv
site-packages on PYTHONPATH. This ports the same fix to the desktop
spawn path.

Changes:
- Refactor pyvenv.cfg reading into readPyvenvCfg(); add isUvVenv().
- getNoConsoleVenvPython(): for uv venvs, prefer base pythonw.exe over
  the venv launcher shim. Non-uv venvs keep the old behaviour.
- createPythonBackend / createActiveBackend: add venv Lib/site-packages
  to PYTHONPATH so the base interpreter can still resolve imports when
  we bypass the venv launcher (harmless dedup for non-uv venvs).
- Lock the new logic via source-pattern assertions in the existing
  windows-child-process.test.cjs.

Closes NousResearch#53016.
@alt-glitch alt-glitch added type/bug Something isn't working comp/desktop Electron desktop app (apps/desktop/*) platform/windows Native Windows-specific behavior or breakage P2 Medium — degraded but workaround exists sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows labels Jun 27, 2026
@luntion luntion closed this Jun 28, 2026
@luntion
luntion deleted the fix/desktop-uv-venv-pythonw-shim-flash branch June 28, 2026 09:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/desktop Electron desktop app (apps/desktop/*) P2 Medium — degraded but workaround exists platform/windows Native Windows-specific behavior or breakage sweeper:risk-platform-windows Sweeper risk: may break or behave differently on native Windows type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Windows desktop app flashes cmd console window intermittently after update

2 participants