Skip to content

Keep the GUI-subsystem node out of every pane's PATH - #697

Merged
nedtwigg merged 5 commits into
mainfrom
dev-pane-node-path
Sep 17, 2026
Merged

nedtwigg merged 5 commits into
mainfrom
dev-pane-node-path

Conversation

@nedtwigg

Copy link
Copy Markdown
Member

On Windows, node in a pnpm dev:standalone pane resolved to the bundled
node.exe in standalone/src-tauri/target/debug, which build.rs patches to
the GUI subsystem so spawning the sidecar never flashes a console window.

A child of a GUI-subsystem node gets no console at all, and the symptoms are
silent in both directions:

  • process.stdin.isTTY is undefined, stdin is already at EOF, and
    setRawMode does not exist — so a script that reads the terminal dies at
    startup.
  • stdout is not attached either, so it cannot even report why.

cargo run puts that directory on the dev app's PATH (that is how Windows
finds the app's DLLs) and panes inherit the app's env, so the patched node.exe
sat ahead of the developer's own. Measured in a dev pane before this change:

binary PATH position isTTY setRawMode PE subsystem
target/debug/node.exe 4 undefined undefined 2 (GUI)
node_modules/.bin/node.EXE 8 true function 3 (console)
.version-fox/sdks/nodejs/node.exe 35 true function 3 (console)

The fix

start_sidecar passes the directory as DORMOUSE_GUI_NODE_DIR, and the
sidecar drops it from the PATH each pane inherits. It goes in the same chain as
the existing DORMOUSE_CLI_BIN prepend and ORIGINAL_PATH scrub, and the
variable itself is stripped from the pane env like the other internal ones.

The host names the directory rather than the sidecar deriving it, because
process.execPath is the wrong answer twice over: standalone/scripts/dev-agent-browser.mjs
runs the sidecar under the developer's own node, and this module is also the VS
Code pty host (vscode-ext/src/pty-host.js), where the running node's
directory — /usr/bin, say — must stay on PATH. The strip is win32-only for
the same reason.

Scope

Dev builds only. The installed app ships the same GUI-subsystem node.exe,
but neither its install directory nor any target/debug is on the persistent
user or machine PATH that a shortcut-launched app inherits, and panes add only
DORMOUSE_CLI_BIN — so node there was already the developer's own. Verified
against Dormouse Terminal 0.11.0 installed on this machine. The fix makes a dev
pane match an installed one rather than adding a special case.

Verification

  • cargo check clean (the two warnings are pre-existing, elsewhere in lib.rs).
  • Three new cases in standalone/sidecar/pty-core.test.js cover the strip, path
    spelling (case, / vs \, trailing separator), and that non-win32 keeps the
    directory. All pass.
  • pnpm lint:specs OK. docs/specs/standalone.md gained the rule under
    "Windows node subsystem" and was re-baselined with --ratchet in this PR.

Not verified in a running dev app: the sidecar has to be rebuilt to pick this
up, so the end-to-end check is node in a fresh dev pane resolving to the
developer's own node.

Pre-existing Windows test failures, untouched by this PR

Confirmed identical on pristine origin/main, so left alone:

  • standalone/sidecar/pty-core.test.js — 3 shell-integration cases assert
    POSIX-separator paths that path.join renders with \ on Windows.
  • standalone/scripts/dev-standalone.test.mjs — all 3 cases fail.

🤖 Generated with Claude Code

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 17, 2026

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 5142a72
Status: ✅  Deploy successful!
Preview URL: https://f83fe993.mouseterm.pages.dev
Branch Preview URL: https://dev-pane-node-path.mouseterm.pages.dev

View logs

On Windows the bundled node.exe is patched to the GUI subsystem so spawning
the sidecar never flashes a console window. A child of that node gets no
console at all: stdin is already at EOF, `setRawMode` is missing, and
everything it prints is dropped.

`cargo run` puts the directory holding it on the dev app's PATH so Windows
resolves the app's DLLs, and panes inherit the app's env -- so a bare `node`
in a dev pane found that node.exe ahead of the developer's own and failed
silently in both directions. The installed app never has that directory on
PATH, so this only ever bit dev builds, and dropping the entry makes a dev
pane match an installed one.

`start_sidecar` passes the directory as DORMOUSE_GUI_NODE_DIR and the sidecar
strips it from the PATH each pane inherits. The host names the directory
because `process.execPath` is the wrong answer twice over: the agent-browser
harness runs the sidecar under the developer's own node, and the same module
runs under the VS Code pty host, where the running node's directory must stay.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feedback on work in progress, not a merge verdict — mark the PR ready when you want the full review.

The PATH-position/subsystem table and the "verified against 0.11.0 installed" scope check are what make this rule credible, and right now they live only in the PR body. AGENTS.md ("Specs") routes exactly that — evidence and measurements — to docs/specs/standalone.rationale.md, keyed by the spec's heading and marked (rationale) at the rule it backs; the two sibling bullets in this same section already do it, and ## Windows node subsystem exists there to hang it on. Without that, the next person to touch the strip has no record of why the installed app was ruled out, and re-derives it from a Windows box. The inline suggestion trims the bullet to match the siblings, assuming the evidence moves.

Comment thread docs/specs/standalone.md Outdated
Comment thread standalone/src-tauri/src/lib.rs Outdated
nedtwigg and others added 3 commits September 17, 2026 15:50
Per review: AGENTS.md routes measurements and dead approaches to
`<foo>.rationale.md`, keyed by the spec heading and marked `(rationale)` at
the rule. The PATH-position/subsystem table and the ruling-out of the
installed app lived only in the PR body, where the next person to touch the
strip would not find them; they now sit under `## Windows node subsystem` in
`docs/specs/standalone.rationale.md`, along with why `process.execPath` is the
wrong way to name the directory.

`node_path` comes back from `resolve_node_binary_path` as `dir.join(...)`, so
`parent()` is always `Some` and the `unwrap_or_default()` arm was dead.
Passing the `&Path` straight to `Command::env` also drops a `to_string_lossy`
round-trip that could have handed the sidecar a spelling no longer matching
the real directory, silently turning the strip into a no-op.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
spec-lint check 4 resolves any backticked repo-relative path, and the dev
app's target dir is not in SKIP_PATH_PREFIXES -- so the rationale's
`standalone/src-tauri/target/debug/node.exe` passed here, where the artifact
exists, and failed CI on a clean checkout. Describe the binary by where it
sits instead of spelling a path that only exists after a build; the skip list
is for references the specs need, not for prose that can be reworded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
145 added lines for dropping one PATH entry was too much. No behaviour change,
and every case the tests pinned is still pinned.

- Fold `normalizeWindowsPathEntry` into a local. The trim and quote-strip were
  dead: the only entry that has to match is the one `cargo run` appended, which
  is neither padded nor quoted. Case folding stays and is load-bearing, not
  just test-pinned -- cargo builds its entry from the workspace path as typed,
  while the host derives the directory from `current_exe()`, so the two can
  differ in case.
- Collapse four guards to one. `!target` was unreachable given `dir` is truthy,
  and `!existing` folds into the first condition.
- Cut the comment from 16 lines to 9. The measurements and the installed-app
  ruling-out are in the rationale now; what stays is the mechanism an editor of
  this module needs, and the one sentence that stops a future simplification
  from reaching for `process.execPath`.
- Merge the two win32 tests. They were one rule plus two facts about matching:
  spelling the target entry with `/`, a trailing separator and a different case
  pins all three at once, next to the `debug\deps` sibling that pins no prefix
  matching.
- Hoist `paneEnv` so the env chain keeps its original shape.

Also corrects a reason I had wrong: the comment and the non-win32 test claimed
`DORMOUSE_GUI_NODE_DIR` could name a directory a pane needs, like `/usr/bin`.
Only `start_sidecar` ever sets it, always to the bundled node's directory --
that hazard belongs to the rejected `process.execPath` approach. The gate is
still right, for the real reason: on Unix the app's directory is never on PATH.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feedback on work in progress, not a merge verdict — mark the PR ready when you want the full review.

The cut-down reads well and the evidence landed cleanly in the rationale. The rewrite did introduce one thing worth fixing before this goes out: the comment on withoutGuiNodeDir names the wrong mechanism. It says a child of a GUI-subsystem process gets no console at all — but the pane's shell is exactly that child (the sidecar runs under the GUI node and node-pty hands the shell a ConPTY), and it has a console and works. What actually fails is the patched node.exe itself: a GUI-subsystem binary does not attach to an inherited console. That is what resolve_dor_node_path in standalone/src-tauri/src/lib.rs already says — "A GUI-subsystem node, however, does not attach to an inherited console" — and what this spec section's dor bullet repeats.

The consequence is that the comment points a future editor at parentage: read it literally and the strip looks removable by spawning panes from a console-subsystem process, which would not help. The same sentence is in the win32 test's comment. The spec bullet and both rationale paragraphs state it correctly, so only these two need the edit.

Comment thread standalone/sidecar/pty-core.js Outdated
Comment thread standalone/sidecar/pty-core.test.js Outdated
Per review, the rewritten comment said a *child* of a GUI-subsystem process
gets no console. That is wrong, and this branch's own measurements disprove it:
the pane's shell is exactly such a child -- the sidecar runs under the GUI node
and node-pty hands the shell a ConPTY -- and it works, as does the
console-subsystem node.exe from `node_modules/.bin`, which reported
`isTTY` true from inside the same pane.

What fails is the patched binary itself: a GUI-subsystem binary does not attach
to an *inherited* console, which is what `resolve_dor_node_path` and this
section's `dor` bullet already say. Read the old wording literally and the strip
looks removable by spawning panes from a console-subsystem process, which would
not help at all.

The spec bullet and both rationale paragraphs already stated it correctly; only
the code comment and the win32 test's comment needed the edit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nedtwigg
nedtwigg marked this pull request as ready for review September 17, 2026 23:53
@nedtwigg
nedtwigg merged commit 795092f into main Sep 17, 2026
12 checks passed
@nedtwigg
nedtwigg deleted the dev-pane-node-path branch September 17, 2026 23:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants