feat: desktop WSL onboarding + happy experience#23407
feat: desktop WSL onboarding + happy experience#23407Hona wants to merge 108 commits intoanomalyco:devfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds Electron-side WSL server onboarding and multi-distro support, replacing the old single on/off WSL toggle with discoverable WSL sidecars that can be added and selected like other servers. It also updates the app UI so WSL-backed sessions use WSL-aware path handling and server management flows.
Changes:
- Replaces boolean WSL enablement with a new Electron WSL servers API, controller, IPC surface, and WSL process helpers.
- Adds app-side WSL state/context plus new server-selection/onboarding UI for adding, updating, retrying, and displaying WSL servers.
- Adjusts routing, picker behavior, and a few supporting utilities/refactors to fit WSL-backed local workflows.
Reviewed changes
Copilot reviewed 26 out of 27 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
packages/desktop/src/index.tsx |
Removes legacy Tauri-side WSL picker/config behavior. |
packages/desktop-electron/src/renderer/index.tsx |
Wires Electron renderer platform to active WSL server state and WSL server list. |
packages/desktop-electron/src/renderer/env.d.ts |
Adds activeServer to renderer window globals. |
packages/desktop-electron/src/preload/types.ts |
Defines WSL server/runtime/state IPC types and expands wslPath. |
packages/desktop-electron/src/preload/index.ts |
Exposes WSL server IPC methods in preload. |
packages/desktop-electron/src/main/wsl.ts |
Adds WSL command helpers, probing, install, upgrade, and terminal utilities. |
packages/desktop-electron/src/main/wsl-servers.ts |
Implements WSL server controller/state management and persistence. |
packages/desktop-electron/src/main/server.ts |
Adds WSL sidecar spawn logic and shared port allocation. |
packages/desktop-electron/src/main/ipc.ts |
Registers WSL server IPC handlers and routes relaunch through injected deps. |
packages/desktop-electron/src/main/index.ts |
Boots/stops WSL servers from Electron main process and hooks new IPC deps. |
packages/desktop-electron/src/main/constants.ts |
Replaces old WSL enabled store key with WSL servers store key. |
packages/desktop-electron/src/main/apps.ts |
Makes WSL path conversion distro-aware and handles WSL UNC paths. |
packages/app/src/utils/solid-dnd.tsx |
Simplifies drag constraint transformer registration. |
packages/app/src/pages/layout.tsx |
Uses web picker for WSL sidecars and passes home-navigation callback to server dialog. |
packages/app/src/pages/home.tsx |
Mirrors WSL-aware picker/server-dialog behavior on home page. |
packages/app/src/index.ts |
Re-exports WSL context/types from app package surface. |
packages/app/src/context/wsl-servers.tsx |
Adds query-backed WSL servers context/provider. |
packages/app/src/context/server.tsx |
Tracks active server globally and treats sidecars as local servers. |
packages/app/src/context/platform.tsx |
Extends platform contract with WSL server management types/API. |
packages/app/src/context/global-sync/child-store.ts |
Extracts reusable child disposal helper and disposeAll. |
packages/app/src/context/global-sync.tsx |
Uses new disposeAll cleanup path. |
packages/app/src/components/status-popover.tsx |
Passes close handler into popover body. |
packages/app/src/components/status-popover-body.tsx |
Updates server switching flow to close popover and coordinate route changes. |
packages/app/src/components/server/server-row.tsx |
Displays WSL badge and supports explicit version override. |
packages/app/src/components/dialog-wsl-server.tsx |
Adds multi-step WSL onboarding/install/update wizard UI. |
packages/app/src/components/dialog-select-server.tsx |
Integrates WSL server add/manage/update/remove flows into server selection dialog. |
packages/app/src/app.tsx |
Mounts WSL servers provider and adjusts keyed app/router rendering. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const handleAddedWsl = async (distro: string) => { | ||
| const key = ServerConnection.Key.make(`wsl:${distro}`) | ||
| setStore("addWsl", "showWizard", false) | ||
| const conn = items().find((item) => ServerConnection.key(item) === key) |
| </Show> | ||
|
|
||
| <Show when={i.type === "http"}> | ||
| <Show when={i.type === "http" || i.type === "sidecar"}> |
| }) | ||
| const opencodeReady = createMemo(() => { | ||
| const check = opencodeCheck() | ||
| return !!check?.resolvedPath && !check.error |
| <div class="rounded-md border border-border-weak-base px-3 py-3 flex items-center justify-between gap-3"> | ||
| <div class="text-12-regular text-text-warning-base">Windows restart required.</div> | ||
| <Button variant="secondary" size="large" onClick={() => void platform.restart()}> | ||
| Relaunch OpenCode | ||
| </Button> |
| if (distro && !/^[a-zA-Z0-9_.-]+$/.test(distro)) { | ||
| return Promise.reject(new Error("Invalid distro name")) |
| async installOpencode(name: string) { | ||
| await runJob({ kind: "install-opencode", distro: name, startedAt: Date.now() }, async (abort) => { | ||
| const resolved = await resolveWslOpencode(name, { signal: abort.signal }) | ||
| const existingVersion = resolved | ||
| ? await readWslCommandVersion(resolved, name, { signal: abort.signal }) | ||
| : null | ||
| const result = | ||
| resolved && existingVersion | ||
| ? await upgradeWslOpencode(appVersion, resolved, name, { signal: abort.signal }) | ||
| : await installWslOpencode(appVersion, name, { signal: abort.signal }) | ||
| if (result.code !== 0) { | ||
| throw new Error(summarize(result.stderr || result.stdout) || "OpenCode installation failed") | ||
| } | ||
| await refreshOpencodeCheck(name, { signal: abort.signal }) | ||
| }) |
| for (const item of wslServers.data?.servers ?? []) { | ||
| const runtime = item.runtime | ||
| if (runtime.kind !== "ready") continue | ||
| list.push({ |
| const navigateHome = () => props.onNavigateHome?.() | ||
|
|
||
| const apply = () => { | ||
| dialog.close() | ||
| if (persist && conn.type === "http") { | ||
| server.add(conn) | ||
| navigateHome() | ||
| return | ||
| } | ||
|
|
||
| batch(() => { | ||
| navigateHome() | ||
| server.setActive(nextKey) |
| // joiner. If the requested distro differs from the UNC distro, we still | ||
| // translate literally — callers are responsible for only picking paths | ||
| // inside the active distro. | ||
| if (mode === "linux") { | ||
| const unc = parseWslUncPath(path) | ||
| if (unc) return `/${unc.subpath}` |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 27 changed files in this pull request and generated 8 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| createEffect(() => { | ||
| if (typeof window === "undefined") return | ||
| window.__OPENCODE__ ??= {} | ||
| window.__OPENCODE__.activeServer = state.active |
| export async function wslPath(path: string, mode: "windows" | "linux" | null, distro?: string | null): Promise<string> { | ||
| if (process.platform !== "win32") return path | ||
|
|
||
| // `\\wsl$\<distro>\...` / `\\wsl.localhost\<distro>\...` -> `/<subpath>` in | ||
| // the target distro. Do the conversion in-process rather than shelling out | ||
| // to `wslpath -u`, which mangles backslashes via wsl.exe's command-line | ||
| // joiner. If the requested distro differs from the UNC distro, we still | ||
| // translate literally — callers are responsible for only picking paths | ||
| // inside the active distro. | ||
| if (mode === "linux") { | ||
| const unc = parseWslUncPath(path) | ||
| if (unc) return `/${unc.subpath}` |
| if (distro && !/^[a-zA-Z0-9_.-]+$/.test(distro)) { | ||
| return Promise.reject(new Error("Invalid distro name")) | ||
| } |
| }) | ||
| const opencodeReady = createMemo(() => { | ||
| const check = opencodeCheck() | ||
| return !!check?.resolvedPath && !check.error |
| if (!state || state.job?.kind === "runtime") return "Checking WSL..." | ||
| if (state.pendingRestart) return "Windows needs a restart to finish installing WSL." | ||
| if (state.runtime?.available) return state.runtime.version ?? "WSL is ready." | ||
| return state.runtime?.error ?? "WSL is required to continue." | ||
| }) | ||
|
|
||
| const distroMessage = createMemo(() => { | ||
| const state = current() | ||
| if (!state) return "Checking distros..." | ||
| const distro = store.selectedDistro | ||
| if (state.job?.kind === "install-distro") return `Installing ${state.job.distro}...` | ||
| if (state.job?.kind === "probe-distro") return `Checking ${state.job.distro}...` | ||
| if (state.job?.kind === "distros") return "Listing distros..." | ||
| if (distroUnavailableMessage()) return distroUnavailableMessage()! | ||
| if (selectedProbe() && distroReady()) return `${selectedProbe()!.name} is ready.` | ||
| if (distro) return `Finishing setup for ${distro}.` | ||
| return "Pick a distro or install one below." | ||
| }) | ||
|
|
||
| const opencodeMessage = createMemo(() => { | ||
| const state = current() | ||
| if (!state) return "Checking OpenCode..." | ||
| const distro = store.selectedDistro | ||
| if (state.job?.kind === "probe-opencode" || state.job?.kind === "install-opencode") { | ||
| return distro ? `Checking OpenCode in ${distro}...` : "Checking OpenCode..." | ||
| } | ||
| if (opencodeCheck()?.error) return opencodeCheck()!.error | ||
| if (opencodeCheck()?.matchesDesktop === false) { | ||
| return distro ? `Update OpenCode in ${distro}.` : "Update OpenCode." | ||
| } | ||
| if (opencodeReady()) return distro ? `OpenCode is ready in ${distro}.` : "OpenCode is ready." | ||
| return distro ? `Install OpenCode in ${distro}.` : "Choose a distro first." |
| aria-disabled={blocked()} | ||
| onClick={() => { | ||
| if (blocked()) return | ||
| props.close?.() | ||
| navigate("/") | ||
| queueMicrotask(() => server.setActive(key)) | ||
| const activate = () => { |
| <span>{isAddMode() ? language.t("dialog.server.add.title") : language.t("dialog.server.edit.title")}</span> | ||
| <span> | ||
| {isAddWslMode() | ||
| ? "Add WSL server" |
| @@ -579,35 +737,54 @@ export function DialogSelectServer() { | |||
| /> | |||
| <DropdownMenu.Portal> | |||
| <DropdownMenu.Content class="mt-1"> | |||
Issue for this PR
Closes #
Type of change
What does this PR do?
Please provide a description of the issue, the changes you made to fix it, and why they work. It is expected that you understand why your changes work and if you do not understand why at least say as much so a maintainer knows how much to value the PR.
If you paste a large clearly AI generated description here your PR may be IGNORED or CLOSED!
How did you verify your code works?
Screenshots / recordings
If this is a UI change, please include a screenshot or recording.
Checklist
If you do not follow this template your PR will be automatically rejected.