Skip to content

Add desktop WSL backend mode#2353

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

Add desktop WSL backend mode#2353
Jgratton24 wants to merge 11 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 and #192 — the original community ask for WSL support (105 👍, 23 comments). 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 Node server to run inside a WSL distro

  • Introduces a DesktopWslEnvironment service (DesktopWslEnvironment.ts) that detects WSL availability, lists distros, converts paths, and ensures node-pty is built inside the chosen distro.
  • Adds wslMode and wslDistro to DesktopAppSettings, persisted to disk and defaulting to local/null.
  • Extends DesktopBackendConfiguration to produce a WSL start config (using wsl.exe, bootstrapDelivery='stdin', env secret forwarding via WSLENV) when wslMode='wsl' on Windows.
  • Adds getWslState and setWslBackend IPC methods, exposed on the preload bridge, so the renderer can query WSL state and switch backends at runtime with automatic rollback on failure.
  • Adds a 'Backend runtime' selector in the Connections settings UI (ConnectionsSettings.tsx) that orchestrates a backend restart, reauthentication, and readiness wait when the user changes modes.
  • Adds a suppressReconnect helper that prevents transient reconnect toasts during the backend swap.
  • Risk: Switching backends involves a full backend restart and reauthentication cycle; if the new backend doesn't become ready within 6 minutes, the system rolls back to the previous mode.

Macroscope summarized 4f3a90c.


Note

High Risk
High risk because it changes how the desktop backend process is launched (including env/secret forwarding and bootstrap delivery) and adds an in-app backend restart/rollback flow that can affect local data directories and connectivity on Windows.

Overview
Adds an opt-in Windows WSL backend mode so the Electron UI stays native while the local backend can be launched via wsl.exe.

The desktop backend launcher now resolves start config from persisted app settings, supports WSL-specific preflight (WSL availability, path conversion, node-pty readiness), switches bootstrap delivery from fd3 to stdin for WSL, forwards selected secrets via WSLENV while preserving existing entries, and avoids leaking T3CODE_HOME/t3Home into the Linux backend.

Introduces a new DesktopWslEnvironment service plus WSL path/distro parsing helpers and tests, expands DesktopAppSettings to persist wslMode/wslDistro, and adds IPC endpoints (getWslState, setWslBackend) that restart the backend with a bounded readiness wait and rollback on failure.

The web UI gains a “Backend runtime” selector with a staged swap modal, re-authentication after swaps, and temporary suppression of websocket reconnect toasts during intentional backend restarts; the server bootstrap code also tolerates EACCES when reading bootstrap payload from stdin under WSL.

Reviewed by Cursor Bugbot for commit 4f3a90c. 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: ef64e778-a6ff-4527-a661-9ae7232609cc

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 substantial new feature (WSL backend mode) with ~1900 lines of new code, new UI workflows, new IPC methods, and significant runtime behavior changes for how the desktop backend is spawned and managed. New features of this scope warrant careful 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/web/src/components/settings/ConnectionsSettings.tsx
Comment thread apps/desktop/src/wsl/wslPathParsing.ts
Comment thread apps/desktop/src/ipc/methods/wsl.ts
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx
Comment thread apps/desktop/src/backend/DesktopBackendConfiguration.ts Outdated
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx
Comment thread apps/desktop/src/wsl/wslPathParsing.ts
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx
@Jgratton24
Copy link
Copy Markdown
Author

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

@benjaminlgur benjaminlgur mentioned this pull request May 14, 2026
@shurkanTwo
Copy link
Copy Markdown

I am so stoked for this

@benjaminlgur
Copy link
Copy Markdown

benjaminlgur commented May 15, 2026

How possible would it be to have wsl backend set per project instead of the whole app?

I have some projects I prefer to run with native windows instead of wsl.

Jgratton24 added 10 commits May 16, 2026 11:38
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.
- 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
…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
… 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
…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
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.
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.
…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
…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.
Comment thread apps/desktop/src/settings/DesktopProjectBackendPreferences.ts Outdated
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx Outdated
Comment thread apps/web/src/desktopWslBackendSwap.ts Outdated
@Jgratton24 Jgratton24 force-pushed the josh/desktop-wsl-backend branch from 1423710 to 923ed12 Compare May 16, 2026 15:46
Comment thread apps/desktop/src/settings/DesktopProjectBackendPreferences.ts Outdated
Comment thread apps/web/src/desktopWslBackendSwap.ts Outdated
Comment thread apps/desktop/src/settings/DesktopProjectBackendPreferences.ts Outdated
Comment thread apps/web/src/projectBackendPreference.ts Outdated
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx
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 8b1b56c. Configure here.

Comment thread apps/desktop/src/backend/DesktopBackendConfiguration.ts Outdated
@Jgratton24 Jgratton24 force-pushed the josh/desktop-wsl-backend branch from 1091e1d to 4f3a90c Compare May 16, 2026 20:38
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

6 participants