Add desktop WSL backend mode#2353
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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. Comment |
There was a problem hiding this comment.
💡 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".
ApprovabilityVerdict: 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. |
|
wow this is great man |
|
Wow great work! Will find some time to test and review next week! |
f241be6 to
017f1d6
Compare
28281c2 to
443ec84
Compare
|
@Jgratton24 is attempting to deploy a commit to the Ping Labs Team on Vercel. A member of the Team first needs to authorize it. |
|
Still on my list of things to review! I've not forgotten |
|
@juliusmarminge migration to the new code structure is complete and ready for your review |
|
I am so stoked for this |
|
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. |
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.
1423710 to
923ed12
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.
1091e1d to
4f3a90c
Compare

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:
DesktopWslEnvironmentservice (apps/desktop/src/wsl/DesktopWslEnvironment.ts): Effect-based service that detects WSL availability, lists distros, pre-warms the VM, converts Windows paths viawslpath, resolves the user's Linux home dir (cached per distro), and preparesnode-ptyinside 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.apps/desktop/src/wsl/wslPathParsing.ts):wsl.exe --list --verboseparser, UNC-path distro extraction, picker default-path resolution (~,~/...,/..., and\\wsl.localhost\...), and strictDISTRO_NAME_PATTERN. Fully unit-tested.apps/desktop/src/backend/DesktopBackendConfiguration.ts): whenwslMode === "wsl", the backend manager spawnswsl.exe -d <distro> -- node <linux-entry> --bootstrap-fd 0 --dev-url <url>. Bootstrap JSON is delivered on stdin (extra stdio fds do not survive thewsl.exebridge); the dev-server URL is passed as a CLI flag because WSLENV translation of URL-shaped values is unreliable.t3Homeis omitted from the bootstrap so the Linux backend uses its own home directory — keeping per-backend state (env-id, threads, projects) cleanly partitioned.apps/desktop/src/settings/DesktopAppSettings.ts): newwslModeandwslDistrofields persisted with strict distro-name validation.setWslModereturns a{ changed }discriminator so the IPC handler can skip the restart when the toggle is a no-op.apps/desktop/src/ipc/methods/wsl.ts):getWslStateandsetWslBackendmethods. 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.apps/web/src/components/settings/ConnectionsSettings.tsx): a "Backend runtime"<Select>with anAlertDialogconfirmation, phased loading copy (Restarting backend…→Re-establishing session…→Syncing threads…), and a 180s global ceiling. WS connection events are silenced viasuppressReconnectfor 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 fromenvironmentStateById, and waits for the new welcome event before declaring success.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/homeparent.apps/server/src/bootstrap.ts):EACCESon 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: theDesktopWslEnvironmentservice and pure path helpers are independently testable, the WSL spawn branch inDesktopBackendConfigurationis a self-contained addition, and the renderer swap UX is localized toConnectionsSettings.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>listingLocal (Windows)and one entry per discovered WSL distro (default distro marked). Picking a different value opens anAlertDialogconfirming the swap, which transitions throughRestarting 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 on with Ubuntu selected
Confirmation dialog before a swap
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
Verification
bun run typecheckcleanbun run lintclean (no new warnings introduced by this PR)bun --filter @t3tools/desktop run test— coversDesktopWslEnvironmenttoolchain parsing and thewslPathParsinghelpers. Pre-existing failures inDesktopAppIdentity/DesktopEnvironmentare unrelated Windows path-normalization issues that also fail on the parent commit.bun --filter @t3tools/web run test~/projectinitialPath 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
Note
Add WSL backend mode to the desktop app, allowing the Node server to run inside a WSL distro
DesktopWslEnvironmentservice (DesktopWslEnvironment.ts) that detects WSL availability, lists distros, converts paths, and ensuresnode-ptyis built inside the chosen distro.wslModeandwslDistrotoDesktopAppSettings, persisted to disk and defaulting tolocal/null.DesktopBackendConfigurationto produce a WSL start config (usingwsl.exe,bootstrapDelivery='stdin', env secret forwarding viaWSLENV) whenwslMode='wsl'on Windows.getWslStateandsetWslBackendIPC methods, exposed on the preload bridge, so the renderer can query WSL state and switch backends at runtime with automatic rollback on failure.suppressReconnecthelper that prevents transient reconnect toasts during the backend swap.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-ptyreadiness), switches bootstrap delivery from fd3 to stdin for WSL, forwards selected secrets viaWSLENVwhile preserving existing entries, and avoids leakingT3CODE_HOME/t3Homeinto the Linux backend.Introduces a new
DesktopWslEnvironmentservice plus WSL path/distro parsing helpers and tests, expandsDesktopAppSettingsto persistwslMode/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
EACCESwhen 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.