Skip to content

Add desktop WSL backend mode#2353

Open
Jgratton24 wants to merge 10 commits into
pingdotgg:mainfrom
Jgratton24:josh/desktop-wsl-backend
Open

Add desktop WSL backend mode#2353
Jgratton24 wants to merge 10 commits into
pingdotgg:mainfrom
Jgratton24:josh/desktop-wsl-backend

Conversation

@Jgratton24
Copy link
Copy Markdown

@Jgratton24 Jgratton24 commented Apr 26, 2026

What Changed

Adds an opt-in Windows desktop mode that keeps the Electron UI native while launching the local T3 Code backend inside WSL. Scoped to the desktop backend lifecycle path — complements rather than replaces the broader WSL-hosted interop work in #170.

Architecture:

  • DesktopWslEnvironment service (apps/desktop/src/wsl/DesktopWslEnvironment.ts): Effect-based service that detects WSL availability, lists distros, pre-warms the VM, converts Windows paths via wslpath, resolves the user's Linux home dir (cached per distro), and prepares node-pty inside the target distro. Toolchain pre-flight names the specific missing tools (node, make, g++, python3) with an actionable apt-install line up front, before any rebuild attempt.
  • Pure path helpers (apps/desktop/src/wsl/wslPathParsing.ts): wsl.exe --list --verbose parser, UNC-path distro extraction, picker default-path resolution (~, ~/..., /..., and \\wsl.localhost\...), and strict DISTRO_NAME_PATTERN. Fully unit-tested.
  • WSL spawn path (apps/desktop/src/backend/DesktopBackendConfiguration.ts): when wslMode === "wsl", the backend manager spawns wsl.exe -d <distro> -- node <linux-entry> --bootstrap-fd 0 --dev-url <url>. Bootstrap JSON is delivered on stdin (extra stdio fds do not survive the wsl.exe bridge); the dev-server URL is passed as a CLI flag because WSLENV translation of URL-shaped values is unreliable. t3Home is omitted from the bootstrap so the Linux backend uses its own home directory — keeping per-backend state (env-id, threads, projects) cleanly partitioned.
  • Settings (apps/desktop/src/settings/DesktopAppSettings.ts): new wslMode and wslDistro fields persisted with strict distro-name validation. setWslMode returns a { changed } discriminator so the IPC handler can skip the restart when the toggle is a no-op.
  • IPC (apps/desktop/src/ipc/methods/wsl.ts): getWslState and setWslBackend methods. The swap stops the running backend in-process, starts the new one, waits for ready with a 2-minute bound, and rolls back to the previous mode on timeout. The rollback's own readiness is checked and surfaces a distinct "degraded state" message if it also fails.
  • Renderer swap flow (apps/web/src/components/settings/ConnectionsSettings.tsx): a "Backend runtime" <Select> with an AlertDialog confirmation, phased loading copy (Restarting backend…Re-establishing session…Syncing threads…), and a 180s global ceiling. WS connection events are silenced via suppressReconnect for the duration of the swap so toasts don't flash during the deliberate disconnect. After the new backend's HTTP readiness, the renderer re-authenticates (each backend signs sessions with its own key, so the old cookie 401s), drops the previous env's slice from environmentStateById, and waits for the new welcome event before declaring success.
  • Folder picker (apps/desktop/src/ipc/methods/window.ts): when WSL mode is on, the picker default path resolves through the same pure helper, and ~/... paths expand against the user's actual Linux home (cached per-distro) instead of the /home parent.
  • Server bootstrap fix (apps/server/src/bootstrap.ts): EACCES on the inherited stdin fd is treated as a duplication error so the /proc/self/fd/<fd> fallback path applies under WSL.

Why

Closes #2346. Running the desktop app on Windows currently means launching the backend directly under Windows, which forces users with a WSL-based dev setup to either run the desktop app inside WSL (no native UX) or fall back to the web UI. This change keeps the Electron UI native on Windows while letting the backend run alongside the user's existing Linux toolchain.

The PR is size:XL, but the implementation is partitioned by responsibility: the DesktopWslEnvironment service and pure path helpers are independently testable, the WSL spawn branch in DesktopBackendConfiguration is a self-contained addition, and the renderer swap UX is localized to ConnectionsSettings.tsx. There is no straightforward way to split this without either shipping a permanently-disabled feature flag or merging the UI before the backend works behind it.

UI Changes

Adds a "Backend runtime" selector to the Connections settings panel: a design-system <Select> listing Local (Windows) and one entry per discovered WSL distro (default distro marked). Picking a different value opens an AlertDialog confirming the swap, which transitions through Restarting backend…Re-establishing session…Syncing threads… while the backend restarts, the renderer re-bootstraps, and the new welcome event arrives. Toasts surface success and error states.

WSL backend off

WSL backend off

WSL backend on with Ubuntu selected

WSL backend on

Confirmation dialog before a swap

Enable WSL backend confirmation dialog

The dialog sets the cold-start time expectation ("this may take a little while") and notes that each backend keeps its own threads — switching back returns the original list rather than wiping it.

Phased loading during a swap

Restarting backend loading state

Verification

  • bun run typecheck clean
  • bun run lint clean (no new warnings introduced by this PR)
  • bun --filter @t3tools/desktop run test — covers DesktopWslEnvironment toolchain parsing and the wslPathParsing helpers. Pre-existing failures in DesktopAppIdentity/DesktopEnvironment are unrelated Windows path-normalization issues that also fail on the parent commit.
  • bun --filter @t3tools/web run test
  • Full end-to-end pass on Windows 11 / Ubuntu (WSL2): Windows → WSL → Windows backend swaps with cold VM start, folder picker resolving a WSL ~/project initialPath against the user's real home dir, re-picking the resolved-default distro confirmed as a no-op (no dialog, no restart), and the rollback path when the target distro is broken.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes

Note

Add WSL backend mode to the desktop app allowing the server to run under Linux on Windows

  • Introduces a DesktopWslEnvironment service (DesktopWslEnvironment.ts) that enumerates distros, converts paths, pre-warms WSL, and ensures node-pty is built inside the selected distro.
  • Adds WSL-aware path parsing utilities (wslPathParsing.ts) for distro listing, UNC path extraction, and folder-picker default path resolution.
  • Extends DesktopBackendConfiguration to support a wsl spawn mode: on Windows, the backend is launched via wsl.exe with bootstrap JSON delivered on stdin, selected API keys forwarded via WSLENV, and t3Home omitted.
  • Adds getWslState and setWslBackend IPC endpoints with runtime backend-swap logic including bounded readiness wait and automatic rollback on failure.
  • Adds a "Backend runtime" selector in the Connections settings UI that orchestrates the swap, suppresses reconnect toasts during transition, waits for the new backend's welcome, and forces reauthentication.
  • Persists wslMode and wslDistro in DesktopAppSettings with normalization and validation of distro names.
  • Risk: switching backends restarts the desktop server process; rollback is attempted automatically but a failed rollback leaves the user in a broken state until manual intervention.

Macroscope summarized 7c9f3a4.


Note

High Risk
High risk because it changes how the desktop backend is spawned and bootstrapped (including env/stdio delivery), adds a restart-and-rollback swap flow over IPC, and touches renderer auth/WS connection UX during backend transitions.

Overview
Adds an opt-in Windows desktop mode that runs the local backend inside WSL while keeping the Electron UI native, including WSL detection/distro enumeration, path conversion, and node-pty preparation with preflight errors.

Updates desktop backend startup to support a WSL spawn path (wsl.exe ... node ...) with bootstrap JSON delivered via stdin, selective env forwarding, t3Home omitted in WSL mode, and a preflight failure signal that prevents futile restart loops.

Introduces IPC + UI to manage the swap (getWslState/setWslBackend), persists wslMode/wslDistro, restarts the backend in-process with bounded readiness waits and rollback on failure, and updates the web settings panel to drive the swap while suppressing reconnect/offline toasts and re-authenticating after a backend key change.

Reviewed by Cursor Bugbot for commit 7c9f3a4. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 4949060d-e7c6-494d-af72-1b4be202ab2c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Apr 26, 2026
Comment thread apps/desktop/src/main.ts Outdated
Comment thread apps/desktop/src/main.ts Outdated
Comment thread apps/desktop/src/main.ts Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a574cbb5d0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/desktop/src/backendReadiness.ts Outdated
@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp Bot commented Apr 26, 2026

Approvability

Verdict: Needs human review

This PR introduces a complete new feature: WSL backend mode for Windows users. It adds new user-facing settings, backend lifecycle management with swap/rollback logic, WSL subprocess management, and significant UI changes. New features of this scope warrant human review.

You can customize Macroscope's approvability policy. Learn more.

Comment thread apps/desktop/src/backendReadiness.ts Outdated
Comment thread apps/desktop/src/main.ts Outdated
Comment thread apps/desktop/src/main.ts Outdated
Comment thread apps/desktop/src/wsl.ts Outdated
Comment thread apps/desktop/src/wsl.ts Outdated
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx Outdated
Comment thread apps/desktop/src/main.ts Outdated
Comment thread apps/desktop/src/main.ts Outdated
Comment thread apps/desktop/src/main.ts Outdated
@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Apr 26, 2026
@adenafil
Copy link
Copy Markdown

wow this is great man

Comment thread apps/desktop/src/backendReadiness.ts Outdated
Comment thread apps/desktop/src/main.ts Outdated
Comment thread apps/desktop/src/main.ts Outdated
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx
@juliusmarminge
Copy link
Copy Markdown
Member

Wow great work! Will find some time to test and review next week!

@juliusmarminge juliusmarminge self-requested a review April 27, 2026 02:34
Comment thread apps/desktop/src/main.ts Outdated
@github-actions github-actions Bot added size:XXL 1,000+ changed lines (additions + deletions). and removed size:XL 500-999 changed lines (additions + deletions). labels Apr 27, 2026
Comment thread apps/desktop/src/wsl.ts Outdated
Comment thread apps/desktop/src/main.ts Outdated
@Jgratton24 Jgratton24 force-pushed the josh/desktop-wsl-backend branch from f241be6 to 017f1d6 Compare April 27, 2026 17:52
Comment thread apps/desktop/src/wsl.ts Outdated
Comment thread apps/desktop/src/main.ts Outdated
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx Outdated
Comment thread apps/web/src/rpc/wsConnectionState.ts
Comment thread apps/desktop/src/wsl.ts Outdated
@Jgratton24 Jgratton24 force-pushed the josh/desktop-wsl-backend branch from 28281c2 to 443ec84 Compare April 27, 2026 19:10
@vercel
Copy link
Copy Markdown

vercel Bot commented May 4, 2026

@Jgratton24 is attempting to deploy a commit to the Ping Labs Team on Vercel.

A member of the Team first needs to authorize it.

Comment thread apps/desktop/src/main.ts Outdated
Comment thread apps/web/src/rpc/wsConnectionState.ts Outdated
@juliusmarminge
Copy link
Copy Markdown
Member

Still on my list of things to review! I've not forgotten

Comment thread apps/desktop/src/wsl.ts Outdated
Comment thread apps/desktop/src/main.ts Outdated
Comment thread apps/desktop/src/wsl.ts Outdated
Comment thread apps/desktop/src/main.ts Outdated
Comment thread apps/desktop/src/wsl.ts Outdated
Comment thread apps/desktop/src/main.ts Outdated
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx Outdated
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx Outdated
Comment thread apps/desktop/src/main.ts Outdated
Comment thread apps/desktop/src/main.ts Outdated
Comment thread apps/desktop/src/main.ts Outdated
Comment thread apps/server/src/provider/Layers/ClaudeProvider.ts Outdated
Comment thread apps/desktop/src/wsl.ts Outdated
Comment thread apps/web/src/environments/primary/context.ts Outdated
@Jgratton24
Copy link
Copy Markdown
Author

Jgratton24 commented May 8, 2026

@juliusmarminge wanted to ask before I start the port. The recent main.ts refactor (DesktopApp, DesktopBackendManager, etc) replaces basically all the code I'm touching, so getting the WSL backend into the new layer structure is a multi-hour effort

Still on track to land if I do that work, or would you rather take the design from here and integrate it yourself? Either way's fine, just want to know before redoing it.

@juliusmarminge
Copy link
Copy Markdown
Member

I can do it if you don't have time. Should be "easier" to do now though with a bit more structured code 🫣

@Callanplays
Copy link
Copy Markdown

I'm beggin! 🙏 Please! Need this so bad :)

Stdin pipes inherited across the wsl.exe boundary fail to re-open
via /proc/self/fd/0, so add EACCES to the codes that drop back to
reading the fd directly. Without this the WSL desktop backend
fails to load its bootstrap envelope with "Failed to duplicate
bootstrap fd" and ends up in a scheduled-restart loop.
Lets the desktop app launch the local backend inside a WSL distro
instead of natively on Windows. Adds:

- Backend plumbing (apps/desktop/src/wsl): pure path parsing
  utilities, a DesktopWslEnvironment Effect service wrapping
  wsl.exe operations (listDistros, preWarm, windowsToWslPath,
  ensureNodePty, isAvailable), and an explicit preflight that
  checks for missing node / build tools before spawning so the
  failure message names the actual problem.
- Spawn path: DesktopBackendConfiguration branches on the new
  wslMode setting and assembles "wsl.exe -d <distro> -- node
  <linux-entry> --bootstrap-fd 0" with the bootstrap envelope on
  stdin (wsl.exe drops additional file descriptors). Sensitive env
  vars forward via WSLENV; --dev-url is passed as a CLI flag so the
  WSL dev backend lands in dev/ instead of userdata/ deterministi-
  cally. The Windows-side T3CODE_HOME is scrubbed and extendEnv is
  disabled for WSL so the WSL backend cannot accidentally share a
  baseDir with the local backend via /mnt/c/...
- Settings: wslMode + wslDistro on DesktopAppSettings, with
  validation that drops distro names containing control or shell
  meta characters. Contracts get DesktopWslMode / DesktopWslDistro
  / DesktopWslState schemas.
- IPC: getWslState and setWslBackend on the desktop bridge. The
  setter pre-warms the WSL VM, persists settings, then drives an
  in-process backend stop + start with a 2-minute readiness wait
  and a rollback path that reverts to the previous mode if the new
  backend never reports ready. pickFolder defaults to the WSL home
  UNC path when wslMode is "wsl".
- Web UI: backend-runtime selector in Connection Settings with a
  three-stage swap modal (restarting / re-establishing session /
  syncing) that suppresses the WS reconnect toast for the duration
  of the swap, waits for the new backend's welcome event before
  closing, and clears the previous env's store state so the side-
  bar does not render stale threads. New suppressReconnect helper
  on the connection-status atom plus exports for the descriptor
  refresh and reauth used by the swap flow.
@Jgratton24 Jgratton24 force-pushed the josh/desktop-wsl-backend branch from ea1348b to 3ae1bb8 Compare May 11, 2026 13:38
Comment thread apps/desktop/src/wsl/DesktopWslEnvironment.ts Outdated
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx Outdated
Comment thread apps/web/src/environments/primary/context.ts Outdated
Comment thread apps/desktop/src/backend/DesktopBackendManager.ts
- drain stdout/stderr concurrently in runWslShell so node-gyp output
  on both pipes can't deadlock the child
- short-circuit waitForReady when desiredRunning flips off, so an
  external stop() during swap doesn't waste the full timeout
- guard runSwap continuations after the 180s flow timeout so an
  orphaned IPC resolution can't overwrite rolled-back UI state
- drop unused refreshPrimaryEnvironmentDescriptor — descriptor URL is
  stable across the swap and consumers re-fetch lazily
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx Outdated
…tate removal

- move clearTimeout(flowTimeoutHandle) into the finally block so it fires
  on success, error, and timeout — previously the error path left a live
  180s timer that would reject an unreferenced promise (unhandled rejection)
- remove the second removeEnvironmentState call after welcome; the first
  call right after the IPC swap already wipes the old environment state
  and nothing recreates state under the old env id during reauth/welcome
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx
Comment thread apps/desktop/src/wsl/wslPathParsing.ts
Comment thread apps/desktop/src/ipc/methods/wsl.ts
… mapping, surface failed rollback

- map null distro to the actual default distro name in the backend select
  so the dropdown highlights a real option instead of an orphan "__default__"
  with no matching item when distros are listed
- add getUserHome to DesktopWslEnvironment (cached per distro) and pass the
  resolved /home/<user> into the picker helper so ~/path expands correctly
  instead of producing /home/<rest>
- surface a clearer error when the rollback backend also fails to start, so
  the user knows the app is degraded rather than seeing the misleading
  "Rolled back to the previous mode" message
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx
Comment thread apps/desktop/src/backend/DesktopBackendConfiguration.ts Outdated
…undant WSLENV entry

- swap "__local__" / "__default__" select values for "backend:local" /
  "backend:default-wsl" — the colon is rejected by DISTRO_NAME_PATTERN so
  the sentinels can never collide with an actual WSL distro name
- remove VITE_DEV_SERVER_URL from WSL_FORWARDED_ENV_NAMES; the value is
  delivered exclusively via the --dev-url CLI flag because WSLENV translation
  of URL-shaped values is unreliable, and keeping it in both paths
  contradicted the comment at the CLI-flag site
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx
The dropdown maps state.distro: null to the actual default distro's name
so the Select highlights a real option, but the no-op check still compared
target.distro (e.g. "Ubuntu") against state.distro (null). Re-picking the
visually-active row opened the confirmation dialog and triggered a full
backend restart for what was clearly a no-op. Resolve both sides through
the same null->default mapping before comparing.
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx
The renderer's 180s ceiling was shorter than the IPC's worst-case duration:
setWslBackend can take up to ~2min for the initial readiness wait plus
another ~2min for the rollback readiness wait before throwing
WslBackendSwapError, so the client was firing "Backend swap took too long"
while the main process was still actively rolling back. Bump the ceiling
to 6 minutes (4min IPC worst case + ~60s reauth retry budget + 45s welcome
race) so a real hang still surfaces but a legitimate rollback completes.
Comment thread apps/desktop/src/wsl/wslPathParsing.ts
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx
…n through error recovery

- remove the unused `enabled` field from WslConfig and the unreferenced
  DEFAULT_WSL_CONFIG export; the toggle moved to DesktopAppSettings.wslMode
  during the migration and the field was carried along by every caller as
  noise that didn't influence behavior
- wrap the entire backend-swap flow (success + catch) in suppressReconnect
  so the catch-block reauth doesn't fire reconnect/offline toasts on top of
  the error toast the user is reading. The previous structure only
  suppressed during the happy path; recovery work landed outside the window
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 80f0dc6. Configure here.

Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx
…e-fire false resolve

onWelcome subscribes with `immediate: true`, so the listener fires
synchronously with whatever welcome payload is already in the atom. The
previous code compared against `previousPrimaryEnvId` (descriptor-derived);
if the descriptor hadn't loaded yet, that was null and any non-null current
welcome would resolve the promise instantly, completing the "syncing" stage
before the new backend's welcome actually arrived. Capture the current
welcome's env-id from the atom as the baseline instead so the immediate
fire never matches the "new welcome arrived" predicate.
@Jgratton24
Copy link
Copy Markdown
Author

@juliusmarminge migration to the new code structure is complete and ready for your review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Desktop WSL backend mode for Windows app

4 participants