Skip to content

Add desktop WSL backend mode#2353

Open
Jgratton24 wants to merge 34 commits intopingdotgg:mainfrom
Jgratton24:josh/desktop-wsl-backend
Open

Add desktop WSL backend mode#2353
Jgratton24 wants to merge 34 commits intopingdotgg: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.

Concretely:

  • New apps/desktop/src/wsl.ts module: WSL availability detection, distro discovery, async wslpath conversion, persisted wsl-config.json with strict distro-name validation.
  • Backend launcher in main.ts spawns through wsl.exe -- node <linuxEntry> --bootstrap-fd 0 when WSL mode is enabled. Bootstrap envelope is sent on stdin (extra stdio fds do not reliably survive the wsl.exe bridge); t3Home is omitted so the Linux backend uses its own home directory.
  • New IPC channels (wsl-list-distros, wsl-get-config, wsl-set-config) wired through the preload bridge.
  • New WSL backend section in ConnectionsSettings.tsx with a toggle and distro selector.
  • Async wslpath conversion for the bundled backend entry and folder picker selections; failed conversions fail closed instead of returning Windows paths to the WSL backend.
  • Windows process environment is preserved when spawning wsl.exe; selected API keys are forwarded via WSLENV.
  • Backend lifecycle: WSL config update awaits the old backend's exit before relaunch, drains any in-flight startBackend awaiting wslpath, and rolls back the persisted config if the new backend fails to start.
  • Folder picker preserves caller initialPath (Linux, ~, UNC) and runs wslpath inside the picked path's distro when it differs from the configured one; default UNC path resolves the actual default distro instead of hardcoding Ubuntu.
  • Server bootstrap EACCES is 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 new wsl.ts helper (224 LOC) plus its tests (251 LOC) plus a localized UI section (130 LOC). The surgery in main.ts is 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 from wsl.exe --list --verbose) plus a toggle for enabling WSL mode. Flipping the toggle opens an AlertDialog confirming 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 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 and notes that each backend keeps its own threads — flipping back returns the original list rather than wiping it.

Phased loading during a swap

Restarting backend loading state

Verification

  • bun test apps/desktop/src/wsl.test.ts apps/desktop/src/backendReadiness.test.ts
  • bun run typecheck
  • bun run build:desktop
  • bun run lint clean on apps/desktop/src

I 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:

  • Reconcile renderer state on backend swap. Each backend has its own environment-id, so toggling WSL ⇄ Windows used to leave the previous backend's threads/projects sitting in the store. The renderer now drops the previous env's slice from environmentStateById, disposes the prior EnvironmentConnection, and navigates off any /<oldEnvId>/<threadId> route the user was sitting on. startServerStateSync is keyed to the live primary id so it follows the swap.
  • Re-authenticate the desktop session after a swap. Each backend signs sessions with its own key, so the renderer's cookie 401s the new backend — including the WS upgrade, which on desktop primary authenticates via cookie (no wsToken query param). Added reauthenticatePrimaryEnvironment() that re-runs the desktop bootstrap exchange against the new backend before any reconnect, and bumped BOOTSTRAP_RETRY_TIMEOUT_MS from 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".
  • Backend-lifecycle hardening. Concurrent WSL toggle requests are serialized via a wslConfigUpdateInFlight chain in main.ts. The wsl-config.json rollback fires on synchronous throws inside startBackend (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. The node-pty rebuild step is gated on !app.isPackaged. The wslpath timeout is bumped from 3s to 10s so cold-VM starts don't surface as conversion failures.
  • Confirmation dialog and phased loading. Flipping the WSL toggle now opens an AlertDialog (see screenshots above) with the cold-start time expectation and a note that each backend keeps its own threads. The dialog button transitions through Restarting backend…Re-establishing session… so the longer cold-launch path has visible progress instead of a single static spinner.
  • UX polish in the Connections settings panel. The distro dropdown stays editable when WSL is off (selection stages locally, no backend churn). The native <select> was swapped for the design-system <Select> so the chevron lines up with the rest of the panel.
  • Misc fixes from latest bot review. scheduleBackendRestart now .catch()es synchronous rejections from the timer-fired startBackend() so they can't surface as unhandled rejections. runWslShell kills its wsl.exe child on stdin error/write failure (the timeout already did, but the error paths didn't). suppressWsConnectionLifecycle only resets the connection state on the outermost depth transition so a future nested caller can't wipe state mid-flight.

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 run the local server inside a Windows Subsystem for Linux distro

  • Adds wsl.ts with utilities to list WSL distros, parse wsl.exe output, convert Windows paths to Linux paths, build and stage a WSL-compatible node-pty binary, and load/save WSL config.
  • Extends main.ts to spawn the backend inside WSL when enabled, passing a JSON bootstrap envelope over stdin; includes pre-warm at startup, IPC handlers for distro listing and config get/set, and rollback on failed config change.
  • Adds a 'WSL backend' section in ConnectionsSettings.tsx with a distro selector and enable/disable toggle; confirming restarts the backend, reauthenticates, and waits for the new backend's welcome event before closing the dialog.
  • Introduces WS lifecycle suppression (suppressWsConnectionLifecycle) so connection status is not updated during backend restarts triggered by WSL config changes.
  • Adds nodeEngineRange.ts for semver-like engines.node range checking used during node-pty toolchain preflight.
  • Risk: switching WSL config kills the running backend and restarts it; if the new backend fails to start, config is rolled back but the app may be temporarily disconnected.

Macroscope summarized ea1348b.


Note

High Risk
High risk because it changes desktop backend startup/restart semantics, adds a new WSL-based execution path (process spawning, env/stdio forwarding, path conversion), and introduces renderer reauthentication/state-reset logic during backend swaps that can disrupt connectivity if wrong.

Overview
Adds an opt-in WSL-backed local backend mode for the Windows desktop app: the main process can now spawn the server via wsl.exe (stdin bootstrap envelope, path conversion via wslpath, selective env forwarding, node-pty Linux prebuild staging) and persists/validates a wsl-config.json.

Introduces new Desktop IPC surface (wslListDistros, wslGetConfig, wslSetConfig) and a Connections settings UI to toggle WSL mode and choose a distro; applying changes serializes updates, restarts the backend with rollback on failure, and adjusts folder picking defaults/returned paths for WSL.

Hardens lifecycle/UX around swaps: backend restart backoff is WSL-aware, the window opens even after readiness timeouts, the renderer clears old environment state and disconnects the previous primary connection, re-authenticates after a backend swap (longer transient bootstrap retry window), and suppresses WS lifecycle status churn during the deliberate restart.

Reviewed by Cursor Bugbot for commit ea1348b. 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: bfb1ae96-71b6-4ed3-a261-7677c5bca1d3

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
Comment thread apps/desktop/src/main.ts
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 significant new feature (WSL backend mode) with new user-facing controls, 500+ lines of new WSL management logic, complex backend swap coordination including session re-authentication, and cross-cutting changes to auth/runtime layers. New features of this scope require 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
Comment thread apps/desktop/src/wsl.ts
Comment thread apps/desktop/src/wsl.ts
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
@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
Comment thread apps/desktop/src/main.ts
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
Comment thread apps/desktop/src/main.ts
- 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.
WS open event swallowed during suppression (Medium):
The protocol layer's lifecycle composer gated onOpen on the global
suppression flag, but the WS event listener uses { once: true }, so a
swallowed open event is gone for that socket. If the swap finished
fast enough that the new socket opened during the suppression window,
the atom stayed at INITIAL and the UI was stuck on "connecting" until
a heartbeat timeout forced a reconnect. Fix: drop the global-suppression
gate from onOpen (per-session validity still applies) and remove the
symmetric exit-side reset in suppressWsConnectionLifecycle so the
recorded "open" state is preserved when suppression ends.

Missing timeout.unref() in listWslDistrosAsync (Low):
Match the pattern in windowsToWslPathAsync and preWarmWslAsync so the
8s probe timer doesn't keep the event loop alive during shutdown.
Comment thread apps/web/src/components/ProjectFavicon.tsx Outdated
@juliusmarminge
Copy link
Copy Markdown
Member

juliusmarminge commented May 5, 2026

anything i need to do to get it working? i just installed wsl on my windows laptop and tried activating this, but getting Error invoking remote method 'desktop:wsl-set-config': Error: Could not start the backend inside WSL.

EDIT 1: ah i must have node installed on the wsl box
image

EDIT 2: Fixed that, now issues with node-gyp:
image

When ensureWslNodePty falls through to the dev-mode rebuild, probe for
make/g++/python3 first and return a targeted message ("install
build-essential + python3") if any are absent. Without this, fresh WSL
distros surface only the trailing `gyp ERR! not ok` lines, which never
include the actual `command not found` line, leaving users with no clue
which package to install.
@Jgratton24
Copy link
Copy Markdown
Author

@juliusmarminge Just reproduced this. It's a missing toolchain in your WSL distro, not a desktop bug. node-pty is a native module and the desktop rebuilds it on first launch via node-gyp inside the distro, which needs make, g++, and python3. Ubuntu's default WSL image doesn't ship those.

Quick fix:

sudo apt install -y build-essential python3

then re-toggle WSL mode in settings.

The "exit 1 / gyp ERR! not ok" tail is unhelpful because I was truncating the build output and cutting off the actual g++: command not found line. Pushed 54d092a to add a pre-flight probe that names the missing tools up front

@juliusmarminge
Copy link
Copy Markdown
Member

do we also detect a potentially missing node binary? Related-ish to #2504

Jgratton24 added 2 commits May 5, 2026 19:53
The early return on `!src` already guarantees the variable is truthy for
the rest of the render, so the `|| !src` and `{src ? ... : null}` checks
were dead code.
A missing node binary in WSL would previously fail at line 3 of the
node-pty build script with `node: command not found` buried in the
truncated tail. Add node to the pre-flight probe and split the
remediation message so node and the build toolchain can each point at
their own install path.
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx
Comment thread apps/desktop/src/main.ts
@Jgratton24
Copy link
Copy Markdown
Author

Good catch, we weren't. Just pushed 8fd37b4 to add node to the pre-flight probe, with its own install hint (nvm instead of apt).

#2504 was useful context btw. Our WSL path uses bash -l -s so login shell PATH pulls in nvm/volta/etc already, so no need for the discovery routine you wrote for sh -s. engine range check is worth porting separately though

- ConnectionsSettings: re-authenticate after WSL toggle failures. Main
  rolls back to the previous config on failure, but the rolled-back
  backend is a fresh process with a fresh session-signing key, so the
  renderer's existing cookie was rejected with 401 by every reconnect
  attempt until the app was restarted.

- main.ts WSL stdin handler: stop scheduling a 500ms restart from the
  async stdin error listener. The listener fired after the exit handler
  was registered, so marking the child expected and racing the restart
  timer against SIGTERM could leave the app with no backend (timer fires
  → startBackend bails on still-set backendProcess → exit handler later
  returns early on wasExpected). The async path now only kills; the
  exit handler observes the unexpected exit and schedules the restart.
  The synchronous catch path (no listeners registered yet) keeps owning
  the kill + restart cycle directly.
@juliusmarminge
Copy link
Copy Markdown
Member

Yea we require quite modern node version so should be called out and verified otherwise we get bug reports for weird shit

Pre-flight now reads `apps/server`'s `engines.node` and rejects WSL
distros whose Node version is too old, naming the actual version and
the required range. Without this we'd let an outdated Node fall through
to node-gyp/runtime errors that don't mention version at all.

The semver-range check is ported from the JS used by the SSH launcher's
remote node check in PR pingdotgg#2504. When that PR merges (or this one does),
whichever lands second extracts the helper into `packages/shared` so
both call sites can drop their copy.
Comment thread apps/desktop/src/main.ts
Comment thread apps/desktop/src/main.ts
@Jgratton24
Copy link
Copy Markdown
Author

Done, just pushed 24130e4. Probe now reads engines.node from apps/server/package.json and rejects WSL distros whose Node is too old, naming both the actual version and the required range. Helper is a TS port of your satisfiesRange from #2504, kept desktop-local for now. Whichever PR merges second can lift it into packages/shared

The prologue's `isQuitting` / `backendProcess` / `backendStartInFlight`
short-circuits each have an owner for the retry elsewhere (the in-flight
start's own `scheduleBackendRestart` paths, or its child's exit handler).
Add a diagnostic log so a future regression that returns false without
scheduling a follow-up is visible in logs instead of silently stalling.
No behavior change.
Comment thread apps/desktop/src/main.ts
A synchronous throw inside startBackend (most plausibly a
ChildProcess.spawn failure before any exit handler is registered) used
to log and stop, since there was no child whose exit event could drive
the next retry. Schedule another attempt from the catch so the existing
backoff/restartTimer guard owns the retry instead of stalling.
Comment thread apps/desktop/src/wsl.ts
Comment thread apps/desktop/src/main.ts
…backoff

- listWslDistrosAsync: add the `settled` guard pattern used by sibling
  helpers. Node fires 'close' after 'error', so the close handler was
  running Buffer.concat + parseWslDistroList on partial post-error data
  even though the second resolve was a no-op.

- WSL config rollback: reset `restartAttempt` to 0 before scheduling the
  rollback restart. The counter only resets on a successful child spawn,
  so a rollback after multiple prior crashes was inheriting backoff up
  to the WSL-active cap (20s) instead of the intended 500ms base delay
  for a deliberate restart.
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx Outdated
Comment thread apps/desktop/src/main.ts
Move `setDesktopWslConfig` and `setPendingDesktopWslConfig` to fire
immediately after `wslSetConfig` returns true, instead of after the
trailing `refreshPrimaryEnvironmentDescriptor` / `reauthenticatePrimary
Environment` calls. If either of those threw, the backend was already
running the new config but the toggle UI stayed on the old position,
and re-toggling triggered an unnecessary backend restart with an
already-active config. Also rewrote the catch-block comment to cover
both failure modes (rollback in main, or in-suppress reauth that
failed) since the previous text only described the rollback case.
Comment thread apps/desktop/src/main.ts
Comment thread apps/desktop/src/main.ts
…gn abort

- Move the `restartAttempt = 0` reset off the spawn event (which fires
  for `wsl.exe` immediately and always when WSL is installed) and onto
  the listening-detector resolve. The "Listening on http://" log line
  is the actual signal that the inner backend is healthy. Previously a
  persistent WSL backend failure tight-looped every 500ms because spawn
  reset the counter on every doomed retry.

- Bootstrap dev path: only `return` when readiness aborts during quit.
  Otherwise (concurrent stopBackend during startup, e.g. user toggling
  WSL config in the first second) fall through and create the window so
  the app isn't left windowless.
Comment thread apps/server/src/provider/Layers/ClaudeProvider.ts
- Pre-warm WSL VM before spawning backend so the readiness probe
  starts against a hot relay
- Renderer holds the modal through restart -> reauth -> welcome,
  with explicit thread cleanup once the new env id is known
- Cap rollback path to 5s so a failed swap cannot strand the modal
- Drop the prior "first time" copy since cold-start cost recurs per swap
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 2 potential issues.

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 d8363f5. Configure here.

Comment thread apps/desktop/src/wsl.ts Outdated
Comment thread apps/web/src/environments/primary/context.ts
- Memoize the in-flight fetch in refreshPrimaryEnvironmentDescriptor
  so a concurrent resolveInitialPrimaryEnvironmentDescriptor caller
  shares the same network request during a backend swap
- Tighten DISTRO_NAME_PATTERN to require the trailing char be \w so
  hand-edited config like "Ubuntu " / "Ubuntu-" / "Ubuntu." rejects
  before producing broken UNC paths
@Jgratton24
Copy link
Copy Markdown
Author

Jgratton24 commented May 8, 2026

@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.

@juliusmarminge
Copy link
Copy Markdown
Member

I can do it if you don't have time. Should be "easier" to do now though with a bit more structured code 🫣

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

3 participants