OSC 633 shell integration for PowerShell#147
Conversation
Inject OSC 633 prompt/command boundaries into pwsh and Windows powershell.exe, the same way bash and zsh already get them. - New emitter shellIntegration.ps1 wraps the user's `prompt` function to emit the A/B/C/D/E/P;Cwd sequences. PowerShell has no preexec hook, so (like VS Code's integration) a command's E/C/D are reported from the next prompt render — command line from history, exit code from $?/$LASTEXITCODE. Boundaries and exit codes are exact. The escaping matches the bash/zsh emitters; Cwd is sent raw since the parser reads it verbatim. - pty-core.js: inject via `-NoExit -Command ". '<script>'"` (profile still loads, so the user's prompt is defined before we wrap it). Skipped when the caller passed explicit args, like bash. Added a shellStem() helper that handles `\` separators and `.exe` so Windows shell paths match; resolveLoginArg reuses it. - Tests for pwsh and Windows PowerShell injection, explicit-args passthrough, and missing-script fallback. - terminal-escapes.md: mark PowerShell implemented. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deploying mouseterm with
|
| Latest commit: |
3ebbf5e
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://16f2df33.mouseterm.pages.dev |
| Branch Preview URL: | https://powershell-osc633.mouseterm.pages.dev |
dormouse-bot
left a comment
There was a problem hiding this comment.
One correctness issue, verified by simulating the history-id state machine in pwsh: the first interactive command of every session never gets its E/C/D reported.
The -1 sentinel is meant to suppress only the genuine first prompt, but it lingers through that first render because $Global:__dormouse_633_last_history_id is assigned only when history is non-empty. At the very first prompt Get-History is empty, so the sentinel stays -1; when the next prompt renders after the user's first command, the guard ... -ne -1 is still false and the command's line/output-start/exit-code are dropped. From the second command onward it works. (This is easy to miss in manual testing — by the time you're inspecting output you've already run several commands.) VS Code's PowerShell integration avoids this by assigning the id unconditionally each render. Inline suggestion below; I confirmed it restores reporting for the first command while still suppressing empty-line / Ctrl+C renders.
Secondary, non-blocking: by the time the user's original prompt is invoked ($Global:__dormouse_633_original_prompt.Invoke()), $? has been clobbered by the intervening Get-History and assignments, so it is always $True there. $LASTEXITCODE survives (nothing reassigns it), so starship-style prompts are fine, but a prompt that colors itself off $? will always see success. Restoring $? before the invoke is awkward in PowerShell — worth a note rather than a required change.
|
@dormouse-bot fix and commit |
The -1 first-prompt sentinel only cleared when Get-History was non-empty, so it lingered through the genuine first prompt (history empty) and caused the first real command's line/output-start/exit-code to be dropped; from the second command onward it worked. Assign the history id unconditionally each render (0 when history is still empty) so the sentinel clears after the first prompt — history ids start at 1, so 0 never matches a real command and the genuine first prompt's report stays suppressed. Verified in pwsh by driving the prompt state machine: the first command now emits E/C/D while the first prompt and empty-line renders stay silent. Also documents that $? is always $True when the user's original prompt is invoked (Get-History clobbers it); $LASTEXITCODE survives, so starship-style prompts are unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Done — fixed in ed788e3. The first interactive command's |
Emitting E/C/D all from the next prompt meant the running command never showed — it appeared to start and finish at once, after it had already completed. Wrap PSReadLine's PSConsoleHostReadLine (called just before a submitted command runs) to emit E (command line) and C (command start) up front, matching bash/zsh preexec; D (finish + exit code) still comes from the next prompt like a zsh precmd. Falls back to the next-prompt E/C/D triple when PSReadLine is absent. The hook is retried from prompt() since PSReadLine may import after the script dot-sources. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Git Bash is detected with args ['--login', '-i'], which tripped the "explicit args" guard and skipped bash --init-file injection, so it fell back to the keystroke heuristic and the command title never updated. Those flags only select an interactive login shell, which the init-file script already subsumes (it sources the login profile itself — verified that bash.exe accepts the Windows-path --init-file and emits the full 633 sequence). Inject whenever bash's args are only interactive/login flags; keep skipping specific invocations like `-c <cmd>`. Fail-safe preserved: if the script is missing, the original --login -i args survive. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
"Developer PowerShell for VS" is detected as
`-NoExit -Command "& { Import-Module ... }"`, whose explicit args made the
blunt hasExplicitArgs guard skip OSC 633 injection entirely.
Key the decision on interactivity and argument structure instead of "are
there args": for an interactive PowerShell launch (-NoExit), append our
dot-source to an existing -Command (so the dev-shell setup runs first and
our prompt wrapper installs after it) or add one if absent; leave
non-interactive one-offs (-Command/-File/-EncodedCommand without -NoExit)
untouched. Verified the merged `& { ... }; . 'script'` command runs both
halves and emits 633. hasExplicitArgs is now unused and removed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…adlock) The sidecar is spawned CREATE_NO_WINDOW, and on Windows a *synchronous* child spawn of a console app without that flag deadlocks on console allocation. So `wsl.exe -l -q` (via execSync/cmd.exe) hung, hit its 5s timeout, and the catch silently dropped every WSL distro — while every other shell still showed. It worked from a console (dev tests), which is why it looked fine. Two fixes: - Enumerate WSL distros from the registry (HKCU\...\Lxss\<guid>\ DistributionName) via reg.exe instead of wsl.exe -l -q — the same source Windows Terminal uses, and avoids wsl.exe's console/stdio quirks. - Pass windowsHide:true (CREATE_NO_WINDOW) on the reg call — the actual deadlock fix. Apply it to the open-ports powershell.exe/netstat spawns too, which had the identical latent hang. Diagnosed by logging detection in the real GUI sidecar (reg.exe ETIMEDOUT without windowsHide; OK in 20ms with it, Ubuntu then appears). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Picking a WSL distro launched the distro's shell with no OSC 633, because the Windows-side injection (--init-file/env) can't reach inside WSL. Append a `sh -c` detector to `wsl.exe -d <distro>` (passed as one argv element, so node-pty hands it to wsl→sh verbatim). It reads the login shell from /etc/passwd and execs bash with our bash init-file — referenced via the script's /mnt path so the Linux side can read it — when bash exists; steps aside for an explicit zsh/fish login shell; and falls back to the login shell only when bash is absent (e.g. Alpine). Biasing to bash on empty detection avoids dropping the user into /bin/sh when the (cold- start-flaky) lookup returns nothing. Verified end-to-end through resolveSpawnConfig + real node-pty/ConPTY: spawning Ubuntu emits the full P/A/B/E/C/D 633 lifecycle. Limitations: bash only; assumes the default /mnt automount root. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Readability cleanup from /simplify: the inline sh detector was built with `+` concatenation that spliced the mount path mid-statement. Express it as one sh statement per array element joined with a space (mount via a template literal). Byte-identical output — the exact-string test is unchanged and still passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
What
Extends OSC 633 shell integration to PowerShell (
pwsh) and Windows PowerShell (powershell.exe) — bash and zsh already had it. Dormouse now injects prompt/command boundaries so PowerShell sessions get real command history, exit codes, and cwd tracking instead of falling back to the keystroke heuristic.How
standalone/sidecar/shell-integration/pwsh/shellIntegration.ps1wraps the user'spromptfunction to emit theA/B/C/D/E/P;Cwdsequences. PowerShell has nopreexechook, so — like VS Code's PowerShell integration — a command'sE/C/Dare reported from the next prompt render (command line fromGet-History, exit code from$?/$LASTEXITCODE). Boundaries and exit codes are exact; theC/Dmarkers land at the following prompt rather than bracketing output in real time. Escaping matches the bash/zsh emitters (\→\,;→\x3b, etc.);Cwdis sent raw because the parser reads it verbatim.standalone/sidecar/pty-core.js: inject via-NoExit -Command ". '<script>'"for both PowerShell flavors (profile still loads, so the user's prompt is defined before we wrap it). Skipped when the caller passed explicit args, mirroring bash. Added ashellStem()helper that handles\separators and strips.exeso Windows shell paths match;resolveLoginArgreuses it.docs/specs/terminal-escapes.mdmarks PowerShell implemented and documents the mechanism + the next-prompt limitation.Testing
.ps1in a real PowerShell session: first prompt emitsA/P;Cwd/B; a full command cycle emitsE(escaped),C,D;<exit>; failing native command reports the true exit code (D;3); install guard prevents re-wrapping.Packaging
No build change needed — the VS Code build already copies
shell-integrationrecursively and standalone ships it via the tauri resources glob, sopwsh/is bundled automatically..ps1isn't a dotfile, so the dotfile-stripping caveat doesn't apply.🤖 Generated with Claude Code