feat: turn local server setup into guided step flow

This commit is contained in:
LukeParkerDev
2026-04-16 15:12:27 +10:00
parent b321a2de2b
commit 8554345ba0
2 changed files with 532 additions and 312 deletions

View File

@@ -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"
}

View File

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