import { createResource, createEffect, createMemo, onCleanup, Show, createSignal } from "solid-js" import { createStore, reconcile } from "solid-js/store" import { useDialog } from "@opencode-ai/ui/context/dialog" import { Dialog } from "@opencode-ai/ui/dialog" import { List } from "@opencode-ai/ui/list" import { Button } from "@opencode-ai/ui/button" import { IconButton } from "@opencode-ai/ui/icon-button" import { TextField } from "@opencode-ai/ui/text-field" import { normalizeServerUrl, serverDisplayName, useServer } from "@/context/server" import { usePlatform } from "@/context/platform" import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" import { useNavigate } from "@solidjs/router" import { useLanguage } from "@/context/language" import { Popover } from "@opencode-ai/ui/popover" import { Tooltip } from "@opencode-ai/ui/tooltip" import { useGlobalSDK } from "@/context/global-sdk" type ServerStatus = { healthy: boolean; version?: string } interface AddRowProps { value: string placeholder: string adding: boolean error: string status: boolean | undefined onChange: (value: string) => void onKeyDown: (event: KeyboardEvent) => void onBlur: () => void } interface EditRowProps { value: string placeholder: string busy: boolean error: string status: boolean | undefined onChange: (value: string) => void onKeyDown: (event: KeyboardEvent) => void onBlur: () => void } async function checkHealth(url: string, platform: ReturnType): Promise { const sdk = createOpencodeClient({ baseUrl: url, fetch: platform.fetch, signal: AbortSignal.timeout(3000), }) return sdk.global .health() .then((x) => ({ healthy: x.data?.healthy === true, version: x.data?.version })) .catch(() => ({ healthy: false })) } function AddRow(props: AddRowProps) { return (
) } function EditRow(props: EditRowProps) { return (
event.stopPropagation()}>
) } export function DialogSelectServer() { const navigate = useNavigate() const dialog = useDialog() const server = useServer() const platform = usePlatform() const globalSDK = useGlobalSDK() const language = useLanguage() const [store, setStore] = createStore({ status: {} as Record, addServer: { url: "", adding: false, error: "", showForm: false, status: undefined as boolean | undefined, }, editServer: { id: undefined as string | undefined, value: "", error: "", busy: false, status: undefined as boolean | undefined, }, }) const [defaultUrl, defaultUrlActions] = createResource(() => platform.getDefaultServerUrl?.()) const isDesktop = platform.platform === "desktop" const looksComplete = (value: string) => { const normalized = normalizeServerUrl(value) if (!normalized) return false const host = normalized.replace(/^https?:\/\//, "").split("/")[0] if (!host) return false if (host.includes("localhost") || host.startsWith("127.0.0.1")) return true return host.includes(".") || host.includes(":") } const previewStatus = async (value: string, setStatus: (value: boolean | undefined) => void) => { setStatus(undefined) if (!looksComplete(value)) return const normalized = normalizeServerUrl(value) if (!normalized) return const result = await checkHealth(normalized, platform) setStatus(result.healthy) } const resetAdd = () => { setStore("addServer", { url: "", error: "", showForm: false, status: undefined, }) } const resetEdit = () => { setStore("editServer", { id: undefined, value: "", error: "", status: undefined, busy: false, }) } const replaceServer = (original: string, next: string) => { const active = server.url const nextActive = active === original ? next : active server.add(next) if (nextActive) server.setActive(nextActive) server.remove(original) } const items = createMemo(() => { const current = server.url const list = server.list if (!current) return list if (!list.includes(current)) return [current, ...list] return [current, ...list.filter((x) => x !== current)] }) const current = createMemo(() => items().find((x) => x === server.url) ?? items()[0]) const sortedItems = createMemo(() => { const list = items() if (!list.length) return list const active = current() const order = new Map(list.map((url, index) => [url, index] as const)) const rank = (value?: ServerStatus) => { if (value?.healthy === true) return 0 if (value?.healthy === false) return 2 return 1 } return list.slice().sort((a, b) => { if (a === active) return -1 if (b === active) return 1 const diff = rank(store.status[a]) - rank(store.status[b]) if (diff !== 0) return diff return (order.get(a) ?? 0) - (order.get(b) ?? 0) }) }) async function refreshHealth() { const results: Record = {} await Promise.all( items().map(async (url) => { results[url] = await checkHealth(url, platform) }), ) setStore("status", reconcile(results)) } createEffect(() => { items() refreshHealth() const interval = setInterval(refreshHealth, 10_000) onCleanup(() => clearInterval(interval)) }) async function select(value: string, persist?: boolean) { if (!persist && store.status[value]?.healthy === false) return dialog.close() if (persist) { server.add(value) navigate("/") return } server.setActive(value) navigate("/") } const handleAddChange = (value: string) => { if (store.addServer.adding) return setStore("addServer", { url: value, error: "" }) void previewStatus(value, (next) => setStore("addServer", { status: next })) } const scrollListToBottom = () => { const scroll = document.querySelector('[data-component="list"] [data-slot="list-scroll"]') if (!scroll) return requestAnimationFrame(() => { scroll.scrollTop = scroll.scrollHeight }) } const handleEditChange = (value: string) => { if (store.editServer.busy) return setStore("editServer", { value, error: "" }) void previewStatus(value, (next) => setStore("editServer", { status: next })) } async function handleAdd(value: string) { if (store.addServer.adding) return const normalized = normalizeServerUrl(value) if (!normalized) { resetAdd() return } setStore("addServer", { adding: true, error: "" }) const result = await checkHealth(normalized, platform) setStore("addServer", { adding: false }) if (!result.healthy) { setStore("addServer", { error: language.t("dialog.server.add.error") }) return } resetAdd() await select(normalized, true) } async function handleEdit(original: string, value: string) { if (store.editServer.busy) return const normalized = normalizeServerUrl(value) if (!normalized) { resetEdit() return } if (normalized === original) { resetEdit() return } setStore("editServer", { busy: true, error: "" }) const result = await checkHealth(normalized, platform) setStore("editServer", { busy: false }) if (!result.healthy) { setStore("editServer", { error: language.t("dialog.server.add.error") }) return } replaceServer(original, normalized) resetEdit() } const handleAddKey = (event: KeyboardEvent) => { event.stopPropagation() if (event.key !== "Enter" || event.isComposing) return event.preventDefault() handleAdd(store.addServer.url) } const blurAdd = () => { if (!store.addServer.url.trim()) { resetAdd() return } handleAdd(store.addServer.url) } const handleEditKey = (event: KeyboardEvent, original: string) => { event.stopPropagation() if (event.key === "Escape") { event.preventDefault() resetEdit() return } if (event.key !== "Enter" || event.isComposing) return event.preventDefault() handleEdit(original, store.editServer.value) } async function handleRemove(url: string) { server.remove(url) } return (
x} onSelect={(x) => { if (x) select(x) }} divider={true} class="px-5 [&_[data-slot=list-search-wrapper]]:w-full [&_[data-slot=list-scroll]]:max-h-[300px] [&_[data-slot=list-scroll]]:overflow-y-auto [&_[data-slot=list-items]]:bg-surface-raised-base [&_[data-slot=list-items]]:rounded-md [&_[data-slot=list-item]]:h-14 [&_[data-slot=list-item]]:p-3" add={ store.addServer.showForm ? { render: () => ( ), } : undefined } > {(i) => { const [popoverOpen, setPopoverOpen] = createSignal(false) const [truncated, setTruncated] = createSignal(false) let nameRef: HTMLSpanElement | undefined let versionRef: HTMLSpanElement | undefined const check = () => { const nameTruncated = nameRef ? nameRef.scrollWidth > nameRef.clientWidth : false const versionTruncated = versionRef ? versionRef.scrollWidth > versionRef.clientWidth : false setTruncated(nameTruncated || versionTruncated) } createEffect(() => { check() window.addEventListener("resize", check) onCleanup(() => window.removeEventListener("resize", check)) }) const tooltipValue = () => { const name = serverDisplayName(i) const version = store.status[i]?.version return ( {name} {version} ) } return (
handleEditKey(event, i)} onBlur={() => handleEdit(i, store.editServer.value)} /> } >
{serverDisplayName(i)} {store.status[i]?.version} {language.t("dialog.server.status.default")}

{language.t("dialog.server.current")}

e.stopPropagation()} onPointerDown={(e) => e.stopPropagation()}> event.stopPropagation()} /> } class="w-max !min-w-fit !max-w-none" >
) }}
) }