mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-22 03:45:23 +00:00
feat: turn local server setup into guided step flow
This commit is contained in:
@@ -1,17 +1,20 @@
|
||||
import { Button } from "@opencode-ai/ui/button"
|
||||
import { showToast } from "@opencode-ai/ui/toast"
|
||||
import { createEffect, createMemo, For, onCleanup, Show } from "solid-js"
|
||||
import { createEffect, createMemo, For, Match, onCleanup, Show, Switch } from "solid-js"
|
||||
import { createStore, reconcile, unwrap } from "solid-js/store"
|
||||
import { useLanguage } from "@/context/language"
|
||||
import type { LocalServerConfig, LocalServerState } from "@/context/platform"
|
||||
import type { LocalServerConfig, LocalServerState, LocalServerStep } from "@/context/platform"
|
||||
import { usePlatform } from "@/context/platform"
|
||||
|
||||
const STEP_ORDER: LocalServerStep[] = ["wsl", "distro", "opencode", "switch"]
|
||||
|
||||
export function DialogLocalServer() {
|
||||
const language = useLanguage()
|
||||
const platform = usePlatform()
|
||||
const [store, setStore] = createStore({
|
||||
state: undefined as LocalServerState | undefined,
|
||||
loading: true,
|
||||
step: undefined as LocalServerStep | undefined,
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
@@ -43,7 +46,10 @@ export function DialogLocalServer() {
|
||||
const localServer = () => platform.localServer
|
||||
const busy = createMemo(() => !!current()?.job)
|
||||
const mode = createMemo(() => current()?.config.mode ?? "windows")
|
||||
const selected = createMemo(() => current()?.checks.distro?.selected)
|
||||
const selectedProbe = createMemo(() => current()?.checks.distro?.selected)
|
||||
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")
|
||||
@@ -62,6 +68,33 @@ export function DialogLocalServer() {
|
||||
if (!state) return false
|
||||
return state.runtime.mode !== configuredRuntime().mode || state.runtime.distro !== configuredRuntime().distro
|
||||
})
|
||||
const wslReady = createMemo(() => !!current()?.checks.wsl?.available && !current()?.config.onboarding.pendingRestart)
|
||||
const distroReady = createMemo(() => {
|
||||
const probe = selectedProbe()
|
||||
if (!probe || !current()?.config.distro) return false
|
||||
if (selectedInstalled()?.version === 1) return false
|
||||
return probe.canExecute && probe.hasBash && probe.hasCurl
|
||||
})
|
||||
const opencodeReady = createMemo(() => !!current()?.checks.opencode?.resolvedPath)
|
||||
const switchReady = createMemo(() => wslReady() && distroReady() && opencodeReady())
|
||||
const recommendedStep = createMemo<LocalServerStep>(() => {
|
||||
if (!wslReady()) return "wsl"
|
||||
if (!distroReady()) return "distro"
|
||||
if (!opencodeReady()) return "opencode"
|
||||
return "switch"
|
||||
})
|
||||
const activeStep = createMemo(() => store.step ?? current()?.job?.step ?? recommendedStep())
|
||||
|
||||
createEffect(() => {
|
||||
if (mode() !== "wsl") {
|
||||
if (store.step) setStore("step", undefined)
|
||||
return
|
||||
}
|
||||
const next = current()?.job?.step ?? recommendedStep()
|
||||
if (!store.step || stepIndex(store.step) > stepIndex(next)) {
|
||||
setStore("step", next)
|
||||
}
|
||||
})
|
||||
|
||||
const run = async (action: () => Promise<void>) => {
|
||||
try {
|
||||
@@ -77,6 +110,7 @@ export function DialogLocalServer() {
|
||||
const state = current()
|
||||
if (!state || !localServer()) return
|
||||
const config = plainConfig(state.config)
|
||||
if (next === "wsl") setStore("step", "wsl")
|
||||
await run(() =>
|
||||
localServer()!.setConfig({
|
||||
...config,
|
||||
@@ -85,7 +119,7 @@ export function DialogLocalServer() {
|
||||
...config.onboarding,
|
||||
complete: next === "windows",
|
||||
pendingRestart: next === "windows" ? false : config.onboarding.pendingRestart,
|
||||
step: next === "windows" ? null : config.onboarding.step,
|
||||
step: next === "windows" ? null : (config.onboarding.step ?? "wsl"),
|
||||
},
|
||||
}),
|
||||
)
|
||||
@@ -95,6 +129,7 @@ export function DialogLocalServer() {
|
||||
const state = current()
|
||||
if (!state || !localServer()) return
|
||||
const config = plainConfig(state.config)
|
||||
setStore("step", "distro")
|
||||
await run(() =>
|
||||
localServer()!.setConfig({
|
||||
...config,
|
||||
@@ -109,6 +144,33 @@ export function DialogLocalServer() {
|
||||
)
|
||||
}
|
||||
|
||||
const steps = createMemo(() =>
|
||||
STEP_ORDER.map((step) => ({
|
||||
step,
|
||||
title: stepTitle(step),
|
||||
subtitle: stepSubtitle(step, {
|
||||
current: current(),
|
||||
selectedInstalled: selectedInstalled(),
|
||||
selectedProbe: selectedProbe(),
|
||||
wslReady: wslReady(),
|
||||
distroReady: distroReady(),
|
||||
opencodeReady: opencodeReady(),
|
||||
switchReady: switchReady(),
|
||||
needsRestart: needsRestart(),
|
||||
}),
|
||||
locked: stepIndex(step) > stepIndex(recommendedStep()),
|
||||
state: stepState(step, {
|
||||
active: activeStep(),
|
||||
current: current(),
|
||||
wslReady: wslReady(),
|
||||
distroReady: distroReady(),
|
||||
opencodeReady: opencodeReady(),
|
||||
switchReady: switchReady(),
|
||||
needsRestart: needsRestart(),
|
||||
}),
|
||||
})),
|
||||
)
|
||||
|
||||
return (
|
||||
<div class="px-5 pb-5 flex flex-col gap-4">
|
||||
<Show
|
||||
@@ -116,242 +178,329 @@ export function DialogLocalServer() {
|
||||
fallback={<div class="px-1 py-6 text-14-regular text-text-weak">Loading local server...</div>}
|
||||
>
|
||||
<div class="rounded-md bg-surface-base p-4 flex flex-col gap-3">
|
||||
<div class="text-14-medium text-text-strong">Runtime</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button
|
||||
variant={mode() === "windows" ? "primary" : "secondary"}
|
||||
size="large"
|
||||
onClick={() => void setMode("windows")}
|
||||
>
|
||||
Run on Windows
|
||||
</Button>
|
||||
<Button
|
||||
variant={mode() === "wsl" ? "primary" : "secondary"}
|
||||
size="large"
|
||||
onClick={() => void setMode("wsl")}
|
||||
>
|
||||
Run in WSL
|
||||
</Button>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="flex flex-col gap-1 min-w-0">
|
||||
<div class="text-14-medium text-text-strong">Local runtime</div>
|
||||
<div class="text-12-regular text-text-weak">Choose where the managed Local Server should run.</div>
|
||||
</div>
|
||||
<div class="flex gap-2 shrink-0">
|
||||
<Button
|
||||
variant={mode() === "windows" ? "primary" : "secondary"}
|
||||
size="large"
|
||||
onClick={() => void setMode("windows")}
|
||||
>
|
||||
Run on Windows
|
||||
</Button>
|
||||
<Button
|
||||
variant={mode() === "wsl" ? "primary" : "secondary"}
|
||||
size="large"
|
||||
onClick={() => void setMode("wsl")}
|
||||
>
|
||||
Run in WSL
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-12-regular text-text-weak">
|
||||
Current runtime:{" "}
|
||||
{current()?.runtime.mode === "wsl"
|
||||
? `wsl${current()?.runtime.distro ? `:${current()?.runtime.distro}` : ""}`
|
||||
: "windows"}
|
||||
{current()?.runtime.mode === "wsl" ? `wsl:${current()?.runtime.distro ?? "unknown"}` : "windows"}
|
||||
</div>
|
||||
<Show when={needsRestart()}>
|
||||
<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-weak">Restart OpenCode to apply local runtime changes.</div>
|
||||
<Button variant="secondary" size="large" onClick={() => void platform.restart()}>
|
||||
Restart OpenCode
|
||||
</Button>
|
||||
<Show when={mode() !== "wsl"}>
|
||||
<div class="rounded-md border border-border-weak-base px-3 py-3 text-12-regular text-text-weak">
|
||||
Select <span class="text-text-strong">Run in WSL</span> to start the WSL setup flow.
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={mode() === "wsl"}>
|
||||
<div class="rounded-md bg-surface-base p-4 flex flex-col gap-3">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex flex-col gap-1 min-w-0">
|
||||
<div class="text-14-medium text-text-strong">WSL</div>
|
||||
<div class="text-12-regular text-text-weak whitespace-pre-wrap break-words">
|
||||
{current()?.checks.wsl?.error ??
|
||||
current()?.checks.wsl?.status ??
|
||||
current()?.checks.wsl?.version ??
|
||||
"Not checked yet"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy()}
|
||||
onClick={() => void run(() => localServer()!.runStep("wsl"))}
|
||||
>
|
||||
Check WSL
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy()}
|
||||
onClick={() => void run(() => localServer()!.installWsl())}
|
||||
>
|
||||
Install WSL
|
||||
</Button>
|
||||
</div>
|
||||
<div class="text-14-medium text-text-strong">Setup flow</div>
|
||||
<div class="flex gap-2 overflow-x-auto pb-1">
|
||||
<For each={steps()}>
|
||||
{(item) => (
|
||||
<button
|
||||
type="button"
|
||||
class="min-w-[148px] rounded-md border px-3 py-3 text-left transition-colors"
|
||||
classList={{
|
||||
"border-border-strong-base bg-surface-base-hover": item.state === "current",
|
||||
"border-icon-success-base/40 bg-surface-base": item.state === "done",
|
||||
"border-border-weak-base bg-background-base opacity-60": item.state === "locked",
|
||||
"border-icon-warning-base/40 bg-surface-base": item.state === "warning",
|
||||
}}
|
||||
disabled={item.locked}
|
||||
onClick={() => setStore("step", item.step)}
|
||||
>
|
||||
<div class="text-11-medium uppercase tracking-wide text-text-weaker">{stepNumber(item.step)}</div>
|
||||
<div class="mt-1 text-13-medium text-text-strong">{item.title}</div>
|
||||
<div class="mt-1 text-11-regular text-text-weak whitespace-pre-wrap break-words">
|
||||
{item.subtitle}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<Show when={current()?.config.onboarding.pendingRestart}>
|
||||
<div class="text-12-regular text-text-warning-base">
|
||||
Windows restart required to finish WSL installation.
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md bg-surface-base p-4 flex flex-col gap-3">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex flex-col gap-1 min-w-0">
|
||||
<div class="text-14-medium text-text-strong">Distro</div>
|
||||
<div class="text-12-regular text-text-weak whitespace-pre-wrap break-words">
|
||||
{current()?.checks.distro?.error ??
|
||||
selected()?.name ??
|
||||
current()?.config.distro ??
|
||||
"No distro selected"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy()}
|
||||
onClick={() => void run(() => localServer()!.runStep("distro"))}
|
||||
>
|
||||
Check distros
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy()}
|
||||
onClick={() => void run(() => localServer()!.installDistro("Debian"))}
|
||||
>
|
||||
Install Debian
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy()}
|
||||
onClick={() => void run(() => localServer()!.installDistro("Ubuntu-24.04"))}
|
||||
>
|
||||
Install Ubuntu 24
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Show when={otherDistros().length > 0}>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-12-medium text-text-strong">Other distros</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<For each={otherDistros()}>
|
||||
{(item) => (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy()}
|
||||
onClick={() => void run(() => localServer()!.installDistro(item.name))}
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-12-medium text-text-strong">Installed distros</div>
|
||||
<Show
|
||||
when={(current()?.checks.distro?.installed?.length ?? 0) > 0}
|
||||
fallback={<div class="text-12-regular text-text-weak">No distros detected yet.</div>}
|
||||
>
|
||||
<For each={current()?.checks.distro?.installed ?? []}>
|
||||
{(item) => (
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-border-weak-base px-3 py-2 text-left transition-colors"
|
||||
classList={{ "bg-surface-raised-base": current()?.config.distro === item.name }}
|
||||
onClick={() => void selectDistro(item.name)}
|
||||
<Switch>
|
||||
<Match when={activeStep() === "wsl"}>
|
||||
<div class="rounded-md bg-surface-base p-4 flex flex-col gap-3">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex flex-col gap-1 min-w-0">
|
||||
<div class="text-14-medium text-text-strong">Step 1: Verify WSL</div>
|
||||
<div class="text-12-regular text-text-weak whitespace-pre-wrap break-words">
|
||||
{current()?.checks.wsl?.error ??
|
||||
current()?.checks.wsl?.status ??
|
||||
current()?.checks.wsl?.version ??
|
||||
"WSL has not been checked yet."}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 shrink-0">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy()}
|
||||
onClick={() => void run(() => localServer()!.runStep("wsl"))}
|
||||
>
|
||||
<div class="text-13-medium text-text-strong">{item.name}</div>
|
||||
Check WSL
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy()}
|
||||
onClick={() => void run(() => localServer()!.installWsl())}
|
||||
>
|
||||
Install WSL
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={current()?.config.onboarding.pendingRestart}>
|
||||
<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 to finish WSL installation.
|
||||
</div>
|
||||
<Button variant="secondary" size="large" onClick={() => void platform.restart()}>
|
||||
Restart OpenCode
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
<Match when={activeStep() === "distro"}>
|
||||
<div class="rounded-md bg-surface-base p-4 flex flex-col gap-3">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex flex-col gap-1 min-w-0">
|
||||
<div class="text-14-medium text-text-strong">Step 2: Choose a distro</div>
|
||||
<div class="text-12-regular text-text-weak whitespace-pre-wrap break-words">
|
||||
{current()?.checks.distro?.error ??
|
||||
current()?.config.distro ??
|
||||
"Pick a distro or install one below."}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy()}
|
||||
onClick={() => void run(() => localServer()!.runStep("distro"))}
|
||||
>
|
||||
Check distros
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy()}
|
||||
onClick={() => void run(() => localServer()!.installDistro("Debian"))}
|
||||
>
|
||||
Install Debian
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy()}
|
||||
onClick={() => void run(() => localServer()!.installDistro("Ubuntu-24.04"))}
|
||||
>
|
||||
Install Ubuntu 24
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Show when={otherDistros().length > 0}>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-12-medium text-text-strong">Other distros</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<For each={otherDistros()}>
|
||||
{(item) => (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy()}
|
||||
onClick={() => void run(() => localServer()!.installDistro(item.name))}
|
||||
>
|
||||
{item.label}
|
||||
</Button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="text-12-medium text-text-strong">Installed distros</div>
|
||||
<Show
|
||||
when={(current()?.checks.distro?.installed.length ?? 0) > 0}
|
||||
fallback={<div class="text-12-regular text-text-weak">No distros detected yet.</div>}
|
||||
>
|
||||
<For each={current()?.checks.distro?.installed ?? []}>
|
||||
{(item) => (
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md border border-border-weak-base px-3 py-2 text-left transition-colors"
|
||||
classList={{ "bg-surface-raised-base": current()?.config.distro === item.name }}
|
||||
onClick={() => void selectDistro(item.name)}
|
||||
>
|
||||
<div class="text-13-medium text-text-strong">{item.name}</div>
|
||||
<div class="text-12-regular text-text-weak">
|
||||
{[
|
||||
item.isDefault ? "default" : null,
|
||||
item.state,
|
||||
item.version ? `WSL ${item.version}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ")}
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={selectedProbe()}>
|
||||
{(probe) => (
|
||||
<div class="rounded-md border border-border-weak-base px-3 py-3 flex flex-col gap-1">
|
||||
<div class="text-12-medium text-text-strong">Selected distro checks</div>
|
||||
<div class="text-12-regular text-text-weak">
|
||||
{[item.isDefault ? "default" : null, item.state, item.version ? `WSL ${item.version}` : null]
|
||||
.filter(Boolean)
|
||||
.join(" · ")}
|
||||
User: {probe().username ?? "unknown"}
|
||||
{probe().isRoot ? " · root" : ""}
|
||||
{selectedInstalled()?.version === 1 ? " · WSL 1" : ""}
|
||||
</div>
|
||||
</button>
|
||||
<div class="text-12-regular text-text-weak">
|
||||
bash: {probe().hasBash ? "yes" : "no"} · curl: {probe().hasCurl ? "yes" : "no"} · exec:{" "}
|
||||
{probe().canExecute ? "yes" : "no"}
|
||||
</div>
|
||||
<Show when={selectedInstalled()?.version === 1}>
|
||||
<div class="text-12-regular text-text-warning-base">
|
||||
WSL 2 is required. Convert this distro before continuing.
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={selected()}>
|
||||
{(probe) => (
|
||||
<div class="rounded-md border border-border-weak-base px-3 py-3 flex flex-col gap-1">
|
||||
<div class="text-12-medium text-text-strong">Selected distro checks</div>
|
||||
<div class="text-12-regular text-text-weak">
|
||||
User: {probe().username ?? "unknown"}
|
||||
{probe().isRoot ? " · root" : ""}
|
||||
</div>
|
||||
<div class="text-12-regular text-text-weak">
|
||||
bash: {probe().hasBash ? "yes" : "no"} · curl: {probe().hasCurl ? "yes" : "no"} · exec:{" "}
|
||||
{probe().canExecute ? "yes" : "no"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy() || !current()?.config.distro}
|
||||
onClick={() => void run(() => localServer()!.openTerminal())}
|
||||
>
|
||||
Open terminal
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={current()?.config.distro}>
|
||||
<div class="rounded-md bg-surface-base p-4 flex flex-col gap-3">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex flex-col gap-1 min-w-0">
|
||||
<div class="text-14-medium text-text-strong">OpenCode</div>
|
||||
<div class="text-12-regular text-text-weak whitespace-pre-wrap break-words">
|
||||
{current()?.checks.opencode?.error ?? current()?.checks.opencode?.resolvedPath ?? "Not checked yet"}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy()}
|
||||
onClick={() => void run(() => localServer()!.runStep("opencode"))}
|
||||
disabled={busy() || !current()?.config.distro}
|
||||
onClick={() => void run(() => localServer()!.openTerminal())}
|
||||
>
|
||||
Check OpenCode
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy()}
|
||||
onClick={() => void run(() => localServer()!.installOpencode())}
|
||||
>
|
||||
Install OpenCode
|
||||
Open terminal
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
<Show when={current()?.checks.opencode}>
|
||||
{(check) => (
|
||||
<div class="rounded-md border border-border-weak-base px-3 py-3 flex flex-col gap-1">
|
||||
<div class="text-12-regular text-text-weak">Path: {check().resolvedPath ?? "not found"}</div>
|
||||
<div class="text-12-regular text-text-weak">
|
||||
Version: {check().version ?? "unknown"}
|
||||
<Show when={check().expectedVersion}>
|
||||
{(expected) => <span>{` · desktop ${expected()}`}</span>}
|
||||
<Match when={activeStep() === "opencode"}>
|
||||
<div class="rounded-md bg-surface-base p-4 flex flex-col gap-3">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex flex-col gap-1 min-w-0">
|
||||
<div class="text-14-medium text-text-strong">Step 3: Install OpenCode</div>
|
||||
<div class="text-12-regular text-text-weak whitespace-pre-wrap break-words">
|
||||
{current()?.checks.opencode?.error ??
|
||||
current()?.checks.opencode?.resolvedPath ??
|
||||
"OpenCode has not been checked in this distro yet."}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 shrink-0">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy()}
|
||||
onClick={() => void run(() => localServer()!.runStep("opencode"))}
|
||||
>
|
||||
Check OpenCode
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={busy()}
|
||||
onClick={() => void run(() => localServer()!.installOpencode())}
|
||||
>
|
||||
Install OpenCode
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={current()?.checks.opencode}>
|
||||
{(check) => (
|
||||
<div class="rounded-md border border-border-weak-base px-3 py-3 flex flex-col gap-1">
|
||||
<div class="text-12-regular text-text-weak">Path: {check().resolvedPath ?? "not found"}</div>
|
||||
<div class="text-12-regular text-text-weak">
|
||||
Version: {check().version ?? "unknown"}
|
||||
<Show when={check().expectedVersion}>
|
||||
{(expected) => <span>{` · desktop ${expected()}`}</span>}
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={check().matchesDesktop === false}>
|
||||
<div class="text-12-regular text-text-warning-base">
|
||||
Installed version does not match the desktop app version.
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={check().matchesDesktop === false}>
|
||||
<div class="text-12-regular text-text-warning-base">
|
||||
Installed version does not match the desktop app version.
|
||||
</div>
|
||||
</Show>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
<Match when={activeStep() === "switch"}>
|
||||
<div class="rounded-md bg-surface-base p-4 flex flex-col gap-3">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="flex flex-col gap-1 min-w-0">
|
||||
<div class="text-14-medium text-text-strong">Step 4: Switch Local Server</div>
|
||||
<div class="text-12-regular text-text-weak whitespace-pre-wrap break-words">
|
||||
{needsRestart()
|
||||
? "Restart OpenCode to apply your WSL local runtime configuration."
|
||||
: "WSL local runtime is configured and active."}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
disabled={!switchReady() || !needsRestart()}
|
||||
onClick={() => void platform.restart()}
|
||||
>
|
||||
Restart OpenCode
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border border-border-weak-base px-3 py-3 flex flex-col gap-1">
|
||||
<div class="text-12-regular text-text-weak">
|
||||
Configured runtime:{" "}
|
||||
{configuredRuntime().mode === "wsl" ? `wsl:${configuredRuntime().distro ?? "unknown"}` : "windows"}
|
||||
</div>
|
||||
<div class="text-12-regular text-text-weak">
|
||||
Current runtime:{" "}
|
||||
{current()?.runtime.mode === "wsl" ? `wsl:${current()?.runtime.distro ?? "unknown"}` : "windows"}
|
||||
</div>
|
||||
<Show when={!switchReady()}>
|
||||
<div class="text-12-regular text-text-warning-base">
|
||||
Complete the earlier setup steps before switching.
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Show>
|
||||
|
||||
<Show when={(current()?.transcript.length ?? 0) > 0}>
|
||||
@@ -375,3 +524,72 @@ function requestError(language: ReturnType<typeof useLanguage>, err: unknown) {
|
||||
description: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
|
||||
function stepIndex(step: LocalServerStep) {
|
||||
return STEP_ORDER.indexOf(step)
|
||||
}
|
||||
|
||||
function stepNumber(step: LocalServerStep) {
|
||||
return `${stepIndex(step) + 1}`
|
||||
}
|
||||
|
||||
function stepTitle(step: LocalServerStep) {
|
||||
if (step === "wsl") return "WSL"
|
||||
if (step === "distro") return "Distro"
|
||||
if (step === "opencode") return "OpenCode"
|
||||
return "Switch"
|
||||
}
|
||||
|
||||
function stepSubtitle(
|
||||
step: LocalServerStep,
|
||||
state: {
|
||||
current?: LocalServerState
|
||||
selectedInstalled?: LocalServerState["checks"]["distro"] extends infer T ? any : never
|
||||
selectedProbe?: LocalServerState["checks"]["distro"] extends infer T ? any : never
|
||||
wslReady: boolean
|
||||
distroReady: boolean
|
||||
opencodeReady: boolean
|
||||
switchReady: boolean
|
||||
needsRestart: boolean
|
||||
},
|
||||
) {
|
||||
if (step === "wsl") {
|
||||
if (state.wslReady) return "Ready"
|
||||
return state.current?.checks.wsl?.error ?? "Install or verify WSL"
|
||||
}
|
||||
if (step === "distro") {
|
||||
if (state.distroReady) return state.current?.config.distro ?? "Ready"
|
||||
if (state.selectedInstalled?.version === 1) return "Convert to WSL 2"
|
||||
return state.current?.checks.distro?.error ?? state.current?.config.distro ?? "Choose a distro"
|
||||
}
|
||||
if (step === "opencode") {
|
||||
if (state.opencodeReady) return state.current?.checks.opencode?.version ?? "Ready"
|
||||
return state.current?.checks.opencode?.error ?? "Install OpenCode"
|
||||
}
|
||||
if (!state.switchReady) return "Complete prior steps"
|
||||
return state.needsRestart ? "Restart to apply" : "Active"
|
||||
}
|
||||
|
||||
function stepState(
|
||||
step: LocalServerStep,
|
||||
state: {
|
||||
active: LocalServerStep
|
||||
current?: LocalServerState
|
||||
wslReady: boolean
|
||||
distroReady: boolean
|
||||
opencodeReady: boolean
|
||||
switchReady: boolean
|
||||
needsRestart: boolean
|
||||
},
|
||||
) {
|
||||
if (state.current?.job?.step === step) return "current"
|
||||
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"
|
||||
if (step === "opencode")
|
||||
return state.opencodeReady ? "done" : stepIndex(step) > stepIndex(state.active) ? "locked" : "warning"
|
||||
if (state.switchReady && !state.needsRestart) return "done"
|
||||
if (stepIndex(step) > stepIndex(state.active)) return "locked"
|
||||
return "warning"
|
||||
}
|
||||
|
||||
@@ -479,6 +479,11 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
||||
setStore("localServer", "showPage", true)
|
||||
}
|
||||
|
||||
const localSwapLabel = (conn: ServerConnection.Any) => {
|
||||
if (conn.type !== "sidecar") return ""
|
||||
return conn.variant === "wsl" ? "Swap to Windows" : "Swap to WSL"
|
||||
}
|
||||
|
||||
const submitForm = () => {
|
||||
if (mode() === "add") {
|
||||
if (addMutation.isPending) return
|
||||
@@ -558,114 +563,111 @@ export function DialogSelectServer(props: DialogSelectServerProps = {}) {
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<div class="flex flex-col gap-3">
|
||||
<Show when={platform.localServer}>
|
||||
<div class="px-5">
|
||||
<button
|
||||
type="button"
|
||||
class="w-full rounded-md bg-surface-base px-4 py-3 text-left transition-colors hover:bg-surface-base-hover"
|
||||
onClick={startLocal}
|
||||
>
|
||||
<div class="text-14-medium text-text-strong">Local Server</div>
|
||||
<div class="text-12-regular text-text-weak">Configure Windows or WSL local runtime</div>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<List
|
||||
search={{
|
||||
placeholder: language.t("dialog.server.search.placeholder"),
|
||||
autofocus: false,
|
||||
}}
|
||||
noInitialSelection
|
||||
emptyMessage={language.t("dialog.server.empty")}
|
||||
items={sortedItems}
|
||||
key={(x) => x.http.url}
|
||||
onSelect={(x) => {
|
||||
if (x) void select(x)
|
||||
}}
|
||||
divider={true}
|
||||
class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
|
||||
>
|
||||
{(i) => {
|
||||
const key = ServerConnection.key(i)
|
||||
return (
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1 w-full group/item">
|
||||
<div class="flex flex-col h-full items-start w-5">
|
||||
<ServerHealthIndicator health={store.status[key]} />
|
||||
</div>
|
||||
<ServerRow
|
||||
conn={i}
|
||||
dimmed={store.status[key]?.healthy === false}
|
||||
status={store.status[key]}
|
||||
class="flex items-center gap-3 min-w-0 flex-1"
|
||||
badge={
|
||||
<Show when={defaultKey() === ServerConnection.key(i)}>
|
||||
<span class="text-text-base bg-surface-base text-14-regular px-1.5 rounded-xs">
|
||||
{language.t("dialog.server.status.default")}
|
||||
</span>
|
||||
</Show>
|
||||
}
|
||||
showCredentials
|
||||
/>
|
||||
<div class="flex items-center justify-center gap-4 pl-4">
|
||||
<Show when={ServerConnection.key(current()) === key}>
|
||||
<Icon name="check" class="h-6" />
|
||||
<List
|
||||
search={{
|
||||
placeholder: language.t("dialog.server.search.placeholder"),
|
||||
autofocus: false,
|
||||
}}
|
||||
noInitialSelection
|
||||
emptyMessage={language.t("dialog.server.empty")}
|
||||
items={sortedItems}
|
||||
key={(x) => x.http.url}
|
||||
onSelect={(x) => {
|
||||
if (x) void select(x)
|
||||
}}
|
||||
divider={true}
|
||||
class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:min-h-14 [&_[data-slot=list-item]]:p-3 [&_[data-slot=list-item]]:!bg-transparent"
|
||||
>
|
||||
{(i) => {
|
||||
const key = ServerConnection.key(i)
|
||||
return (
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1 w-full group/item">
|
||||
<div class="flex flex-col h-full items-start w-5">
|
||||
<ServerHealthIndicator health={store.status[key]} />
|
||||
</div>
|
||||
<ServerRow
|
||||
conn={i}
|
||||
dimmed={store.status[key]?.healthy === false}
|
||||
status={store.status[key]}
|
||||
class="flex items-center gap-3 min-w-0 flex-1"
|
||||
badge={
|
||||
<Show when={defaultKey() === ServerConnection.key(i)}>
|
||||
<span class="text-text-base bg-surface-base text-14-regular px-1.5 rounded-xs">
|
||||
{language.t("dialog.server.status.default")}
|
||||
</span>
|
||||
</Show>
|
||||
}
|
||||
showCredentials
|
||||
/>
|
||||
<div class="flex items-center justify-center gap-3 pl-4">
|
||||
<Show when={platform.localServer && i.type === "sidecar"}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
class="shrink-0"
|
||||
onClick={(event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
startLocal()
|
||||
}}
|
||||
>
|
||||
{localSwapLabel(i)}
|
||||
</Button>
|
||||
</Show>
|
||||
|
||||
<Show when={i.type === "http"}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="dot-grid"
|
||||
variant="ghost"
|
||||
class="shrink-0 size-8 hover:bg-surface-base-hover data-[expanded]:bg-surface-base-active"
|
||||
onClick={(e: MouseEvent) => e.stopPropagation()}
|
||||
onPointerDown={(e: PointerEvent) => e.stopPropagation()}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content class="mt-1">
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
if (i.type !== "http") return
|
||||
startEdit(i)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<Show when={canDefault() && defaultKey() !== key}>
|
||||
<DropdownMenu.Item onSelect={() => setDefault(key)}>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("dialog.server.menu.default")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<Show when={canDefault() && defaultKey() === key}>
|
||||
<DropdownMenu.Item onSelect={() => setDefault(null)}>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("dialog.server.menu.defaultRemove")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => handleRemove(ServerConnection.key(i))}
|
||||
class="text-text-on-critical-base hover:bg-surface-critical-weak"
|
||||
>
|
||||
<Show when={ServerConnection.key(current()) === key}>
|
||||
<Icon name="check" class="h-6" />
|
||||
</Show>
|
||||
|
||||
<Show when={i.type === "http"}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenu.Trigger
|
||||
as={IconButton}
|
||||
icon="dot-grid"
|
||||
variant="ghost"
|
||||
class="shrink-0 size-8 hover:bg-surface-base-hover data-[expanded]:bg-surface-base-active"
|
||||
onClick={(e: MouseEvent) => e.stopPropagation()}
|
||||
onPointerDown={(e: PointerEvent) => e.stopPropagation()}
|
||||
/>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content class="mt-1">
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => {
|
||||
if (i.type !== "http") return
|
||||
startEdit(i)
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.edit")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
<Show when={canDefault() && defaultKey() !== key}>
|
||||
<DropdownMenu.Item onSelect={() => setDefault(key)}>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("dialog.server.menu.delete")}
|
||||
{language.t("dialog.server.menu.default")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={canDefault() && defaultKey() === key}>
|
||||
<DropdownMenu.Item onSelect={() => setDefault(null)}>
|
||||
<DropdownMenu.ItemLabel>
|
||||
{language.t("dialog.server.menu.defaultRemove")}
|
||||
</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</Show>
|
||||
<DropdownMenu.Separator />
|
||||
<DropdownMenu.Item
|
||||
onSelect={() => handleRemove(ServerConnection.key(i))}
|
||||
class="text-text-on-critical-base hover:bg-surface-critical-weak"
|
||||
>
|
||||
<DropdownMenu.ItemLabel>{language.t("dialog.server.menu.delete")}</DropdownMenu.ItemLabel>
|
||||
</DropdownMenu.Item>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</List>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</List>
|
||||
</Show>
|
||||
|
||||
<div class="px-5 pb-5">
|
||||
|
||||
Reference in New Issue
Block a user