feat(wsl): run codedash from WSL with Windows-host browser UI#172
Open
vbp1 wants to merge 7 commits intovakovalskii:mainfrom
Open
feat(wsl): run codedash from WSL with Windows-host browser UI#172vbp1 wants to merge 7 commits intovakovalskii:mainfrom
vbp1 wants to merge 7 commits intovakovalskii:mainfrom
Conversation
Adds first-class support for running codedash inside WSL while using the Windows browser as UI: - terminals.js: detect WSL via /proc/version + WSL_DISTRO_NAME; expose wt.exe / powershell.exe as launch targets. Open new terminals through a .sh trampoline to sidestep nested quoting across wt → cmd → wsl → bash. Tag each tab/window title with codedash-<id> so focus can find it later. - terminals.js: focusTerminalByPid accepts sessionId and, on WSL, runs a temp .ps1 that enumerates processes by MainWindowTitle and calls SetForegroundWindow (ShowWindowAsync for minimized windows). - terminals.js: handle Windows-side sessions (cwd under /mnt/<drive> or in Windows path form) by launching natively via cmd.exe or powershell instead of wsl.exe, with path translation back to C:\\. - server.js: skip xdg-open in WSL and print the URL for the user to open on the Windows host; route Focus via sessionId; openIDE helper branches between Linux wrapper, code.exe --remote wsl+<distro> and native Windows invocation based on path origin. - frontend/app.js: send sessionId alongside pid in /api/focus. - data.js: clearer log labels for Windows-host scan directories.
- openInTerminal PowerShell paths went through /bin/sh via execSync +
JSON.stringify, so $Host in the command was expanded to empty before
PowerShell saw it. Switch both Windows-side and WSL-side PS branches
to execFileSync('powershell.exe', [...argv]) so the shell never sees
the payload.
- Also single-quote-escape WSL_DISTRO_NAME instead of JSON.stringify so
the same argv-only path works for the -d flag.
- openIDE called execSync but server.js only imported exec / execFile;
the ReferenceError was swallowed by a bare catch {}, permanently
disabling the Linux wrapper detection. Import execFileSync and probe
via ['which', bin]; narrow the catch so only exit-1 "not found" stays
silent.
- openIDE also built shell strings like `cursor "${target}"`, which
broke on paths containing quotes. Route every branch through
execFile(bin, [target]).
- Focus previously matched against Get-Process.MainWindowTitle, but
Windows Terminal exposes only the currently active tab title as the
host process MainWindowTitle. Any inactive codedash tab became
invisible to the scan, and tabs stacked via `-w 0` made focus
effectively random. Rework in two parts:
1. Launch uses `wt.exe -w new` so every session gets its own
top-level wt window instead of stacking tabs.
2. Focus PowerShell script enumerates visible top-level windows via
Win32 EnumWindows + GetWindowText and matches the pinned
codedash-<id> tag. ShowWindowAsync(SW_RESTORE) still handles
minimized windows before SetForegroundWindow.
- Harden sessionId handling. assertSafeSessionId + a strict
[A-Za-z0-9._-]{1,128} regex runs on every openInTerminal and
focusTerminalByPid WSL call, and /api/launch + /api/focus reject
malformed ids with HTTP 400 before touching any child process. All
five supported agents use ids that fit this shape.
- writeWslLaunchScript now creates a private directory via
fs.mkdtempSync and writes a fixed launch.sh inside, so a client
cannot influence the temp path layout. Cleanup uses fs.rmSync with
recursive:true to wipe the mkdtemp dir after the child has read the
script.
Rewrite focusWslByPid to cover both Windows Terminal tabs and conhost windows (wsl-powershell launch path). The previous implementation relied solely on WT UIA + OSC 2 marker injection into the target pts, which regressed focus for non-WT sessions and could race with conhost hosts that honor OSC title sequences. Detect WT tabs via WT_SESSION in /proc/<pid>/environ (WSLENV forwards it from the Windows side). Only mutate the pts via OSC when the target is actually a WT tab; for conhost sessions, skip straight to a non- mutating fallback that matches top-level windows by MainWindowTitle against the sessionTag pinned at launch via $Host.UI.RawUI.WindowTitle. WindowsTerminal is excluded from the fallback because MainWindowTitle only reflects the active tab. Harden /api/focus against bad input: reject non-positive-integer pid with a 400 before it can flow into /proc/<pid>/fd/<n>. Re-check in focusWslByPid as defense in depth for internal callers.
Switch wt.exe launch args from `-w new` to `-w 0` in both the Windows- side and WSL-side launch branches. The previous `-w new` was only needed because the old focus logic relied on MainWindowTitle, which only reflects the currently active tab. UIA-based focus now enumerates every TabItem and selects the target via SelectionItemPattern, so stacking sessions as tabs in one window is safe. wt falls back to creating a fresh window when none exists, so the "no window" edge case is handled by the terminal itself.
The trampoline launch.sh was invoked via `bash scriptPath`, which skips ~/.profile and ~/.bash_profile. Tools installed in ~/.local/bin (like claude) were not on PATH, causing "command not found" on resume. Switch all three invocation points to `bash -l`: the shebang line and both wt.exe/powershell.exe launch paths now run the script as a login shell, which sources the user's profile and picks up PATH additions.
bash -l (login-only) reads ~/.profile but skips ~/.bashrc, which is where many users configure PATH additions (nvm, pyenv, etc.) and shell customizations. Switch WSL launch invocations from bash -l to bash -li so the trampoline script runs as both login and interactive, sourcing the full user environment.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds first-class WSL support so codedash can run inside WSL while the web UI is opened in a Windows browser, with Launch / Focus / Open-in-IDE all working across the WSL ↔ Windows boundary.
Terminal launch: new
wt.exeandpowershell.exetargets for the terminal picker when running inside WSL. WSL-side sessions start via a trampoline.shfile +wsl.exe -d <distro>to sidestep nested quoting throughwt → cmd → wsl → bash. Windows-side sessions (cwd under/mnt/<drive>/…or a Windows-form path) bypasswsl.exeand run the nativeclaude/codexbinary directly, with/mnt/c/...→C:\...path translation. Launch uses-w 0so each resume opens as a new tab in the current wt window, falling back to a fresh window if none exists.Focus by session (UIA-based): focus logic was rewritten to handle multi-tab wt windows reliably. For Windows Terminal targets, we snapshot every
TabItemviaSystem.Windows.Automation(UIA) on theWindowsTerminalprocess — this sees inactive tabs, unlikeMainWindowTitle. A short-lived OSC 2 marker is injected directly into the target/dev/pts/<X>(resolved from/proc/<pid>/fd/{0,1,2}), so the target tab renames itself client-side. A second UIA pass locates the tab byName == marker, callsSelectionItemPattern.Select()andSetForegroundWindowon the parent wt window (withIsIconic → ShowWindowAsync(SW_RESTORE)for minimized ones). The original tab title is then restored via a second OSC write, using a stable(windowHwnd, tabIndex)key — UIARuntimeIdis not persistent across PS invocations. For non-WT processes (wsl-powershelllaunches intoconhost/OpenConsole), a title-based fallback enumerates top-level windows withMainWindowTitle -eq sessionTag(excludingWindowsTerminalfrom the fallback). Which path runs is gated onWT_SESSIONin/proc/<pid>/environ(forwarded viaWSLENV=WT_SESSION:WT_PROFILE_ID:), so OSC marker mutation never touches a conhost pts and can't race the title fallback.Open in IDE:
openIDEbranches between the Linux Remote-WSL wrapper (code/cursoronPATH),code.exe --remote wsl+<distro> <path>fallback, and native.exeinvocation for Windows-side projects.Browser auto-open:
xdg-openis skipped in WSL; the server prints the URL in yellow so the user opens it from the Windows host.Security hardening:
sessionIdvalidated against^[A-Za-z0-9._-]{1,128}$at/api/launch,/api/focus, and insideterminals.js./api/focusrejects non-positive-integerpidwith 400 before it can flow into/proc/${pid}/fd/${fd};focusWslByPidre-checks as defense in depth.fs.mkdtempSyncwith a fixedlaunch.shfilename so client input cannot influence temp paths.exec-as-shell-string in the IDE helper rewritten toexecFile(bin, [...argv]).Logging: relabels the cross-OS scan messages in
data.jsfrom "Extra ... dirs" to "Windows host homes" / "Windows-side ... dirs" so it's clear these are Windows paths, not WSL paths. No behavior change.Closes #173
Related to #150