Repository navigation
Add desktop WSL backend mode - #2353
Jgratton24 wants to merge 15 commits into
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)
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 ~2000 lines across multiple new files, new IPC methods, settings, and UI components. Additionally, there are unresolved review comments including a high-severity timeout race condition that warrants human attention. 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 |
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
1091e1d to
4f3a90c
Compare
This would be even better. I have the same "problem". |
|
@juliusmarminge I opened #2751 as a follow-up with the parallel Windows + WSL backend work on top of this PR. I’m good with keeping them separate so this merges first and the follow-up rebases, or folding it all into #2353 if that’s easier to review |
|
This is a very much appreciated feature. It's been here for weeks, huge rebases were already made because the codebase changes so fast. Glad @Jgratton24 is being resilient. |
|
@juliusmarminge quick bump on this when you get a chance. I think the only open question is whether you’d rather keep #2751 separate or fold it into #2353 |
|
Hello @Jgratton24, can you take a look at my comment on #2751 when you get a chance. Thanks |
…ckend-merge-main # Conflicts: # apps/desktop/src/backend/DesktopBackendConfiguration.ts
|
Would love to use this feature, I am still waiting for a good desktop app that integrates seamlessly with WSL |
Resolves conflicts from upstream pingdotgg#2013 (mobile WIP) and its companion refactors: - ConnectionsSettings.tsx: drop the now-dead `~/rpc/wsRpcClient` type import (file removed upstream); WsRpcClient is now provided by @t3tools/client-runtime. Keep the live serverState / wsConnectionState / store imports. - DesktopWslEnvironment.ts: rename the service id from the legacy `t3/desktop/WslEnvironment` to `@t3tools/desktop/wsl/DesktopWslEnvironment` to satisfy the Effect deterministic-key lint the TSGo migration enforces. Full typecheck green across all 14 packages; web settings 27/27, desktop wsl 31/31.
Bugbot (medium): the WSL backend swap flow caps the wait for the new
backend's welcome event at 45s, then showed an unconditional "Backend
restarted" success toast. On a timeout the backend has genuinely
restarted (setWslBackend + reauth already succeeded), but thread state
may still reflect the prior backend until the delayed welcome lands, so
the copy overclaimed readiness.
Track which side of the 45s race wins and soften the success
description when the welcome didn't sync in time ("taking a moment to
finish syncing - your threads will refresh automatically"), instead of
asserting the backend is fully ready. Still a success (the restart did
happen); the global welcome subscription refreshes threads when the
welcome eventually arrives. Also clears the welcome-race timer once the
race settles.
…swaps Two bugbot reports on the WSL backend flow: High - "WSL off hides recovery control": when persisted wslMode is "wsl" but WSL is unavailable (wsl.exe removed, no distro), backend resolution still took the WSL spawn path and looped on preflight failures, while the Connections "Backend runtime" row is hidden whenever WSL is unavailable - leaving no in-app way back to Windows. resolveBackendStartConfig now checks wslEnvironment.isAvailable and falls back to the Windows backend (logging a warning) when WSL was requested but isn't usable. Added a regression test asserting the Windows path is taken in that case. Medium - "Six minute cap false failure": the swap wrapped setWslBackend + reauth + welcome wait in one 6-minute Promise.race, so a slow-but- successful swap could trip the ceiling during reauth/sync and surface "Backend swap took too long" even though the backend already switched (and getWslState would show the new mode). Track a swapCommitted flag set once setWslBackend returns; when the global ceiling fires after commit, report the switch as done with the still-syncing caveat instead of a hard failure. Genuine pre-commit failures (aborted false, or setWslBackend throwing) still take the error path.
…ailable Parity with the pingdotgg#2353 fix for the "WSL off hides recovery control" report. resolvePrimary took the wsl-only primary path whenever wslOnly + wslBackendEnabled were persisted, without checking whether WSL is actually usable. If WSL became unavailable (wsl.exe removed, no distro), the primary looped on preflight failures while the Connections backend control is hidden, leaving no in-app way back to Windows. resolvePrimary now checks wslEnvironment.isAvailable and resolves the Windows primary (logging a warning) when wsl-only was requested but WSL isn't usable. Added a regression test asserting the Windows primary path under isAvailable: false.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 4 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 00b200c. Configure here.
| flowTimeoutHandle = window.setTimeout(() => { | ||
| aborted = true; | ||
| reject(new Error("Backend swap took too long. Check WSL is responsive and try again.")); | ||
| }, 360_000); |
There was a problem hiding this comment.
Swap timeout beats WSL build
High Severity
The backend swap uses a six-minute Promise.race ceiling, but setWslBackend can block longer while WSL node-pty is built during configuration.resolve (up to five minutes) plus up to two minutes on waitForReady. The UI can show “swap took too long” and treat the flow as failed while the desktop swap is still running or completes successfully afterward.
Reviewed by Cursor Bugbot for commit 00b200c. Configure here.
| }, [desktopBridge, pendingDesktopWslSelection]); | ||
|
|
||
| const renderWslRow = () => { | ||
| if (!desktopWslState || !desktopWslState.available) return null; |
There was a problem hiding this comment.
WSL unavailable hides runtime selector
Medium Severity
The “Backend runtime” row is omitted whenever desktopWslState.available is false. If settings still say wslMode: "wsl" but WSL is missing or broken, the desktop falls back to the Windows backend yet the UI offers no way to persist local mode, leaving users stuck until WSL works again or settings are edited manually.
Reviewed by Cursor Bugbot for commit 00b200c. Configure here.
|
|
||
| if (previousPrimaryEnvId) { | ||
| useStore.getState().removeEnvironmentState(previousPrimaryEnvId); | ||
| } |
There was a problem hiding this comment.
Swap skips welcome env cleanup
Medium Severity
After a successful swap, stale thread state is dropped only when readPrimaryEnvironmentDescriptor() returns an environment id. The welcome wait uses getWelcomePayload() as its baseline, but if the descriptor is not loaded yet while a welcome payload exists, removeEnvironmentState is skipped and the old environment’s slice can remain in the store after the backend change.
Reviewed by Cursor Bugbot for commit 00b200c. Configure here.
| return toUncPath(trimmedPath); | ||
| } | ||
|
|
||
| return homePath; |
There was a problem hiding this comment.
WSL picker ignores Windows paths
Low Severity
In WSL backend mode, resolveWslPickFolderDefaultPath treats only UNC, ~, and Linux absolute paths specially; any other string (including typical C:\... initial paths) falls through to the generic WSL home default instead of opening near the requested folder.
Reviewed by Cursor Bugbot for commit 00b200c. Configure here.
|
Closing in favor of #2751 |


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
DesktopWslEnvironmentservice (DesktopWslEnvironment.ts) that detects WSL availability, lists distros, converts paths, and verifies/buildsnode-ptyinside a WSL distro.DesktopBackendStartConfigandresolveBackendStartConfigto launch the backend viawsl.exewhen WSL mode is active, forwarding selected secrets and mergingWSLENV; falls back to the Windows backend if WSL is unavailable.getWslStateandsetWslBackendIPC handlers and preload bridge methods so the renderer can query and switch backend runtime.wslModeandwslDistroinDesktopAppSettingswith validation and safe defaults.suppressReconnectto silence connection-state toasts during the backend swap.WslBackendSwapError.Macroscope summarized 00b200c.
Note
High Risk
Large cross-cutting change to backend spawn, env/bootstrap partitioning, and auth/session flow on swap; failures are mitigated by rollback and Windows fallback when WSL is missing, but misconfiguration or partial swap timeouts can still strand users until restart.
Overview
Adds an opt-in WSL backend for the Windows desktop: the Electron shell stays native while the local server can run inside a chosen WSL distro via
wsl.exe, with separate data homes so Windows and WSL threads do not collide.Desktop main process: New
DesktopWslEnvironment(distro list, pre-warm,wslpath, Linux home,node-ptytoolchain checks).DesktopBackendConfigurationpicks WSL vs Windows spawn: WSL uses stdin bootstrap (--bootstrap-fd 0), optional--dev-url, stripsT3CODE_HOMEfrom the child env, mergesWSLENVfor API keys, and omitst3Homein bootstrap; if WSL is requested but unavailable, it falls back to Windows instead of a stuck preflight loop.DesktopBackendManagergains configurable args/env delivery, preflight-only restart paths, andwaitForReadyfor swaps. PersistedwslMode/wslDistroin app settings with distro name validation.IPC & renderer:
getWslState/setWslBackendstop/restart the backend with a 2-minute readiness wait and automatic settings rollback on failure. Connections settings get a Backend runtime selector (confirm dialog, phased progress, reauth after swap, welcome sync).suppressReconnecthides WS toasts during intentional swaps; folder picker defaults resolve to WSL UNC paths when in WSL mode. Server bootstrap treats stdinEACCESlike other fd duplication errors under WSL.Reviewed by Cursor Bugbot for commit 00b200c. Bugbot is set up for automated code reviews on this repo. Configure here.