import { Button } from "@opencode-ai/ui/button" import { useDialog } from "@opencode-ai/ui/context/dialog" import { Dialog } from "@opencode-ai/ui/dialog" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { IconButton } from "@opencode-ai/ui/icon-button" import { List } from "@opencode-ai/ui/list" import { TextField } from "@opencode-ai/ui/text-field" 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 { ServerRow } from "@/components/server/server-row" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server" import { checkServerHealth, type ServerHealth } from "@/utils/server-health" 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 } function showRequestError(language: ReturnType, err: unknown) { showToast({ variant: "error", title: language.t("common.requestFailed"), description: err instanceof Error ? err.message : String(err), }) } function useDefaultServer(platform: ReturnType, language: ReturnType) { const [defaultUrl, defaultUrlActions] = createResource( async () => { try { const url = await platform.getDefaultServerUrl?.() if (!url) return null return normalizeServerUrl(url) ?? null } catch (err) { showRequestError(language, err) return null } }, { initialValue: null }, ) const canDefault = createMemo(() => !!platform.getDefaultServerUrl && !!platform.setDefaultServerUrl) const setDefault = async (url: string | null) => { try { await platform.setDefaultServerUrl?.(url) defaultUrlActions.mutate(url) } catch (err) { showRequestError(language, err) } } return { defaultUrl, canDefault, setDefault } } function useServerPreview(fetcher: typeof fetch) { 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 checkServerHealth({ url: normalized }, fetcher) setStatus(result.healthy) } return { previewStatus } } function AddRow(props: AddRowProps) { return (
{ // Position relative to input-wrapper requestAnimationFrame(() => { const wrapper = el.parentElement?.querySelector('[data-slot="input-wrapper"]') if (wrapper instanceof HTMLElement) { wrapper.appendChild(el) } }) }} />
) } function EditRow(props: EditRowProps) { return (
event.stopPropagation()}>
) } export function DialogSelectServer() { const navigate = useNavigate() const dialog = useDialog() const server = useServer() const platform = usePlatform() const language = useLanguage() const fetcher = platform.fetch ?? globalThis.fetch const { defaultUrl, canDefault, setDefault } = useDefaultServer(platform, language) const { previewStatus } = useServerPreview(fetcher) let listRoot: HTMLDivElement | undefined 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 resetAdd = () => { setStore("addServer", { url: "", error: "", showForm: false, status: undefined, }) } const resetEdit = () => { setStore("editServer", { id: undefined, value: "", error: "", status: undefined, busy: false, }) } const replaceServer = (original: ServerConnection.Http, next: string) => { const active = server.key const newConn = server.add(next) if (!newConn) return const nextActive = active === ServerConnection.key(original) ? ServerConnection.key(newConn) : active if (nextActive) server.setActive(nextActive) server.remove(ServerConnection.key(original)) } const items = createMemo(() => { const current = server.current 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) => ServerConnection.key(x) === server.key) ?? 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?: ServerHealth) => { 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[ServerConnection.key(a)]) - rank(store.status[ServerConnection.key(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 (conn) => { results[ServerConnection.key(conn)] = await checkServerHealth(conn.http, fetcher) }), ) setStore("status", reconcile(results)) } createEffect(() => { items() refreshHealth() const interval = setInterval(refreshHealth, 10_000) onCleanup(() => clearInterval(interval)) }) async function select(conn: ServerConnection.Any, persist?: boolean) { if (!persist && store.status[ServerConnection.key(conn)]?.healthy === false) return dialog.close() if (persist) { server.add(conn.http.url) navigate("/") return } server.setActive(ServerConnection.key(conn)) 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 = listRoot?.querySelector('[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 checkServerHealth({ url: normalized }, fetcher) setStore("addServer", { adding: false }) if (!result.healthy) { setStore("addServer", { error: language.t("dialog.server.add.error") }) return } resetAdd() await select({ type: "http", http: { url: normalized } }, true) } async function handleEdit(original: ServerConnection.Any, value: string) { if (store.editServer.busy || original.type !== "http") return const normalized = normalizeServerUrl(value) if (!normalized) { resetEdit() return } if (normalized === original.http.url) { resetEdit() return } setStore("editServer", { busy: true, error: "" }) const result = await checkServerHealth({ url: normalized }, fetcher) 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: ServerConnection.Any) => { 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: ServerConnection.Key) { server.remove(url) if ((await platform.getDefaultServerUrl?.()) === url) { platform.setDefaultServerUrl?.(null) } } return (
(listRoot = el)}> x.http.url} onSelect={(x) => { if (x) select(x) }} onFilter={(value) => { if (value && store.addServer.showForm && !store.addServer.adding) { resetAdd() } }} 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 [&_[data-slot=list-item]]:!bg-transparent [&_[data-slot=list-item-add]]:px-0" add={ store.addServer.showForm ? { render: () => ( ), } : undefined } > {(i) => { return (
handleEditKey(event, i)} onBlur={() => handleEdit(i, store.editServer.value)} /> } > {language.t("dialog.server.status.default")} } />

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

e.stopPropagation()} onPointerDown={(e: PointerEvent) => e.stopPropagation()} /> { setStore("editServer", { id: i.http.url, value: i.http.url, error: "", status: store.status[ServerConnection.key(i)]?.healthy, }) }} > {language.t("dialog.server.menu.edit")} setDefault(i.http.url)}> {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.delete")}
) }}
) }