From 8d8e8fe8f4d972af47271ad361c6ff6e969ecac0 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:40:43 +1000 Subject: [PATCH] feat: add local server management dialog shell --- .../src/components/dialog-local-server.tsx | 276 ++++++++++++++++++ .../src/components/dialog-select-server.tsx | 275 +++++++++-------- 2 files changed, 437 insertions(+), 114 deletions(-) create mode 100644 packages/app/src/components/dialog-local-server.tsx diff --git a/packages/app/src/components/dialog-local-server.tsx b/packages/app/src/components/dialog-local-server.tsx new file mode 100644 index 0000000000..a810415079 --- /dev/null +++ b/packages/app/src/components/dialog-local-server.tsx @@ -0,0 +1,276 @@ +import { Button } from "@opencode-ai/ui/button" +import { showToast } from "@opencode-ai/ui/toast" +import { createEffect, createMemo, For, onCleanup, Show } from "solid-js" +import { createStore, reconcile } from "solid-js/store" +import { useLanguage } from "@/context/language" +import type { LocalServerState } from "@/context/platform" +import { usePlatform } from "@/context/platform" + +export function DialogLocalServer() { + const language = useLanguage() + const platform = usePlatform() + const [store, setStore] = createStore({ + state: undefined as LocalServerState | undefined, + loading: true, + }) + + createEffect(() => { + const localServer = platform.localServer + if (!localServer) return + let mounted = true + void localServer + .getState() + .then((state) => { + if (!mounted) return + setStore({ state, loading: false }) + }) + .catch((err) => { + if (!mounted) return + requestError(language, err) + setStore("loading", false) + }) + const off = localServer.subscribe((event) => { + setStore("state", reconcile(event.state)) + setStore("loading", false) + }) + onCleanup(() => { + mounted = false + off() + }) + }) + + const current = () => store.state + const localServer = () => platform.localServer + const busy = createMemo(() => !!current()?.job) + const mode = createMemo(() => current()?.config.mode ?? "windows") + const selected = createMemo(() => current()?.checks.distro?.selected) + + const run = async (action: () => Promise) => { + try { + await action() + } catch (err) { + requestError(language, err) + } + } + + const setMode = async (next: "windows" | "wsl") => { + const state = current() + if (!state || !localServer()) return + await run(() => + localServer()!.setConfig({ + ...state.config, + mode: next, + onboarding: { + ...state.config.onboarding, + complete: next === "windows", + pendingRestart: next === "windows" ? false : state.config.onboarding.pendingRestart, + step: next === "windows" ? null : state.config.onboarding.step, + }, + }), + ) + } + + const selectDistro = async (name: string) => { + const state = current() + if (!state || !localServer()) return + await run(() => + localServer()!.setConfig({ + ...state.config, + mode: "wsl", + distro: name, + onboarding: { + ...state.config.onboarding, + complete: false, + step: "distro", + }, + }), + ) + } + + return ( +
+ Loading local server...
} + > +
+
Runtime
+
+ + +
+
+ Current runtime:{" "} + {current()?.runtime.mode === "wsl" + ? `wsl${current()?.runtime.distro ? `:${current()?.runtime.distro}` : ""}` + : "windows"} +
+
+ + +
+
+
+
WSL
+
+ {current()?.checks.wsl?.error ?? + current()?.checks.wsl?.status ?? + current()?.checks.wsl?.version ?? + "Not checked yet"} +
+
+
+ + +
+
+ +
+ Windows restart required to finish WSL installation. +
+
+
+ +
+
+
+
Distro
+
+ {current()?.checks.distro?.error ?? + selected()?.name ?? + current()?.config.distro ?? + "No distro selected"} +
+
+
+ +
+
+ +
+ + +
+ +
+
Installed distros
+ 0} + fallback={
No distros detected yet.
} + > + + {(item) => ( + + )} + +
+
+ + + {(probe) => ( +
+
Selected distro checks
+
+ User: {probe().username ?? "unknown"} + {probe().isRoot ? " · root" : ""} +
+
+ bash: {probe().hasBash ? "yes" : "no"} · curl: {probe().hasCurl ? "yes" : "no"} · exec:{" "} + {probe().canExecute ? "yes" : "no"} +
+
+ )} +
+ +
+ +
+
+
+ + 0}> +
+
Diagnostics
+
+ {(line) =>
{line.text}
}
+
+
+
+ + + ) +} + +function requestError(language: ReturnType, err: unknown) { + showToast({ + variant: "error", + title: language.t("common.requestFailed"), + description: err instanceof Error ? err.message : String(err), + }) +} diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index dd92edec3e..63efa2b275 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -11,6 +11,7 @@ import { showToast } from "@opencode-ai/ui/toast" import { useNavigate } from "@solidjs/router" import { createEffect, createMemo, createResource, onCleanup, Show } from "solid-js" import { createStore, reconcile } from "solid-js/store" +import { DialogLocalServer } from "@/components/dialog-local-server" import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" @@ -191,6 +192,9 @@ export function DialogSelectServer() { showForm: false, status: undefined as boolean | undefined, }, + localServer: { + showPage: false, + }, editServer: { id: undefined as string | undefined, value: "", @@ -419,7 +423,8 @@ export function DialogSelectServer() { ) } - const mode = createMemo<"list" | "add" | "edit">(() => { + const mode = createMemo<"list" | "local" | "add" | "edit">(() => { + if (store.localServer.showPage) return "local" if (store.editServer.id) return "edit" if (store.addServer.showForm) return "add" return "list" @@ -433,9 +438,11 @@ export function DialogSelectServer() { const resetForm = () => { resetAdd() resetEdit() + setStore("localServer", "showPage", false) } const startAdd = () => { + setStore("localServer", "showPage", false) resetEdit() setStore("addServer", { showForm: true, @@ -449,6 +456,7 @@ export function DialogSelectServer() { } const startEdit = (conn: ServerConnection.Http) => { + setStore("localServer", "showPage", false) resetAdd() setStore("editServer", { id: conn.http.url, @@ -461,6 +469,12 @@ export function DialogSelectServer() { }) } + const startLocal = () => { + resetAdd() + resetEdit() + setStore("localServer", "showPage", true) + } + const submitForm = () => { if (mode() === "add") { if (addMutation.isPending) return @@ -477,6 +491,7 @@ export function DialogSelectServer() { const isFormMode = createMemo(() => mode() !== "list") const isAddMode = createMemo(() => mode() === "add") + const isLocalMode = createMemo(() => mode() === "local") const formBusy = createMemo(() => (isAddMode() ? addMutation.isPending : editMutation.isPending)) const formTitle = createMemo(() => { @@ -484,7 +499,13 @@ export function DialogSelectServer() { return (
- {isAddMode() ? language.t("dialog.server.add.title") : language.t("dialog.server.edit.title")} + + {isLocalMode() + ? "Local Server" + : isAddMode() + ? language.t("dialog.server.add.title") + : language.t("dialog.server.edit.title")} +
) }) @@ -508,130 +529,156 @@ export function DialogSelectServer() { + + } + > + + } > - 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 ( -
-
- -
- - - {language.t("dialog.server.status.default")} - - - } - showCredentials - /> -
- - - +
+ +
+ +
+
- - - e.stopPropagation()} - onPointerDown={(e: PointerEvent) => e.stopPropagation()} - /> - - - { - if (i.type !== "http") return - startEdit(i) - }} - > - {language.t("dialog.server.menu.edit")} - - - setDefault(key)}> + 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 ( +
+
+ +
+ + + {language.t("dialog.server.status.default")} + + + } + showCredentials + /> +
+ + + + + + + e.stopPropagation()} + onPointerDown={(e: PointerEvent) => e.stopPropagation()} + /> + + + { + if (i.type !== "http") return + startEdit(i) + }} + > + {language.t("dialog.server.menu.edit")} + + + 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.default")} + {language.t("dialog.server.menu.delete")} - - - 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")} - - - - - + + + + +
-
- ) - }} - + ) + }} + +
- {language.t("dialog.server.add.button")} - + + + } >