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)
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 adds a complete new feature enabling WSL backend mode on Windows, including UI controls, process spawning logic, and session reauthentication. The scope and complexity of this new capability warrants 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! |
- Drain in-flight startBackend before stopBackendAndWaitForExit so a WSL config update no longer races past a restart that is awaiting wslpath and throws a misleading error while the in-flight call spawns with stale config. - Run wslpath inside the distro the user actually picked when they select a path under \wsl.localhost\<distro>\... rather than always using wslConfig.distro, so multi-distro setups convert correctly. - Revert DEFAULT_TIMEOUT_MS in backendReadiness back to 30s; the only caller passes 60s explicitly, so the 5-minute default served no purpose and risked silently slowing future callers. - Cover extractDistroFromUncPath in wsl.test.ts.
Capture the previous on-disk WSL config before persisting the new one, and restore it if startBackend fails. Otherwise the renderer error toast leaves the UI showing the old toggle/distro while the disk has already moved to the never-confirmed configuration, which the next app launch silently honors.
- Bootstrap reader: attach an error listener to the readline Interface so the EAGAIN that the Linux pty bridge surfaces after stdin closes does not bubble up as an unhandled error and exit the backend with code 1 right after parsing the envelope. Guard the resume path so a late error fired before cleanup runs is a no-op. - node-pty Linux prebuild: node-pty's npm tarball ships prebuilds for darwin/win32 only, so a Windows-side install has no pty.node the inner Linux process can load. On WSL backend startup, probe whether node-pty loads inside the distro and, if not, run \`node-gyp rebuild\` once via wsl.exe and stage the result in prebuilds/linux-x64/. Pipe the script through stdin to avoid wsl.exe's quote re-escaping.
…installs - Queue concurrent wsl-set-config IPC calls behind a single in-flight promise (wslConfigUpdateInFlight). Two rapid toggles previously raced through stopBackend / startBackend in interleaved order, leaving the backend pinned to whichever spawn won the race and the on-disk config ahead of reality. - Always roll back the persisted wsl-config.json when the swap fails, not just when startBackend resolves to false. Wrap stopBackend / startBackend / waitForBackendWindowReady in a single try block so synchronous spawn failures, missing distros, or HTTP-readiness timeouts all unwind the on-disk config to the previous value before the IPC call rejects. Without this, an unhandled spawn throw left the renderer with a generic IPC error and the next launch silently honored a never-confirmed config. - Surface a Windows-side rollback message too, so disabling WSL with a failing Windows backend produces the same toast/log behavior as the WSL-enable failure case. - Split ensureWslNodePty's behavior with an allowBuild flag: dev builds still rebuild on demand, packaged builds only reuse a verified staged binary. Add prepareWslNodePty.ts entry point so packagers can stage the prebuild in CI via 'bun run prepare:wsl' instead of relying on a cold-path build inside the desktop process.
When the desktop swaps backends (Windows / WSL), each backend has a
separate environment-id, separate sqlite, and separate session-signing
key. Without explicit handling, the renderer kept the previous backend's
threads/projects in the store and sent the previous backend's session
cookie, which the new backend rejected with 401 — leaving the UI showing
stale data and the WS connection unable to upgrade.
- store: add a clearPrimaryEnvironmentState action that drops the
previous env's slice from environmentStateById on identity change.
Unset activeEnvironmentId if it pointed at the cleared env. Aggregate
selectors iterate the remaining entries directly.
- __root.tsx: track the last-seen primary env id in a ref and reconcile
on every welcome / config event. On identity change: dispose the prior
EnvironmentConnection, clear its store slice, and navigate the user
off any /<oldEnvId>/<threadId> route they were sitting on. Bind
startServerStateSync to the live primary id so it follows the swap.
- auth: add reauthenticatePrimaryEnvironment() that re-runs the desktop
bootstrap exchange against the new backend so the renderer holds a
cookie signed by the live key. Without this, every subsequent HTTP
request and the WS upgrade itself 401, since /ws on desktop primary
authenticates via session cookie (no wsToken query param). Bump
BOOTSTRAP_RETRY_TIMEOUT_MS from 15s to 60s to cover cold WSL launches.
- runtime/{connection,service}: thread a reportLifecycleEvents option
through dispose() so suppressed swaps do not churn the connection-status
UI; add disconnectPrimaryEnvironment for the renderer to call when it
reconciles env identity.
- rpc/{wsConnectionState,protocol,wsTransport,wsRpcClient}: introduce
suppressWsConnectionLifecycle so the settings handler can stop
reporting connect/disconnect events to the connection-status atom
during the deliberate swap window. The lifecycle gate consults a
shouldReportLifecycleEvent hook composed with the existing handlers.
- ProjectFavicon: render the placeholder folder icon when the env's
HTTP base URL is briefly unavailable (e.g. during a swap) instead of
building an invalid src URL.
- Replace the native <select> for distro selection with the design
system's <Select> component (built on @base-ui/react/select), used
elsewhere in settings. Fixes the slightly off-axis chevron and matches
the dropdown styling of the rest of the panel.
- Stop disabling the distro dropdown when WSL is off. The selection now
stages locally (stagedDesktopWslDistro) without hitting the backend;
flipping the toggle on uses the staged value as the new distro, while
changes made while WSL is already enabled still go through the existing
confirmation dialog. A useEffect mirrors the saved distro into the
staged value on first load and whenever WSL is enabled.
- Wire reauthenticatePrimaryEnvironment() into handleDesktopWslChange
so the renderer re-exchanges the desktop bootstrap token for a session
cookie signed by the new backend before any WS reconnect attempts. The
call sits inside suppressWsConnectionLifecycle alongside the descriptor
refresh so its 401-then-200 doesn't briefly surface as a connection
hiccup in the UI.
- Track a desktopWslChangeStage ('restarting-backend' / 'reauthenticating')
so the dialog button reads "Restarting backend…" then "Re-establishing
session…" instead of a single static "Restarting…" — gives the user
feedback during the longer cold-launch path.
- Expand the confirmation copy: set the time expectation ("can take up
to 30 seconds the first time") and clarify that each backend keeps its
own threads, so the previous list isn't gone — it returns when you
switch back.
Two follow-ups from the latest review pass: - main.ts WSL_SET_CONFIG_CHANNEL: when startBackend() succeeded but the readiness wait then failed (e.g. a slow first WSL boot exceeding the 60s timeout), the spawned child kept running under the new mode while the on-disk config and renderer state silently reverted, leaving runtime and persisted state inconsistent until the next backend exit. Stop the new-mode child explicitly, save the previous config, and relaunch under it; if the rollback start also fails, fall back to the exponential-backoff restart so the loop keeps trying. - wsl.ts runWslShellScript: the stdin "error" handler and the catch around stdin.write/end called settle() without first killing the spawned wsl.exe child, so a stdin failure after spawn left the child running with no termination path (the timeout that would have killed it is cleared by settle()). Kill the child before settle() in those paths to match the timeout-handler shape.
f241be6 to
017f1d6
Compare
scheduleBackendRestart fires startBackend() from a setTimeout callback and discards the result with `void`. startBackend is async, so any synchronous throw inside it — a bad spawn argv, a rejection bubbling out of windowsToWslPathAsync or ensureWslNodePty — would otherwise surface as an unhandled promise rejection at runtime, which Electron flags as a noisy warning. Log the failure and let the next backend lifecycle event drive the retry loop instead.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 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 815bdee. Configure here.
- wsl.ts windowsToWslPathAsync: bump the wslpath child timeout from 3s to 10s. The first wsl.exe invocation against a cold distro routinely spends a few seconds spinning up the LX VM before wslpath even runs, and 3s was tight enough to occasionally surface "wslpath conversion failed" errors in the folder picker (which throws on null) and exponential-backoff restart noise in startBackend. - wsConnectionState.ts suppressWsConnectionLifecycle: only call resetWsConnectionState on the outermost depth transition (0 → 1 and 1 → 0). The previous unconditional reset would wipe connection state on every inner finally, leaving the atom stuck at the initial value if a nested caller exited while an outer suppression window was still open. No nested caller exists today; this is defensive.
28281c2 to
443ec84
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.
Concretely:
apps/desktop/src/wsl.tsmodule: WSL availability detection, distro discovery, asyncwslpathconversion, persistedwsl-config.jsonwith strict distro-name validation.main.tsspawns throughwsl.exe -- node <linuxEntry> --bootstrap-fd 0when WSL mode is enabled. Bootstrap envelope is sent on stdin (extra stdio fds do not reliably survive thewsl.exebridge);t3Homeis omitted so the Linux backend uses its own home directory.wsl-list-distros,wsl-get-config,wsl-set-config) wired through the preload bridge.ConnectionsSettings.tsxwith a toggle and distro selector.wslpathconversion for the bundled backend entry and folder picker selections; failed conversions fail closed instead of returning Windows paths to the WSL backend.wsl.exe; selected API keys are forwarded viaWSLENV.startBackendawaitingwslpath, and rolls back the persisted config if the new backend fails to start.initialPath(Linux,~, UNC) and runswslpathinside the picked path's distro when it differs from the configured one; default UNC path resolves the actual default distro instead of hardcodingUbuntu.EACCESis now 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 ~75% of the diff is self-contained: the newwsl.tshelper (224 LOC) plus its tests (251 LOC) plus a localized UI section (130 LOC). The surgery inmain.tsis the irreducible core — async path conversion, env forwarding, and the WSL-aware lifecycle gates. 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 "WSL backend" section to the Connections settings panel: a design-system
<Select>for the distro (populated fromwsl.exe --list --verbose) plus a toggle for enabling WSL mode. Flipping the toggle opens anAlertDialogconfirming the swap, which surfaces phased loading text ("Restarting backend…" → "Re-establishing session…") while the backend restarts and the renderer re-bootstraps. 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 and notes that each backend keeps its own threads — flipping back returns the original list rather than wiping it.
Phased loading during a swap
Verification
bun test apps/desktop/src/wsl.test.ts apps/desktop/src/backendReadiness.test.tsbun run typecheckbun run build:desktopbun run lintclean onapps/desktop/srcI also exercised the WSL launch path locally on Windows / Ubuntu. The backend process accepted the stdin bootstrap and created Linux-side state/log files under
/tmp/t3code-wsl-smoke-final. A separate headless loopback probe did not observe HTTP readiness before my command timeout in this environment, so the runtime smoke is partial rather than a full end-to-end UI pass.Updates since first review
Follow-up commits on top of
f8f22994:environmentStateById, disposes the priorEnvironmentConnection, and navigates off any/<oldEnvId>/<threadId>route the user was sitting on.startServerStateSyncis keyed to the live primary id so it follows the swap.wsTokenquery param). AddedreauthenticatePrimaryEnvironment()that re-runs the desktop bootstrap exchange against the new backend before any reconnect, and bumpedBOOTSTRAP_RETRY_TIMEOUT_MSfrom 15s to 60s to cover cold WSL launches. WS connection-status events are suppressed during the deliberate swap window so the UI doesn't flash "disconnected".wslConfigUpdateInFlightchain inmain.ts. Thewsl-config.jsonrollback fires on synchronous throws insidestartBackend(not just async failures) and now also stops a backend that started but failed its readiness wait, then relaunches under the previous config so runtime and persisted state stay aligned. Thenode-ptyrebuild step is gated on!app.isPackaged. Thewslpathtimeout is bumped from 3s to 10s so cold-VM starts don't surface as conversion failures.AlertDialog(see screenshots above) with the cold-start time expectation and a note that each backend keeps its own threads. The dialog button transitions throughRestarting backend…→Re-establishing session…so the longer cold-launch path has visible progress instead of a single static spinner.<select>was swapped for the design-system<Select>so the chevron lines up with the rest of the panel.scheduleBackendRestartnow.catch()es synchronous rejections from the timer-firedstartBackend()so they can't surface as unhandled rejections.runWslShellkills itswsl.exechild on stdin error/write failure (the timeout already did, but the error paths didn't).suppressWsConnectionLifecycleonly resets the connection state on the outermost depth transition so a future nested caller can't wipe state mid-flight.Checklist
Note
Add WSL backend mode to the desktop app
wsl.tsmodule with utilities for detecting WSL availability, listing distros, converting Windows paths viawslpath, executing bash scripts inside WSL, and building/staging a Linuxnode-ptybinary when missing.main.tsto launch the backend process inside WSL when enabled, forwarding select env vars viaWSLENVand writing the bootstrap envelope via stdin instead of fd 3.wslListDistros,wslGetConfig,wslSetConfig) wired through preload and exposed onDesktopBridgeinipc.ts.ConnectionsSettings.tsxwhere users can enable WSL mode and select a distro; changes go through a confirmation dialog, restart the backend, refresh the environment descriptor, and reauthenticate.suppressWsConnectionLifecycle) so in-progress reconnect logic is paused during backend restarts triggered by WSL config changes.Macroscope summarized 443ec84.
Note
Medium Risk
Medium risk: changes backend launch/restart flow (including spawning via
wsl.exe, config persistence/rollback, and bootstrap pipe handling) and adjusts renderer session/WS lifecycle to handle backend identity swaps, which could impact startup reliability and connectivity on Windows.Overview
Adds an opt-in Windows desktop WSL backend mode that can launch the local backend inside a selected WSL distribution, including path conversion (
wslpath), environment forwarding, and a one-timenode-ptyLinux build/staging helper.The desktop main process now exposes WSL management over IPC (
wsl-list-distros,wsl-get-config,wsl-set-config), persistswsl-config.json, serializes/rolls back config changes on failure, and makes folder picking WSL-aware (UNC default paths + Windows→WSL conversion).On the web UI, Connections settings gains a WSL toggle + distro selector that restarts the backend and then refreshes the primary environment descriptor and reauthenticates to obtain a new session cookie; websocket lifecycle reporting is suppressible during the swap, and the app now disconnects/clears old primary-environment state when the backend’s environment id changes. Also includes small robustness fixes (server bootstrap error handling under WSL, longer bootstrap retry timeout, and minor readiness/favicon/test updates).
Reviewed by Cursor Bugbot for commit 443ec84. Bugbot is set up for automated code reviews on this repo. Configure here.