diff --git a/diff.txt b/diff.txt
deleted file mode 100644
index 351288c8f6..0000000000
--- a/diff.txt
+++ /dev/null
@@ -1,4594 +0,0 @@
-diff --git a/packages/app/src/app.tsx b/packages/app/src/app.tsx
-index dbe1074484..5528523ab9 100644
---- a/packages/app/src/app.tsx
-+++ b/packages/app/src/app.tsx
-@@ -1,5 +1,7 @@
- import "@/index.css"
-+import { Button } from "@opencode-ai/ui/button"
- import { I18nProvider } from "@opencode-ai/ui/context"
-+import { useDialog } from "@opencode-ai/ui/context/dialog"
- import { DialogProvider } from "@opencode-ai/ui/context/dialog"
- import { FileComponentProvider } from "@opencode-ai/ui/context/file"
- import { MarkedProvider } from "@opencode-ai/ui/context/marked"
-@@ -26,6 +28,7 @@ import {
- Suspense,
- } from "solid-js"
- import { Dynamic } from "solid-js/web"
-+import { serverSwitching } from "@/utils/server-switch"
- import { CommandProvider } from "@/context/command"
- import { CommentsProvider } from "@/context/comments"
- import { FileProvider } from "@/context/file"
-@@ -37,6 +40,7 @@ import { LayoutProvider } from "@/context/layout"
- import { ModelsProvider } from "@/context/models"
- import { NotificationProvider } from "@/context/notification"
- import { PermissionProvider } from "@/context/permission"
-+import { usePlatform } from "@/context/platform"
- import { PromptProvider } from "@/context/prompt"
- import { ServerConnection, ServerProvider, serverName, useServer } from "@/context/server"
- import { SettingsProvider } from "@/context/settings"
-@@ -73,7 +77,7 @@ declare global {
- __OPENCODE__?: {
- updaterEnabled?: boolean
- deepLinks?: string[]
-- wsl?: boolean
-+ activeServer?: string
- }
- api?: {
- setTitlebar?: (theme: { mode: "light" | "dark" }) => Promise
-@@ -223,12 +227,15 @@ function ConnectionGate(props: ParentProps<{ disableHealthCheck?: boolean }>) {
- }
-
- function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key: ServerConnection.Key) => void }) {
-+ const dialog = useDialog()
- const language = useLanguage()
-+ const platform = usePlatform()
- const server = useServer()
- const others = () => server.list.filter((s) => ServerConnection.key(s) !== server.key)
- const name = createMemo(() => server.name || server.key)
- const serverToken = "\u0000server\u0000"
- const unreachable = createMemo(() => language.t("app.server.unreachable", { server: serverToken }).split(serverToken))
-+ const canManage = createMemo(() => server.current?.type === "sidecar" && server.current?.variant === "wsl")
-
- const timer = setInterval(() => props.onRetry?.(), 1000)
- onCleanup(() => clearInterval(timer))
-@@ -243,6 +250,34 @@ function ConnectionError(props: { onRetry?: () => void; onServerSelected?: (key:
- {unreachable()[1]}
-
- {language.t("app.server.retrying")}
-+
-+ {
-+ void import("@/components/dialog-select-server")
-+ .then((x) => {
-+ dialog.show(() => (
-+ {
-+ // We're above the Router here so useNavigate() isn't available.
-+ // Update the browser URL directly; after server.setActive fires
-+ // ServerKey remounts the Router which picks up "/" on init.
-+ // Harmless under MemoryRouter (Electron), which restarts at "/".
-+ if (typeof window !== "undefined" && window.history?.replaceState) {
-+ window.history.replaceState(null, "", "/")
-+ }
-+ }}
-+ />
-+ ))
-+ })
-+ .catch((err) => console.error("Failed to load server dialog", err))
-+ }}
-+ >
-+ Manage servers
-+
-+
-
- 0}>
-
-@@ -285,6 +320,12 @@ export function AppInterface(props: {
- router?: Component
- disableHealthCheck?: boolean
- }) {
-+ // ServerKey wraps the whole Router so that switching `server.key` throws
-+ // away any session / pty state from the previous server. Preserving the
-+ // route across servers doesn't work because session ids, pty ids, and
-+ // most URL-addressable resources are server-scoped — you'd 404 on every
-+ // fetch. The click handler that swaps servers also navigates back to "/"
-+ // so the fresh MemoryRouter doesn't try to re-resolve a now-dead URL.
- return (
-
-
-+
-+
-+
-+
-+
-
-
-
-diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx
-index dd92edec3e..93eaf0df49 100644
---- a/packages/app/src/components/dialog-select-server.tsx
-+++ b/packages/app/src/components/dialog-select-server.tsx
-@@ -8,9 +8,9 @@ import { List } from "@opencode-ai/ui/list"
- import { TextField } from "@opencode-ai/ui/text-field"
- import { useMutation } from "@tanstack/solid-query"
- import { showToast } from "@opencode-ai/ui/toast"
--import { useNavigate } from "@solidjs/router"
--import { createEffect, createMemo, createResource, onCleanup, Show } from "solid-js"
-+import { batch, createEffect, createMemo, createResource, onCleanup, Show } from "solid-js"
- import { createStore, reconcile } from "solid-js/store"
-+import { DialogWslServer } from "@/components/dialog-wsl-server"
- import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
- import { useLanguage } from "@/context/language"
- import { usePlatform } from "@/context/platform"
-@@ -19,6 +19,11 @@ import { type ServerHealth, useCheckServerHealth } from "@/utils/server-health"
-
- const DEFAULT_USERNAME = "opencode"
-
-+interface DialogSelectServerProps {
-+ initialView?: "list" | "add-wsl"
-+ onNavigateHome?: () => void
-+}
-+
- interface ServerFormProps {
- value: string
- name: string
-@@ -171,8 +176,7 @@ function ServerForm(props: ServerFormProps) {
- )
- }
-
--export function DialogSelectServer() {
-- const navigate = useNavigate()
-+export function DialogSelectServer(props: DialogSelectServerProps = {}) {
- const dialog = useDialog()
- const server = useServer()
- const platform = usePlatform()
-@@ -191,6 +195,9 @@ export function DialogSelectServer() {
- showForm: false,
- status: undefined as boolean | undefined,
- },
-+ addWsl: {
-+ showWizard: props.initialView === "add-wsl",
-+ },
- editServer: {
- id: undefined as string | undefined,
- value: "",
-@@ -354,11 +361,13 @@ export function DialogSelectServer() {
- dialog.close()
- if (persist && conn.type === "http") {
- server.add(conn)
-- navigate("/")
-+ props.onNavigateHome?.()
- return
- }
-- navigate("/")
-- queueMicrotask(() => server.setActive(ServerConnection.key(conn)))
-+ batch(() => {
-+ props.onNavigateHome?.()
-+ server.setActive(ServerConnection.key(conn))
-+ })
- }
-
- const handleAddChange = (value: string) => {
-@@ -419,7 +428,8 @@ export function DialogSelectServer() {
- )
- }
-
-- const mode = createMemo<"list" | "add" | "edit">(() => {
-+ const mode = createMemo<"list" | "add-wsl" | "add" | "edit">(() => {
-+ if (store.addWsl.showWizard) return "add-wsl"
- if (store.editServer.id) return "edit"
- if (store.addServer.showForm) return "add"
- return "list"
-@@ -433,9 +443,11 @@ export function DialogSelectServer() {
- const resetForm = () => {
- resetAdd()
- resetEdit()
-+ setStore("addWsl", "showWizard", false)
- }
-
- const startAdd = () => {
-+ setStore("addWsl", "showWizard", false)
- resetEdit()
- setStore("addServer", {
- showForm: true,
-@@ -449,6 +461,7 @@ export function DialogSelectServer() {
- }
-
- const startEdit = (conn: ServerConnection.Http) => {
-+ setStore("addWsl", "showWizard", false)
- resetAdd()
- setStore("editServer", {
- id: conn.http.url,
-@@ -461,6 +474,12 @@ export function DialogSelectServer() {
- })
- }
-
-+ const startAddWsl = () => {
-+ resetAdd()
-+ resetEdit()
-+ setStore("addWsl", "showWizard", true)
-+ }
-+
- const submitForm = () => {
- if (mode() === "add") {
- if (addMutation.isPending) return
-@@ -477,14 +496,22 @@ export function DialogSelectServer() {
-
- const isFormMode = createMemo(() => mode() !== "list")
- const isAddMode = createMemo(() => mode() === "add")
-+ const isAddWslMode = createMemo(() => mode() === "add-wsl")
- const formBusy = createMemo(() => (isAddMode() ? addMutation.isPending : editMutation.isPending))
-+ const canAddWsl = createMemo(() => !!platform.wslServers && platform.os === "windows")
-
- const formTitle = createMemo(() => {
- if (!isFormMode()) return language.t("dialog.server.title")
- return (
-
-
-- {isAddMode() ? language.t("dialog.server.add.title") : language.t("dialog.server.edit.title")}
-+
-+ {isAddWslMode()
-+ ? "Add WSL server"
-+ : isAddMode()
-+ ? language.t("dialog.server.add.title")
-+ : language.t("dialog.server.edit.title")}
-+
-
- )
- })
-@@ -495,35 +522,65 @@ export function DialogSelectServer() {
- resetEdit()
- })
-
-- async function handleRemove(url: ServerConnection.Key) {
-- server.remove(url)
-- if ((await platform.getDefaultServer?.()) === url) {
-+ async function handleRemove(key: ServerConnection.Key) {
-+ server.remove(key)
-+ if ((await platform.getDefaultServer?.()) === key) {
- void platform.setDefaultServer?.(null)
- }
- }
-
-+ async function handleRemoveWsl(conn: ServerConnection.Any) {
-+ if (conn.type !== "sidecar" || conn.variant !== "wsl") return
-+ const key = ServerConnection.key(conn)
-+ try {
-+ await platform.wslServers?.removeServer(key)
-+ server.remove(key)
-+ if ((await platform.getDefaultServer?.()) === key) {
-+ void platform.setDefaultServer?.(null)
-+ }
-+ } catch (err) {
-+ showRequestError(language, err)
-+ }
-+ }
-+
-+ async function handleRetryWsl(conn: ServerConnection.Any) {
-+ if (conn.type !== "sidecar" || conn.variant !== "wsl") return
-+ try {
-+ await platform.wslServers?.startServer(ServerConnection.key(conn))
-+ } catch (err) {
-+ showRequestError(language, err)
-+ }
-+ }
-+
- return (
--
-+
-
-
-+
-+ }
-+ >
-+
-+
- }
- >
-
x.http.url}
-+ key={(x) => ServerConnection.key(x)}
- onSelect={(x) => {
- if (x) void select(x)
- }}
-@@ -543,6 +600,7 @@ export function DialogSelectServer() {
- >
- {(i) => {
- const key = ServerConnection.key(i)
-+ const isWslSidecar = i.type === "sidecar" && i.variant === "wsl"
- return (
-
-
-@@ -562,12 +620,12 @@ export function DialogSelectServer() {
- }
- showCredentials
- />
--
-+
-
-
-
-
--
-+
-
-
-
-
-- {
-- if (i.type !== "http") return
-- startEdit(i)
-- }}
-- >
-- {language.t("dialog.server.menu.edit")}
--
--
-+
-+ {
-+ if (i.type !== "http") return
-+ startEdit(i)
-+ }}
-+ >
-+ {language.t("dialog.server.menu.edit")}
-+
-+
-+
-+ void handleRetryWsl(i)}>
-+ Retry start
-+
-+
-+
- setDefault(key)}>
-
- {language.t("dialog.server.menu.default")}
-
-
-
--
-+
- setDefault(null)}>
-
- {language.t("dialog.server.menu.defaultRemove")}
-
-
-
--
-- handleRemove(ServerConnection.key(i))}
-- class="text-text-on-critical-base hover:bg-surface-critical-weak"
-- >
-- {language.t("dialog.server.menu.delete")}
--
-+
-+
-+ (isWslSidecar ? void handleRemoveWsl(i) : handleRemove(key))}
-+ class="text-text-on-critical-base hover:bg-surface-critical-weak"
-+ >
-+
-+ {language.t("dialog.server.menu.delete")}
-+
-+
-+
-
-
-
-@@ -621,17 +690,32 @@ export function DialogSelectServer() {
-
-
-
-- {language.t("dialog.server.add.button")}
--
-+
-+
-+
-+ {language.t("dialog.server.add.button")}
-+
-+
-+
-+ Add WSL
-+
-+
-+
-+
- }
- >
-
-diff --git a/packages/app/src/components/dialog-wsl-server.tsx b/packages/app/src/components/dialog-wsl-server.tsx
-new file mode 100644
-index 0000000000..d971051eec
---- /dev/null
-+++ b/packages/app/src/components/dialog-wsl-server.tsx
-@@ -0,0 +1,611 @@
-+import { Button } from "@opencode-ai/ui/button"
-+import { useDialog } from "@opencode-ai/ui/context/dialog"
-+import { Spinner } from "@opencode-ai/ui/spinner"
-+import { showToast } from "@opencode-ai/ui/toast"
-+import { createEffect, createMemo, For, Match, onCleanup, Show, Switch } from "solid-js"
-+import { createStore, reconcile } from "solid-js/store"
-+import { useLanguage } from "@/context/language"
-+import type { WslServerStep, WslServersState } from "@/context/platform"
-+import { usePlatform } from "@/context/platform"
-+
-+const STEPS: WslServerStep[] = ["wsl", "distro", "opencode"]
-+
-+interface DialogWslServerProps {
-+ onAdded?: () => void
-+}
-+
-+export function DialogWslServer(props: DialogWslServerProps = {}) {
-+ const language = useLanguage()
-+ const platform = usePlatform()
-+ const dialog = useDialog()
-+ const [store, setStore] = createStore({
-+ state: undefined as WslServersState | undefined,
-+ loading: true,
-+ step: undefined as WslServerStep | undefined,
-+ selectedDistro: null as string | null,
-+ installTarget: undefined as string | undefined,
-+ adding: false,
-+ })
-+
-+ createEffect(() => {
-+ const wslServers = platform.wslServers
-+ if (!wslServers) return
-+ let mounted = true
-+ void wslServers
-+ .getState()
-+ .then((state) => {
-+ if (!mounted) return
-+ setStore({ state, loading: false })
-+ })
-+ .catch((err) => {
-+ if (!mounted) return
-+ requestError(language, err)
-+ setStore("loading", false)
-+ })
-+ const off = wslServers.subscribe((event) => {
-+ setStore("state", reconcile(event.state))
-+ setStore("loading", false)
-+ })
-+ onCleanup(() => {
-+ mounted = false
-+ off()
-+ })
-+ })
-+
-+ const current = () => store.state
-+ const wslServers = () => platform.wslServers
-+ const busy = createMemo(() => !!current()?.job || store.adding)
-+ const selectedDistro = () => store.selectedDistro
-+ const selectedProbe = createMemo(() => {
-+ const distro = selectedDistro()
-+ if (!distro) return null
-+ return current()?.distroProbes[distro] ?? null
-+ })
-+ const selectedInstalled = createMemo(() => {
-+ const distro = selectedDistro()
-+ if (!distro) return null
-+ return (current()?.installed ?? []).find((item) => item.name === distro) ?? null
-+ })
-+ const defaultInstalledDistro = createMemo(() => (current()?.installed ?? []).find((item) => item.isDefault) ?? null)
-+ const opencodeCheck = createMemo(() => {
-+ const distro = selectedDistro()
-+ if (!distro) return null
-+ return current()?.opencodeChecks[distro] ?? null
-+ })
-+ const distroWarningProbe = createMemo(() => {
-+ const probe = selectedProbe()
-+ if (!probe) return null
-+ if (distroReady() && !probe.isRoot) return null
-+ return probe
-+ })
-+ const distroUnavailableMessage = createMemo(() => {
-+ const probe = distroWarningProbe()
-+ const distro = selectedDistro()
-+ if (!probe || probe.canExecute || !distro) return null
-+ if (!selectedInstalled()) return `${distro} is not installed yet.`
-+ return `Open ${distro} once to finish setup.`
-+ })
-+ const distroMissingTools = createMemo(() => {
-+ const probe = distroWarningProbe()
-+ if (!probe?.canExecute) return null
-+ if (probe.hasBash && probe.hasCurl) return null
-+ return probe
-+ })
-+ const opencodeMismatchCheck = createMemo(() => {
-+ const check = opencodeCheck()
-+ return check?.matchesDesktop === false ? check : null
-+ })
-+ const existingServerDistros = createMemo(() => new Set((current()?.servers ?? []).map((item) => item.config.distro)))
-+ const addableInstalledDistros = createMemo(() => {
-+ return (current()?.installed ?? []).filter((item) => !existingServerDistros().has(item.name))
-+ })
-+ const installableDistros = createMemo(() => {
-+ const online = current()?.online ?? []
-+ const installed = new Set((current()?.installed ?? []).map((item) => item.name))
-+ const hasVersionedUbuntu = online.some((item) => /^Ubuntu-\d/.test(item.name))
-+ return online
-+ .filter((item) => !installed.has(item.name))
-+ .filter((item) => !(item.name === "Ubuntu" && hasVersionedUbuntu))
-+ })
-+ const installTarget = createMemo(() => installableDistros().find((item) => item.name === store.installTarget) ?? null)
-+ const wslReady = createMemo(() => !!current()?.runtime?.available && !current()?.pendingRestart)
-+ const distroReady = createMemo(() => {
-+ const probe = selectedProbe()
-+ if (!probe || !selectedDistro()) return false
-+ if (selectedInstalled()?.version === 1) return false
-+ return probe.canExecute && probe.hasBash && probe.hasCurl
-+ })
-+ const opencodeReady = createMemo(() => {
-+ const check = opencodeCheck()
-+ return !!check?.resolvedPath && !check.error
-+ })
-+ const allReady = createMemo(() => wslReady() && distroReady() && opencodeReady())
-+ const recommendedStep = createMemo(() => {
-+ if (!wslReady()) return "wsl"
-+ if (!distroReady()) return "distro"
-+ return "opencode"
-+ })
-+ // activeStep falls back to recommendedStep when the user hasn't picked one.
-+ // Once the user clicks a step tab we respect their choice rather than snapping
-+ // them back when a probe result updates recommendedStep.
-+ const activeStep = createMemo(() => store.step ?? recommendedStep())
-+
-+ const autoProbe = createMemo(() => {
-+ const state = current()
-+ if (!state || !wslServers() || busy()) return null
-+ if (state.pendingRestart) return null
-+ if (!state.runtime) return { key: "runtime", run: () => wslServers()!.probeRuntime() }
-+ if (!wslReady()) return null
-+ if (!state.installed.length && !state.online.length) {
-+ return { key: "distros", run: () => wslServers()!.refreshDistros() }
-+ }
-+ const distro = selectedDistro()
-+ if (distro && !state.distroProbes[distro]) {
-+ return { key: `probe-distro:${distro}`, run: () => wslServers()!.probeDistro(distro) }
-+ }
-+ if (!distro || !distroReady()) return null
-+ if (!state.opencodeChecks[distro]) {
-+ return { key: `probe-opencode:${distro}`, run: () => wslServers()!.probeOpencode(distro) }
-+ }
-+ return null
-+ })
-+
-+ let lastAutoProbe: string | null = null
-+ createEffect(() => {
-+ const probe = autoProbe()
-+ if (!probe || probe.key === lastAutoProbe) return
-+ const key = probe.key
-+ lastAutoProbe = key
-+ void (async () => {
-+ try {
-+ await probe.run()
-+ } catch (err) {
-+ // Allow the same probe to run again when reactive inputs next change
-+ // (e.g. user reselects a distro). Without this the user would be stuck
-+ // on a transient wsl.exe failure until they pick a different distro.
-+ if (lastAutoProbe === key) lastAutoProbe = null
-+ requestError(language, err)
-+ }
-+ })()
-+ })
-+
-+ createEffect(() => {
-+ const state = current()
-+ const distro = defaultInstalledDistro()
-+ if (!state || !distro || busy()) return
-+ if (selectedDistro()) return
-+ if (existingServerDistros().has(distro.name)) return
-+ setStore("selectedDistro", distro.name)
-+ })
-+
-+ createEffect(() => {
-+ const distros = installableDistros()
-+ if (!distros.length) {
-+ if (store.installTarget) setStore("installTarget", undefined)
-+ return
-+ }
-+ if (store.installTarget && distros.some((item) => item.name === store.installTarget)) return
-+ setStore("installTarget", distros[0]!.name)
-+ })
-+
-+ const wslMessage = createMemo(() => {
-+ const state = current()
-+ 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 = 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 = 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."
-+ })
-+
-+ const installProgress = createMemo(() => {
-+ const state = current()
-+ if (!state?.job) return null
-+ const transcript = state.transcript.filter((line) => line.text.trim())
-+ const title = transcript[0]?.text
-+ if (!title?.startsWith("Installing ")) return null
-+ return {
-+ title,
-+ lines: transcript.slice(1).slice(-8),
-+ }
-+ })
-+
-+ const run = async (action: () => Promise) => {
-+ try {
-+ await action()
-+ } catch (err) {
-+ requestError(language, err)
-+ }
-+ }
-+
-+ const selectDistro = (name: string) => {
-+ setStore("selectedDistro", name)
-+ setStore("step", "distro")
-+ }
-+
-+ const finish = async () => {
-+ const distro = selectedDistro()
-+ if (!distro) return
-+ const api = wslServers()
-+ if (!api) return
-+ setStore("adding", true)
-+ try {
-+ await api.addServer(distro)
-+ props.onAdded?.()
-+ dialog.close()
-+ } catch (err) {
-+ requestError(language, err)
-+ } finally {
-+ setStore("adding", false)
-+ }
-+ }
-+
-+ const steps = createMemo(() =>
-+ STEPS.map((step) => ({
-+ step,
-+ title: stepTitle(step),
-+ state: stepState(step, {
-+ active: activeStep(),
-+ wslReady: wslReady(),
-+ distroReady: distroReady(),
-+ opencodeReady: opencodeReady(),
-+ opencodeMismatch: opencodeCheck()?.matchesDesktop === false,
-+ }),
-+ locked: stepIndex(step) > stepIndex(recommendedStep()),
-+ })),
-+ )
-+
-+ return (
-+
-+ Loading...
}>
-+
-+
-+ {(item) => (
-+ setStore("step", item.step)}
-+ >
-+ {item.title}
-+
-+ )}
-+
-+
-+
-+
-+
-+
-+
-+
WSL
-+
-+ void run(() => wslServers()!.installWsl())}
-+ >
-+ Install WSL
-+
-+
-+
-+
{wslMessage()}
-+
-+
-+
Windows restart required.
-+
void platform.restart()}>
-+ Restart OpenCode
-+
-+
-+
-+
-+
-+
-+
-+
-+
Choose a distro
-+
{distroMessage()}
-+
-+
-+
0}
-+ fallback={
-+
-+ {current()?.installed.length
-+ ? "All installed distros are already added."
-+ : current()?.runtime?.available
-+ ? "No distros detected yet."
-+ : "Checking distros..."}
-+
-+ }
-+ >
-+
-+ {(item) => (
-+ selectDistro(item.name)}
-+ >
-+ {item.name}
-+
-+ {[item.isDefault ? "default" : null, item.state, item.version ? `WSL ${item.version}` : null]
-+ .filter(Boolean)
-+ .join(" · ")}
-+
-+
-+ )}
-+
-+
-+
-+
-+
0}>
-+
-+
-+
Install
-+
void run(() => wslServers()!.installDistro(installTarget()!.name))}
-+ >
-+ Install
-+
-+
-+
-+
-+ {(item) => {
-+ const selected = () => store.installTarget === item.name
-+ return (
-+ setStore("installTarget", item.name)}
-+ >
-+
-+
-+
{item.label}
-+
-+ {item.name}
-+
-+
-+
-+ )
-+ }}
-+
-+
-+
-+
-+
-+
-+
-+
-+ WSL 2 is required.
-+
-+
-+ {(message) => {message()}
}
-+
-+
-+ This distro needs bash and curl.
-+
-+
-+
-+ This distro is using the root user right now.
-+
-+
-+
-+
-+
-+
{
-+ const distro = selectedDistro()
-+ if (!distro) return
-+ void run(() => wslServers()!.openTerminal(distro))
-+ }}
-+ >
-+ Open terminal
-+
-+
-+
-+
-+
-+
-+
-+
OpenCode
-+
-+ {
-+ const distro = selectedDistro()
-+ if (!distro) return
-+ void run(() => wslServers()!.installOpencode(distro))
-+ }}
-+ >
-+ {opencodeCheck()?.resolvedPath ? "Update OpenCode" : "Install OpenCode"}
-+
-+
-+
-+
{opencodeMessage()}
-+
-+ {(check) => (
-+
-+
Path: {check().resolvedPath ?? "not found"}
-+
-+ Version: {check().version ?? "unknown"}
-+
-+ {(expected) => {` · desktop ${expected()}`} }
-+
-+
-+
-+ Installed version does not match the desktop app version.
-+
-+
-+ )}
-+
-+
-+
-+
-+
-+
-+ {(progress) => (
-+
-+
-+
{progress().title}
-+
-+
-+ {(line) => (
-+
-+ {line.text}
-+
-+ )}
-+
-+
-+
-+ )}
-+
-+
-+ 0}>
-+
-+
Diagnostics
-+
-+
{(line) => {line.text}
}
-+
-+
-+
-+
-+
-+ dialog.close()}>
-+ Cancel
-+
-+ void finish()}
-+ >
-+ {store.adding ? "Adding..." : "Add WSL server"}
-+
-+
-+
-+
-+ )
-+}
-+
-+function requestError(language: ReturnType, err: unknown) {
-+ console.error("WSL servers request failed", err instanceof Error ? (err.stack ?? err.message) : String(err))
-+ showToast({
-+ variant: "error",
-+ title: language.t("common.requestFailed"),
-+ description: err instanceof Error ? err.message : String(err),
-+ })
-+}
-+
-+function stepIndex(step: WslServerStep) {
-+ return STEPS.indexOf(step)
-+}
-+
-+function stepTitle(step: WslServerStep) {
-+ if (step === "wsl") return "WSL"
-+ if (step === "distro") return "Choose distro"
-+ return "OpenCode"
-+}
-+
-+function stepState(
-+ step: WslServerStep,
-+ state: {
-+ active: WslServerStep
-+ wslReady: boolean
-+ distroReady: boolean
-+ opencodeReady: boolean
-+ opencodeMismatch: boolean
-+ },
-+) {
-+ if (state.active === step) return "current"
-+ if (step === "wsl") return state.wslReady ? "done" : "warning"
-+ if (step === "distro")
-+ return state.distroReady ? "done" : stepIndex(step) > stepIndex(state.active) ? "locked" : "warning"
-+ return state.opencodeMismatch
-+ ? "warning"
-+ : state.opencodeReady
-+ ? "done"
-+ : stepIndex(step) > stepIndex(state.active)
-+ ? "locked"
-+ : "warning"
-+}
-diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx
-index 021e5be67e..3b4bda7f27 100644
---- a/packages/app/src/components/session/session-header.tsx
-+++ b/packages/app/src/components/session/session-header.tsx
-@@ -6,9 +6,10 @@ import { IconButton } from "@opencode-ai/ui/icon-button"
- import { Keybind } from "@opencode-ai/ui/keybind"
- import { Spinner } from "@opencode-ai/ui/spinner"
- import { showToast } from "@opencode-ai/ui/toast"
-+import { StatusPopover } from "../status-popover"
- import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
- import { getFilename } from "@opencode-ai/shared/util/path"
--import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js"
-+import { createEffect, createMemo, For, onCleanup, onMount, Show } from "solid-js"
- import { createStore } from "solid-js/store"
- import { Portal } from "solid-js/web"
- import { useCommand } from "@/context/command"
-@@ -24,7 +25,6 @@ import { useSessionLayout } from "@/pages/session/session-layout"
- import { messageAgentColor } from "@/utils/agent"
- import { decode64 } from "@/utils/base64"
- import { Persist, persisted } from "@/utils/persist"
--import { StatusPopover } from "../status-popover"
-
- const OPEN_APPS = [
- "vscode",
-@@ -129,6 +129,13 @@ const showRequestError = (language: ReturnType, err: unknown
- })
- }
-
-+function titlebarMounts() {
-+ return {
-+ center: document.getElementById("opencode-titlebar-center") as HTMLDivElement | undefined,
-+ right: document.getElementById("opencode-titlebar-right") as HTMLDivElement | undefined,
-+ }
-+}
-+
- export function SessionHeader() {
- const layout = useLayout()
- const command = useCommand()
-@@ -219,6 +226,7 @@ export function SessionHeader() {
- const [openRequest, setOpenRequest] = createStore({
- app: undefined as OpenApp | undefined,
- })
-+ const [mounts, setMounts] = createStore(titlebarMounts())
-
- const canOpen = createMemo(() => platform.platform === "desktop" && !!platform.openPath && server.isLocal())
- const current = createMemo(
-@@ -232,6 +240,19 @@ export function SessionHeader() {
- messageAgentColor(params.id ? sync.data.message[params.id] : undefined, sync.data.agent),
- )
-
-+ const syncMounts = () => {
-+ const next = titlebarMounts()
-+ if (mounts.center === next.center && mounts.right === next.right) return
-+ setMounts(next)
-+ }
-+
-+ onMount(() => {
-+ syncMounts()
-+ const observer = new MutationObserver(() => syncMounts())
-+ observer.observe(document.body, { childList: true, subtree: true })
-+ onCleanup(() => observer.disconnect())
-+ })
-+
- const selectApp = (app: OpenApp) => {
- if (!options().some((item) => item.id === app)) return
- setPrefs("app", app)
-@@ -269,12 +290,8 @@ export function SessionHeader() {
- .catch((err: unknown) => showRequestError(language, err))
- }
-
-- const [centerMount, setCenterMount] = createSignal(null)
-- const [rightMount, setRightMount] = createSignal(null)
-- onMount(() => {
-- setCenterMount(document.getElementById("opencode-titlebar-center"))
-- setRightMount(document.getElementById("opencode-titlebar-right"))
-- })
-+ const centerMount = createMemo(() => mounts.center)
-+ const rightMount = createMemo(() => mounts.right)
-
- return (
- <>
-diff --git a/packages/app/src/components/status-popover-body.tsx b/packages/app/src/components/status-popover-body.tsx
-index 0f6a1c1355..cad0b0673a 100644
---- a/packages/app/src/components/status-popover-body.tsx
-+++ b/packages/app/src/components/status-popover-body.tsx
-@@ -6,7 +6,7 @@ import { Tabs } from "@opencode-ai/ui/tabs"
- import { useMutation } from "@tanstack/solid-query"
- import { showToast } from "@opencode-ai/ui/toast"
- import { useNavigate } from "@solidjs/router"
--import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
-+import { type Accessor, batch, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js"
- import { createStore, reconcile } from "solid-js/store"
- import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row"
- import { useLanguage } from "@/context/language"
-@@ -15,6 +15,7 @@ import { useSDK } from "@/context/sdk"
- import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server"
- import { useSync } from "@/context/sync"
- import { useCheckServerHealth, type ServerHealth } from "@/utils/server-health"
-+import { setServerSwitching } from "@/utils/server-switch"
-
- const pollMs = 10_000
-
-@@ -292,8 +293,26 @@ export function StatusPopoverBody(props: { shown: Accessor }) {
- aria-disabled={blocked()}
- onClick={() => {
- if (blocked()) return
-- navigate("/")
-- queueMicrotask(() => server.setActive(key))
-+ // Paint a full-window splash BEFORE the heavy
-+ // ServerKey remount so the user gets visual
-+ // feedback during the multi-second synchronous
-+ // dispose cascade (xterm + file-tree + providers).
-+ // setTimeout(0) yields to the browser so the
-+ // splash lands on screen before the cascade
-+ // starts; a second setTimeout(0) after the batch
-+ // waits for the new subtree to paint, then
-+ // dismisses the splash.
-+ setServerSwitching(true)
-+ setTimeout(() => {
-+ try {
-+ batch(() => {
-+ navigate("/")
-+ server.setActive(key)
-+ })
-+ } finally {
-+ setTimeout(() => setServerSwitching(false), 0)
-+ }
-+ }, 0)
- }}
- >
-
-@@ -329,7 +348,10 @@ export function StatusPopoverBody(props: { shown: Accessor }) {
- const run = ++dialogRun
- void import("./dialog-select-server").then((x) => {
- if (dialogDead || dialogRun !== run) return
-- dialog.show(() => , defaultServer.refresh)
-+ dialog.show(
-+ () => navigate("/")} />,
-+ defaultServer.refresh,
-+ )
- })
- }}
- >
-diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx
-index 57e91d6d33..edbbd752c9 100644
---- a/packages/app/src/components/terminal.tsx
-+++ b/packages/app/src/components/terminal.tsx
-@@ -11,7 +11,7 @@ import { useLanguage } from "@/context/language"
- import { usePlatform } from "@/context/platform"
- import { useSDK } from "@/context/sdk"
- import { useServer } from "@/context/server"
--import { monoFontFamily, useSettings } from "@/context/settings"
-+import { terminalFontFamily, useSettings } from "@/context/settings"
- import type { LocalPTY } from "@/context/terminal"
- import { disposeIfDisposable, getHoveredLinkText, setOptionIfSupported } from "@/utils/runtime-adapters"
- import { terminalWriter } from "@/utils/terminal-writer"
-@@ -300,7 +300,7 @@ export const Terminal = (props: TerminalProps) => {
- })
-
- createEffect(() => {
-- const font = monoFontFamily(settings.appearance.font())
-+ const font = terminalFontFamily(settings.appearance.font())
- if (!term) return
- setOptionIfSupported(term, "fontFamily", font)
- scheduleFit()
-@@ -360,7 +360,7 @@ export const Terminal = (props: TerminalProps) => {
- cols: restoreSize?.cols,
- rows: restoreSize?.rows,
- fontSize: 14,
-- fontFamily: monoFontFamily(settings.appearance.font()),
-+ fontFamily: terminalFontFamily(settings.appearance.font()),
- allowTransparency: false,
- convertEol: false,
- theme: terminalColors(),
-@@ -613,17 +613,30 @@ export const Terminal = (props: TerminalProps) => {
- drop?.()
- if (ws && ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) ws.close(1000)
-
-+ // Defer finalize (persistTerminal + local cleanup()) to a microtask so
-+ // that its synchronous store write inside `persistTerminal` — which
-+ // flows through `props.onCleanup` -> `ops.update` -> `update()` in
-+ // `context/terminal.tsx` and calls `setStore("all", i, ...)` — does
-+ // NOT run inside the outer solid cleanNode cascade. Running it
-+ // synchronously mid-cascade races with solid's recursive owned
-+ // iteration (readSignal on a stale memo re-enters updateComputation,
-+ // which nulls an ancestor's owned while the outer loop is still
-+ // iterating it) and crashes with "Cannot read properties of null
-+ // (reading '1')" at node.owned[i] inside chunk-EZWYHVNM.js cleanNode.
-+ // queueMicrotask runs after the current sync reactive flush, so the
-+ // store write lands in a fresh tick.
- const finalize = () => {
- persistTerminal({ term, addon: serializeAddon, cursor, id, onCleanup: props.onCleanup })
- cleanup()
- }
-+ const schedule = () => queueMicrotask(finalize)
-
- if (!output) {
-- finalize()
-+ schedule()
- return
- }
-
-- output.flush(finalize)
-+ output.flush(schedule)
- })
-
- return (
-diff --git a/packages/app/src/context/global-sync/child-store.ts b/packages/app/src/context/global-sync/child-store.ts
-index 6788e8cc59..2d138e72f5 100644
---- a/packages/app/src/context/global-sync/child-store.ts
-+++ b/packages/app/src/context/global-sync/child-store.ts
-@@ -96,8 +96,15 @@ export function createChildStoreManager(input: {
- lifecycle.delete(directory)
- const dispose = disposers.get(directory)
- if (dispose) {
-- dispose()
- disposers.delete(directory)
-+ // Defer the actual solid-js root disposal. When disposeDirectory runs
-+ // from pinForOwner's onCleanup during a parent remount, calling
-+ // dispose() here triggers a nested cleanNode cascade on the inner
-+ // root while the outer cascade is mid-traversal, which corrupts
-+ // solid-js's graph walk state and throws `Cannot read properties of
-+ // null (reading '1')` at chunk-*.js:992. Running dispose on a
-+ // microtask lets the outer cleanup finish first.
-+ queueMicrotask(dispose)
- }
- delete children[directory]
- input.onDispose(directory)
-diff --git a/packages/app/src/context/platform.tsx b/packages/app/src/context/platform.tsx
-index 3bdc46391b..75e04a4a5b 100644
---- a/packages/app/src/context/platform.tsx
-+++ b/packages/app/src/context/platform.tsx
-@@ -9,6 +9,111 @@ type OpenFilePickerOptions = { title?: string; multiple?: boolean; accept?: stri
- type SaveFilePickerOptions = { title?: string; defaultPath?: string }
- type UpdateInfo = { updateAvailable: boolean; version?: string }
-
-+export type WslServerStep = "wsl" | "distro" | "opencode"
-+
-+export type WslRuntimeCheck = {
-+ available: boolean
-+ version: string | null
-+ status: string | null
-+ error: string | null
-+}
-+export type WslInstalledDistro = {
-+ name: string
-+ state: string | null
-+ version: number | null
-+ isDefault: boolean
-+}
-+export type WslOnlineDistro = {
-+ name: string
-+ label: string
-+}
-+export type WslDistroProbe = {
-+ name: string
-+ canExecute: boolean
-+ hasBash: boolean
-+ hasCurl: boolean
-+ username: string | null
-+ isRoot: boolean | null
-+ error: string | null
-+}
-+export type WslOpencodeCheck = {
-+ distro: string
-+ resolvedPath: string | null
-+ version: string | null
-+ expectedVersion: string | null
-+ matchesDesktop: boolean | null
-+ error: string | null
-+}
-+export type WslTranscriptLine = {
-+ stream: "stdout" | "stderr" | "system"
-+ text: string
-+ at: number
-+}
-+
-+export type WslServerAcknowledgements = {
-+ root: boolean
-+ mismatch: { path: string; version: string } | null
-+}
-+
-+export type WslServerConfig = {
-+ id: string
-+ distro: string
-+ acknowledgements: WslServerAcknowledgements
-+}
-+
-+export type WslServerRuntime =
-+ | { kind: "starting" }
-+ | { kind: "ready"; url: string; username: string | null; password: string | null }
-+ | { kind: "failed"; message: string }
-+ | { kind: "stopped" }
-+
-+export type WslServerItem = {
-+ config: WslServerConfig
-+ runtime: WslServerRuntime
-+}
-+
-+export type WslJob =
-+ | { kind: "runtime"; startedAt: number }
-+ | { kind: "distros"; startedAt: number }
-+ | { kind: "install-wsl"; startedAt: number }
-+ | { kind: "install-distro"; distro: string; startedAt: number }
-+ | { kind: "probe-distro"; distro: string; startedAt: number }
-+ | { kind: "probe-opencode"; distro: string; startedAt: number }
-+ | { kind: "install-opencode"; distro: string; startedAt: number }
-+
-+export type WslServersState = {
-+ runtime: WslRuntimeCheck | null
-+ installed: WslInstalledDistro[]
-+ online: WslOnlineDistro[]
-+ distroProbes: Record
-+ opencodeChecks: Record
-+ pendingRestart: boolean
-+ servers: WslServerItem[]
-+ job: WslJob | null
-+ transcript: WslTranscriptLine[]
-+ lastError: string | null
-+}
-+export type WslServersEvent = { type: "state"; state: WslServersState }
-+
-+export type WslServersPlatform = {
-+ getState(): Promise
-+ subscribe(cb: (event: WslServersEvent) => void): () => void
-+ probeRuntime(): Promise
-+ refreshDistros(): Promise
-+ installWsl(): Promise
-+ installDistro(name: string): Promise
-+ probeDistro(name: string): Promise
-+ probeOpencode(name: string): Promise
-+ installOpencode(name: string): Promise
-+ openTerminal(name: string): Promise
-+ addServer(distro: string): Promise
-+ removeServer(id: string): Promise
-+ startServer(id: string): Promise
-+ stopServer(id: string): Promise
-+ cancelJob(): Promise
-+ updateAcknowledgements(id: string, acks: Partial): Promise
-+}
-+
- export type Platform = {
- /** Platform discriminator */
- platform: "web" | "desktop"
-@@ -64,11 +169,8 @@ export type Platform = {
- /** Set the default server URL to use on app startup (platform-specific) */
- setDefaultServer?(url: ServerConnection.Key | null): Promise | void
-
-- /** Get the configured WSL integration (desktop only) */
-- getWslEnabled?(): Promise
--
-- /** Set the configured WSL integration (desktop only) */
-- setWslEnabled?(config: boolean): Promise | void
-+ /** Manage WSL sidecar servers (Electron on Windows only) */
-+ wslServers?: WslServersPlatform
-
- /** Get the preferred display backend (desktop only) */
- getDisplayBackend?(): Promise | DisplayBackend | null
-diff --git a/packages/app/src/context/prompt.tsx b/packages/app/src/context/prompt.tsx
-index 9b666e5e75..0d1cee7107 100644
---- a/packages/app/src/context/prompt.tsx
-+++ b/packages/app/src/context/prompt.tsx
-@@ -232,10 +232,13 @@ export const { use: usePrompt, provider: PromptProvider } = createSimpleContext(
- const cache = new Map()
-
- const disposeAll = () => {
-- for (const entry of cache.values()) {
-- entry.dispose()
-- }
-+ // Defer the dispose calls to a microtask; synchronous nested dispose
-+ // inside a parent onCleanup corrupts solid-js's in-flight cleanNode
-+ // traversal during mass remounts (see context/terminal.tsx for the
-+ // same pattern).
-+ const pending = Array.from(cache.values(), (entry) => entry.dispose)
- cache.clear()
-+ if (pending.length) queueMicrotask(() => pending.forEach((d) => d()))
- }
-
- onCleanup(disposeAll)
-diff --git a/packages/app/src/context/server.tsx b/packages/app/src/context/server.tsx
-index 1204fba557..096ef23db9 100644
---- a/packages/app/src/context/server.tsx
-+++ b/packages/app/src/context/server.tsx
-@@ -23,7 +23,7 @@ export function serverName(conn?: ServerConnection.Any, ignoreDisplayName = fals
-
- function projectsKey(key: ServerConnection.Key) {
- if (!key) return ""
-- if (key === "sidecar") return "local"
-+ if (key === "sidecar" || key === "local:windows") return "local"
- if (isLocalHost(key)) return "local"
- return key
- }
-@@ -81,7 +81,7 @@ export namespace ServerConnection {
- return Key.make(conn.http.url)
- case "sidecar": {
- if (conn.variant === "wsl") return Key.make(`wsl:${conn.distro}`)
-- return Key.make("sidecar")
-+ return Key.make("local:windows")
- }
- case "ssh":
- return Key.make(`ssh:${conn.host}`)
-@@ -200,7 +200,19 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
-
- const isReady = createMemo(() => ready() && !!state.active)
-
-- const check = (conn: ServerConnection.Any) => checkServerHealth(conn.http).then((x) => x.healthy)
-+ const check = (conn: ServerConnection.Any) =>
-+ checkServerHealth(conn.http).then((x) => {
-+ if (!x.healthy) {
-+ // Electron's console-message bridge only preserves the first
-+ // console argument, so pre-stringify everything into one string.
-+ console.warn(
-+ `[server health] unhealthy key=${ServerConnection.key(conn)} url=${conn.http.url} hasAuth=${!!(
-+ conn.http.username || conn.http.password
-+ )}`,
-+ )
-+ }
-+ return x.healthy
-+ })
-
- createEffect(() => {
- const current_ = current()
-@@ -211,9 +223,17 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
- return
- }
- setState("healthy", undefined)
-+ console.log(`[server health] start polling key=${ServerConnection.key(current_)} url=${current_.http.url}`)
- onCleanup(startHealthPolling(current_))
- })
-
-+ createEffect(() => {
-+ const key = state.active
-+ if (typeof window === "undefined") return
-+ window.__OPENCODE__ ??= {}
-+ window.__OPENCODE__.activeServer = key
-+ })
-+
- const origin = createMemo(() => projectsKey(state.active))
- const projectsList = createMemo(() => store.projects[origin()] ?? [])
- const current: Accessor = createMemo(
-@@ -221,7 +241,7 @@ export const { use: useServer, provider: ServerProvider } = createSimpleContext(
- )
- const isLocal = createMemo(() => {
- const c = current()
-- return (c?.type === "sidecar" && c.variant === "base") || (c?.type === "http" && isLocalHost(c.http.url))
-+ return c?.type === "sidecar" || (c?.type === "http" && isLocalHost(c.http.url))
- })
-
- return {
-diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx
-index a585789ce4..1534b173eb 100644
---- a/packages/app/src/context/settings.tsx
-+++ b/packages/app/src/context/settings.tsx
-@@ -53,9 +53,13 @@ export const sansDefault = "System Sans"
-
- const monoFallback =
- 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'
-+const terminalMonoFallback =
-+ '"Symbols Nerd Font Mono", "Symbols Nerd Font", "JetBrainsMono NFM", "JetBrainsMono NF", "JetBrainsMono Nerd Font Mono", "Hack Nerd Font Mono", "Hack Nerd Font", "MesloLGM Nerd Font Mono", "MesloLGM Nerd Font", "CaskaydiaCove NFM", "CaskaydiaCove Nerd Font Mono", "CaskaydiaMono Nerd Font Mono", ' +
-+ monoFallback
- const sansFallback = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif'
-
- const monoBase = monoFallback
-+const terminalMonoBase = terminalMonoFallback
- const sansBase = sansFallback
-
- function input(font: string | undefined) {
-@@ -85,6 +89,10 @@ export function monoFontFamily(font: string | undefined) {
- return stack(font, monoBase)
- }
-
-+export function terminalFontFamily(font: string | undefined) {
-+ return stack(font, terminalMonoBase)
-+}
-+
- export function sansFontFamily(font: string | undefined) {
- return stack(font, sansBase)
- }
-diff --git a/packages/app/src/context/terminal.tsx b/packages/app/src/context/terminal.tsx
-index 31d2d6e04c..482f55c716 100644
---- a/packages/app/src/context/terminal.tsx
-+++ b/packages/app/src/context/terminal.tsx
-@@ -364,10 +364,15 @@ export const { use: useTerminal, provider: TerminalProvider } = createSimpleCont
- onCleanup(() => caches.delete(cache))
-
- const disposeAll = () => {
-- for (const entry of cache.values()) {
-- entry.dispose()
-- }
-+ // Snapshot disposers, then defer them to a microtask. When this runs
-+ // from onCleanup during a parent remount (e.g. switching servers),
-+ // calling dispose() synchronously starts a nested cleanNode cascade on
-+ // a sibling root while the outer cascade is mid-traversal, corrupting
-+ // solid-js's graph walk state and throwing `Cannot read properties of
-+ // null (reading '1')` at chunk-*.js:992.
-+ const pending = Array.from(cache.values(), (entry) => entry.dispose)
- cache.clear()
-+ if (pending.length) queueMicrotask(() => pending.forEach((d) => d()))
- }
-
- onCleanup(disposeAll)
-diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts
-index d80e9fffb0..4173cf9ca7 100644
---- a/packages/app/src/index.ts
-+++ b/packages/app/src/index.ts
-@@ -1,7 +1,21 @@
- export { AppBaseProviders, AppInterface } from "./app"
-+export { DialogWslServer } from "./components/dialog-wsl-server"
- export { ACCEPTED_FILE_EXTENSIONS, ACCEPTED_FILE_TYPES, filePickerFilters } from "./constants/file-picker"
- export { useCommand } from "./context/command"
- export { loadLocaleDict, normalizeLocale, type Locale } from "./context/language"
--export { type DisplayBackend, type Platform, PlatformProvider } from "./context/platform"
-+export {
-+ type DisplayBackend,
-+ type Platform,
-+ PlatformProvider,
-+ type WslInstalledDistro,
-+ type WslOnlineDistro,
-+ type WslOpencodeCheck,
-+ type WslServerConfig,
-+ type WslServerItem,
-+ type WslServersEvent,
-+ type WslServersPlatform,
-+ type WslServersState,
-+ type WslServerStep,
-+} from "./context/platform"
- export { ServerConnection } from "./context/server"
- export { handleNotificationClick } from "./utils/notification-click"
-diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx
-index 46cacdf627..b779ebd4f5 100644
---- a/packages/app/src/pages/home.tsx
-+++ b/packages/app/src/pages/home.tsx
-@@ -75,7 +75,7 @@ export default function Home() {
- size="large"
- variant="ghost"
- class="mt-4 mx-auto text-14-regular text-text-weak"
-- onClick={() => dialog.show(() => )}
-+ onClick={() => dialog.show(() => navigate("/")} />)}
- >
- {
- if (dialogDead || dialogRun !== run) return
-- dialog.show(() =>
)
-+ dialog.show(() =>
navigate("/")} />)
- })
- }
-
-@@ -1840,7 +1847,7 @@ export default function Layout(props: ParentProps) {
- )
-
- function handleDragStart(event: unknown) {
-- const id = getDraggableId(event)
-+ const id = projectSortableWorktree(getDraggableId(event))
- if (!id) return
- setHoverProject(undefined)
- setStore("activeProject", id)
-@@ -1849,11 +1856,14 @@ export default function Layout(props: ParentProps) {
- function handleDragOver(event: DragEvent) {
- const { draggable, droppable } = event
- if (draggable && droppable) {
-+ const from = projectSortableWorktree(draggable.id?.toString())
-+ const to = projectSortableWorktree(droppable.id?.toString())
-+ if (!from || !to) return
- const projects = layout.projects.list()
-- const fromIndex = projects.findIndex((p) => p.worktree === draggable.id.toString())
-- const toIndex = projects.findIndex((p) => p.worktree === droppable.id.toString())
-+ const fromIndex = projects.findIndex((p) => p.worktree === from)
-+ const toIndex = projects.findIndex((p) => p.worktree === to)
- if (fromIndex !== toIndex && toIndex !== -1) {
-- layout.projects.move(draggable.id.toString(), toIndex)
-+ layout.projects.move(from, toIndex)
- }
- }
- }
-@@ -1891,7 +1901,7 @@ export default function Layout(props: ParentProps) {
- })
-
- function handleWorkspaceDragStart(event: unknown) {
-- const id = getDraggableId(event)
-+ const id = workspaceSortableDirectory(getDraggableId(event))
- if (!id) return
- setStore("activeWorkspace", id)
- }
-@@ -1899,13 +1909,16 @@ export default function Layout(props: ParentProps) {
- function handleWorkspaceDragOver(event: DragEvent) {
- const { draggable, droppable } = event
- if (!draggable || !droppable) return
-+ const from = workspaceSortableDirectory(draggable.id?.toString())
-+ const to = workspaceSortableDirectory(droppable.id?.toString())
-+ if (!from || !to) return
-
- const project = sidebarProject()
- if (!project) return
-
- const ids = workspaceIds(project)
-- const fromIndex = ids.findIndex((dir) => dir === draggable.id.toString())
-- const toIndex = ids.findIndex((dir) => dir === droppable.id.toString())
-+ const fromIndex = ids.findIndex((dir) => dir === from)
-+ const toIndex = ids.findIndex((dir) => dir === to)
- if (fromIndex === -1 || toIndex === -1) return
- if (fromIndex === toIndex) return
-
-@@ -2265,7 +2278,7 @@ export default function Layout(props: ParentProps) {
- }}
- class="size-full flex flex-col py-2 gap-4 overflow-y-auto no-scrollbar [overflow-anchor:none]"
- >
--
-+
-
- {(directory) => (
- layout.projects.list()
-+ const projectIds = createMemo(() => projects().map((project) => project.worktree))
- const projectOverlay = () => store.activeProject} />
- const sidebarContent = (mobile?: boolean) => (
- layout.sidebar.opened()}
- aimMove={aim.move}
- projects={projects}
-- renderProject={(project) => (
--
-- )}
-+ projectIds={projectIds}
-+ renderProject={(worktree) => {
-+ const project = createMemo(() => projects().find((item) => item.worktree === worktree))
-+ return (
-+
-+ {(project) => (
-+
-+ )}
-+
-+ )
-+ }}
- handleDragStart={handleDragStart}
- handleDragEnd={handleDragEnd}
- handleDragOver={handleDragOver}
-diff --git a/packages/app/src/pages/layout/sidebar-project.tsx b/packages/app/src/pages/layout/sidebar-project.tsx
-index 076e1ef88b..d681cf3218 100644
---- a/packages/app/src/pages/layout/sidebar-project.tsx
-+++ b/packages/app/src/pages/layout/sidebar-project.tsx
-@@ -34,6 +34,17 @@ export type ProjectSidebarContext = {
- sessionProps: Omit
- }
-
-+const PROJECT_SORTABLE_PREFIX = "project:"
-+
-+export function projectSortableId(worktree: string) {
-+ return `${PROJECT_SORTABLE_PREFIX}${worktree}`
-+}
-+
-+export function projectSortableWorktree(id: string | undefined) {
-+ if (!id?.startsWith(PROJECT_SORTABLE_PREFIX)) return
-+ return id.slice(PROJECT_SORTABLE_PREFIX.length)
-+}
-+
- export const ProjectDragOverlay = (props: {
- projects: Accessor
- activeProject: Accessor
-@@ -275,7 +286,7 @@ export const SortableProject = (props: {
- }): JSX.Element => {
- const globalSync = useGlobalSync()
- const language = useLanguage()
-- const sortable = createSortable(props.project.worktree)
-+ const sortable = createSortable(projectSortableId(props.project.worktree))
- const selected = createMemo(() => props.ctx.currentProject()?.worktree === props.project.worktree)
- const workspaces = createMemo(() => props.ctx.workspaceIds(props.project).slice(0, 2))
- const workspaceEnabled = createMemo(() => props.ctx.workspacesEnabled(props.project))
-diff --git a/packages/app/src/pages/layout/sidebar-shell.tsx b/packages/app/src/pages/layout/sidebar-shell.tsx
-index ca36af2a42..d9cd4d5a20 100644
---- a/packages/app/src/pages/layout/sidebar-shell.tsx
-+++ b/packages/app/src/pages/layout/sidebar-shell.tsx
-@@ -11,13 +11,15 @@ import { ConstrainDragXAxis } from "@/utils/solid-dnd"
- import { IconButton } from "@opencode-ai/ui/icon-button"
- import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
- import { type LocalProject } from "@/context/layout"
-+import { projectSortableId } from "./sidebar-project"
-
- export const SidebarContent = (props: {
- mobile?: boolean
- opened: Accessor
- aimMove: (event: MouseEvent) => void
- projects: Accessor
-- renderProject: (project: LocalProject) => JSX.Element
-+ projectIds: Accessor
-+ renderProject: (worktree: string) => JSX.Element
- handleDragStart: (event: unknown) => void
- handleDragEnd: () => void
- handleDragOver: (event: DragEvent) => void
-@@ -63,8 +65,8 @@ export const SidebarContent = (props: {
-
-
-