Skip to content

feat(wsl): run codedash from WSL with Windows-host browser UI#172

Open
vbp1 wants to merge 7 commits intovakovalskii:mainfrom
vbp1:feat/wsl-support
Open

feat(wsl): run codedash from WSL with Windows-host browser UI#172
vbp1 wants to merge 7 commits intovakovalskii:mainfrom
vbp1:feat/wsl-support

Conversation

@vbp1
Copy link
Copy Markdown

@vbp1 vbp1 commented Apr 11, 2026

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.exe and powershell.exe targets for the terminal picker when running inside WSL. WSL-side sessions start via a trampoline .sh file + wsl.exe -d <distro> to sidestep nested quoting through wt → cmd → wsl → bash. Windows-side sessions (cwd under /mnt/<drive>/… or a Windows-form path) bypass wsl.exe and run the native claude/codex binary directly, with /mnt/c/...C:\... path translation. Launch uses -w 0 so 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 TabItem via System.Windows.Automation (UIA) on the WindowsTerminal process — this sees inactive tabs, unlike MainWindowTitle. 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 by Name == marker, calls SelectionItemPattern.Select() and SetForegroundWindow on the parent wt window (with IsIconic → ShowWindowAsync(SW_RESTORE) for minimized ones). The original tab title is then restored via a second OSC write, using a stable (windowHwnd, tabIndex) key — UIA RuntimeId is not persistent across PS invocations. For non-WT processes (wsl-powershell launches into conhost/OpenConsole), a title-based fallback enumerates top-level windows with MainWindowTitle -eq sessionTag (excluding WindowsTerminal from the fallback). Which path runs is gated on WT_SESSION in /proc/<pid>/environ (forwarded via WSLENV=WT_SESSION:WT_PROFILE_ID:), so OSC marker mutation never touches a conhost pts and can't race the title fallback.

  • Open in IDE: openIDE branches between the Linux Remote-WSL wrapper (code/cursor on PATH), code.exe --remote wsl+<distro> <path> fallback, and native .exe invocation for Windows-side projects.

  • Browser auto-open: xdg-open is skipped in WSL; the server prints the URL in yellow so the user opens it from the Windows host.

  • Security hardening:

    • sessionId validated against ^[A-Za-z0-9._-]{1,128}$ at /api/launch, /api/focus, and inside terminals.js.
    • /api/focus rejects non-positive-integer pid with 400 before it can flow into /proc/${pid}/fd/${fd}; focusWslByPid re-checks as defense in depth.
    • WSL launch trampoline writes into fs.mkdtempSync with a fixed launch.sh filename so client input cannot influence temp paths.
    • Every exec-as-shell-string in the IDE helper rewritten to execFile(bin, [...argv]).
  • Logging: relabels the cross-OS scan messages in data.js from "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

vbp1 added 5 commits April 11, 2026 19:01
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.
@vbp1 vbp1 marked this pull request as ready for review April 11, 2026 19:56
vbp1 added 2 commits April 12, 2026 15:57
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.
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.

feat: run codedash from WSL with Windows-host browser UI (launch, focus, IDE)

1 participant