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 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. |
|
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 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. |
|
I can do it if you don't have time. Should be "easier" to do now though with a bit more structured code 🫣 |
|
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.
ea1348b to
3ae1bb8
Compare
- 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
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 80f0dc6. Configure here.
…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.
|
@juliusmarminge migration to the new code structure is complete and ready for your review |

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. 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 server to run under Linux on Windows
DesktopWslEnvironmentservice (DesktopWslEnvironment.ts) that enumerates distros, converts paths, pre-warms WSL, and ensuresnode-ptyis built inside the selected distro.DesktopBackendConfigurationto support awslspawn mode: on Windows, the backend is launched viawsl.exewith bootstrap JSON delivered on stdin, selected API keys forwarded viaWSLENV, andt3Homeomitted.getWslStateandsetWslBackendIPC endpoints with runtime backend-swap logic including bounded readiness wait and automatic rollback on failure.wslModeandwslDistroinDesktopAppSettingswith normalization and validation of distro names.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-ptypreparation 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,t3Homeomitted in WSL mode, and a preflight failure signal that prevents futile restart loops.Introduces IPC + UI to manage the swap (
getWslState/setWslBackend), persistswslMode/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.