diff --git a/packages/app/src/components/dialog-local-server.tsx b/packages/app/src/components/dialog-local-server.tsx index 5d01d71e11..8739a27d04 100644 --- a/packages/app/src/components/dialog-local-server.tsx +++ b/packages/app/src/components/dialog-local-server.tsx @@ -1,6 +1,7 @@ import { Button } from "@opencode-ai/ui/button" +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 { createEffect, createMemo, For, Match, on, onCleanup, Show, Switch } from "solid-js" import { createStore, reconcile, unwrap } from "solid-js/store" import { useLanguage } from "@/context/language" import type { LocalServerConfig, LocalServerState, LocalServerStep } from "@/context/platform" @@ -15,6 +16,7 @@ export function DialogLocalServer(props: { targetMode?: "windows" | "wsl" }) { state: undefined as LocalServerState | undefined, loading: true, step: undefined as LocalServerStep | undefined, + installTarget: undefined as string | undefined, }) createEffect(() => { @@ -47,16 +49,54 @@ export function DialogLocalServer(props: { targetMode?: "windows" | "wsl" }) { const targetMode = createMemo<"windows" | "wsl">( () => props.targetMode ?? (current()?.config.mode === "wsl" ? "wsl" : "wsl"), ) + const configuredDistro = createMemo(() => current()?.config.distro ?? null) const busy = createMemo(() => !!current()?.job) - const selectedProbe = createMemo(() => current()?.checks.distro?.selected) + const selectedProbe = createMemo(() => { + const probe = current()?.checks.distro?.selected + return probe?.name === configuredDistro() ? probe : null + }) const selectedInstalled = createMemo(() => (current()?.checks.distro?.installed ?? []).find((item) => item.name === current()?.config.distro), ) - const otherDistros = createMemo(() => - (current()?.checks.distro?.online ?? []) - .filter((item) => item.name !== "Debian" && item.name !== "Ubuntu-24.04") - .slice(0, 8), + const defaultInstalledDistro = createMemo( + () => (current()?.checks.distro?.installed ?? []).find((item) => item.isDefault) ?? null, ) + const opencodeCheck = createMemo(() => { + const check = current()?.checks.opencode + return check?.distro === configuredDistro() ? check : 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 = configuredDistro() + 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 installableDistros = createMemo(() => { + const online = current()?.checks.distro?.online ?? [] + const installed = new Set((current()?.checks.distro?.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 configuredRuntime = createMemo(() => { const state = current() if (!state) return { mode: "windows" as const, distro: null as string | null } @@ -77,7 +117,10 @@ export function DialogLocalServer(props: { targetMode?: "windows" | "wsl" }) { if (selectedInstalled()?.version === 1) return false return probe.canExecute && probe.hasBash && probe.hasCurl }) - const opencodeReady = createMemo(() => !!current()?.checks.opencode?.resolvedPath) + const opencodeReady = createMemo(() => { + const check = opencodeCheck() + return !!check?.resolvedPath && !check.error && check.matchesDesktop !== false + }) const switchReady = createMemo(() => wslReady() && distroReady() && opencodeReady()) const recommendedStep = createMemo(() => { if (targetMode() === "windows") return "switch" @@ -86,12 +129,102 @@ export function DialogLocalServer(props: { targetMode?: "windows" | "wsl" }) { if (!opencodeReady()) return "opencode" return "switch" }) - const activeStep = createMemo(() => store.step ?? current()?.job?.step ?? recommendedStep()) + const activeStep = createMemo(() => current()?.job?.step ?? store.step ?? recommendedStep()) + + createEffect( + on(recommendedStep, (next) => { + setStore("step", next) + }), + ) + + const autoProbe = createMemo(() => { + const state = current() + if (!state || !localServer() || busy() || targetMode() === "windows") return null + if (state.config.onboarding.pendingRestart) return null + if (!state.checks.wsl) return { key: "wsl", step: "wsl" as const } + if (!wslReady()) return null + if (!state.checks.distro) return { key: "distro:list", step: "distro" as const } + if (state.config.distro && !selectedProbe()) { + return { key: `distro:${state.config.distro}`, step: "distro" as const } + } + if (!state.config.distro || !distroReady()) return null + if (!opencodeCheck()) { + return { key: `opencode:${state.config.distro}`, step: "opencode" as const } + } + return null + }) + + let lastAutoProbe: string | null = null + createEffect(() => { + const probe = autoProbe() + if (!probe || probe.key === lastAutoProbe) return + lastAutoProbe = probe.key + void run(() => localServer()!.runStep(probe.step)) + }) createEffect(() => { - const next = current()?.job?.step ?? recommendedStep() - if (!store.step || stepIndex(store.step) > stepIndex(next)) { - setStore("step", next) + const state = current() + const distro = defaultInstalledDistro() + if (!state || !distro || !localServer() || busy() || targetMode() !== "wsl") return + if (state.config.distro) return + void selectDistro(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?.step === "wsl") return "Checking WSL..." + if (state.config.onboarding.pendingRestart) return "Windows needs a restart to finish installing WSL." + if (state.checks.wsl?.available) return state.checks.wsl.version ?? "WSL is ready." + return state.checks.wsl?.error ?? "WSL is required to continue." + }) + + const distroMessage = createMemo(() => { + const state = current() + if (!state) return "Checking distros..." + if (state.job?.step === "distro") { + if (state.config.distro && !selectedInstalled()) return `Installing ${state.config.distro}...` + return state.config.distro ? `Checking ${state.config.distro}...` : "Checking distros..." + } + if (distroUnavailableMessage()) return distroUnavailableMessage()! + if (state.checks.distro?.error && !selectedProbe()) return state.checks.distro.error + if (selectedProbe() && distroReady()) return `${selectedProbe()!.name} is ready.` + if (state.config.distro) return `Finishing setup for ${state.config.distro}.` + return "Pick a distro or install one below." + }) + + const opencodeMessage = createMemo(() => { + const state = current() + if (!state) return "Checking OpenCode..." + if (state.job?.step === "opencode") { + return state.config.distro ? `Checking OpenCode in ${state.config.distro}...` : "Checking OpenCode..." + } + if (opencodeCheck()?.error) return opencodeCheck()!.error + if (opencodeCheck()?.matchesDesktop === false) { + return state.config.distro ? `Update OpenCode in ${state.config.distro}.` : "Update OpenCode." + } + if (opencodeReady()) + return state.config.distro ? `OpenCode is ready in ${state.config.distro}.` : "OpenCode is ready." + return state.config.distro ? `Install OpenCode in ${state.config.distro}.` : "Choose a distro first." + }) + const installProgress = createMemo(() => { + const state = current() + if (!state?.job || state.status.kind !== "running") 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), } }) @@ -192,16 +325,8 @@ export function DialogLocalServer(props: { targetMode?: "windows" | "wsl" }) {
-
Verify WSL
-
- +
WSL
+ -
-
-
- {current()?.checks.wsl?.error ?? - current()?.checks.wsl?.status ?? - current()?.checks.wsl?.version ?? - "WSL has not been checked yet."} +
+
{wslMessage()}
Windows restart required.
@@ -231,61 +351,17 @@ export function DialogLocalServer(props: { targetMode?: "windows" | "wsl" }) {
-
-
Choose a distro
- -
-
- {current()?.checks.distro?.error ?? current()?.config.distro ?? "Pick a distro or install one below."} -
- -
- - -
- - 0}> -
- - {(item) => ( - - )} - -
-
+
Choose a distro
+
{distroMessage()}
0} - fallback={
No distros detected yet.
} + fallback={ +
+ {current()?.checks.distro ? "No distros detected yet." : "Checking distros..."} +
+ } > {(item) => ( @@ -307,29 +383,90 @@ export function DialogLocalServer(props: { targetMode?: "windows" | "wsl" }) {
- - {(probe) => ( -
-
- User: {probe().username ?? "unknown"} - {probe().isRoot ? " · root" : ""} - {selectedInstalled()?.version === 1 ? " · WSL 1" : ""} -
-
- bash: {probe().hasBash ? "yes" : "no"} · curl: {probe().hasCurl ? "yes" : "no"} · exec:{" "} - {probe().canExecute ? "yes" : "no"} -
- -
WSL 2 is required.
-
+ 0}> +
+
+
Install
+
- )} +
+ + {(item) => { + const selected = () => store.installTarget === item.name + return ( + + ) + }} + +
+
+
+ + +
+ +
WSL 2 is required.
+
+ + {(message) =>
{message()}
} +
+ +
This distro needs bash and curl.
+
+ +
+ This distro is using the root user right now. +
+
+
+
OpenCode
+ -
+
-
- {current()?.checks.opencode?.error ?? - current()?.checks.opencode?.resolvedPath ?? - "OpenCode has not been checked in this distro yet."} -
- +
{opencodeMessage()}
+ {(check) => (
Path: {check().resolvedPath ?? "not found"}
@@ -375,11 +500,9 @@ export function DialogLocalServer(props: { targetMode?: "windows" | "wsl" }) { {(expected) => {` · desktop ${expected()}`}}
- -
- Installed version does not match the desktop app version. -
-
+
+ Installed version does not match the desktop app version. +
)}
@@ -389,9 +512,7 @@ export function DialogLocalServer(props: { targetMode?: "windows" | "wsl" }) {
-
- {targetMode() === "windows" ? "Swap to Windows" : "Switch Local Server"} -
+
Switch