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 { Icon } from "@opencode-ai/ui/icon" 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 { ServerHealthIndicator, ServerRow } from "@/components/server/server-row" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server" import { type ServerHealth, useCheckServerHealth } from "@/utils/server-health" const DEFAULT_USERNAME = "opencode" interface ServerFormProps { value: string name: string username: string password: string placeholder: string busy: boolean error: string status: boolean | undefined onChange: (value: string) => void onNameChange: (value: string) => void onUsernameChange: (value: string) => void onPasswordChange: (value: string) => void onSubmit: () => void onBack: () => 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() { const language = useLanguage() const platform = usePlatform() const [defaultKey, defaultUrlActions] = createResource( async () => { try { const key = await platform.getDefaultServer?.() if (!key) return null return key } catch (err) { showRequestError(language, err) return null } }, { initialValue: null }, ) const canDefault = createMemo(() => !!platform.getDefaultServer && !!platform.setDefaultServer) const setDefault = async (key: ServerConnection.Key | null) => { try { await platform.setDefaultServer?.(key) defaultUrlActions.mutate(key) } catch (err) { showRequestError(language, err) } } return { defaultKey, canDefault, setDefault } } function useServerPreview() { const checkServerHealth = useCheckServerHealth() 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, username: string, password: string, setStatus: (value: boolean | undefined) => void, ) => { setStatus(undefined) if (!looksComplete(value)) return const normalized = normalizeServerUrl(value) if (!normalized) return const http: ServerConnection.HttpBase = { url: normalized } if (username) http.username = username if (password) http.password = password const result = await checkServerHealth(http) setStatus(result.healthy) } return { previewStatus } } function ServerForm(props: ServerFormProps) { const language = useLanguage() const keyDown = (event: KeyboardEvent) => { event.stopPropagation() if (event.key === "Escape") { event.preventDefault() props.onBack() return } if (event.key !== "Enter" || event.isComposing) return event.preventDefault() props.onSubmit() } return (
) } export function DialogSelectServer() { const navigate = useNavigate() const dialog = useDialog() const server = useServer() const platform = usePlatform() const language = useLanguage() const { defaultKey, canDefault, setDefault } = useDefaultServer() const { previewStatus } = useServerPreview() const checkServerHealth = useCheckServerHealth() const [store, setStore] = createStore({ status: {} as Record, addServer: { url: "", name: "", username: DEFAULT_USERNAME, password: "", adding: false, error: "", showForm: false, status: undefined as boolean | undefined, }, editServer: { id: undefined as string | undefined, value: "", name: "", username: "", password: "", error: "", busy: false, status: undefined as boolean | undefined, }, }) const resetAdd = () => { setStore("addServer", { url: "", name: "", username: DEFAULT_USERNAME, password: "", adding: false, error: "", showForm: false, status: undefined, }) } const resetEdit = () => { setStore("editServer", { id: undefined, value: "", name: "", username: "", password: "", error: "", status: undefined, busy: false, }) } const replaceServer = (original: ServerConnection.Http, next: ServerConnection.Http) => { 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) }), ) 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 && conn.type === "http") { server.add(conn) navigate("/") return } navigate("/") queueMicrotask(() => server.setActive(ServerConnection.key(conn))) } const handleAddChange = (value: string) => { if (store.addServer.adding) return setStore("addServer", { url: value, error: "" }) void previewStatus(value, store.addServer.username, store.addServer.password, (next) => setStore("addServer", { status: next }), ) } const handleAddNameChange = (value: string) => { if (store.addServer.adding) return setStore("addServer", { name: value, error: "" }) } const handleAddUsernameChange = (value: string) => { if (store.addServer.adding) return setStore("addServer", { username: value, error: "" }) void previewStatus(store.addServer.url, value, store.addServer.password, (next) => setStore("addServer", { status: next }), ) } const handleAddPasswordChange = (value: string) => { if (store.addServer.adding) return setStore("addServer", { password: value, error: "" }) void previewStatus(store.addServer.url, store.addServer.username, value, (next) => setStore("addServer", { status: next }), ) } const handleEditChange = (value: string) => { if (store.editServer.busy) return setStore("editServer", { value, error: "" }) void previewStatus(value, store.editServer.username, store.editServer.password, (next) => setStore("editServer", { status: next }), ) } const handleEditNameChange = (value: string) => { if (store.editServer.busy) return setStore("editServer", { name: value, error: "" }) } const handleEditUsernameChange = (value: string) => { if (store.editServer.busy) return setStore("editServer", { username: value, error: "" }) void previewStatus(store.editServer.value, value, store.editServer.password, (next) => setStore("editServer", { status: next }), ) } const handleEditPasswordChange = (value: string) => { if (store.editServer.busy) return setStore("editServer", { password: value, error: "" }) void previewStatus(store.editServer.value, store.editServer.username, 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 conn: ServerConnection.Http = { type: "http", http: { url: normalized }, } if (store.addServer.name.trim()) conn.displayName = store.addServer.name.trim() if (store.addServer.password) conn.http.password = store.addServer.password if (store.addServer.password && store.addServer.username) conn.http.username = store.addServer.username const result = await checkServerHealth(conn.http) setStore("addServer", { adding: false }) if (!result.healthy) { setStore("addServer", { error: language.t("dialog.server.add.error") }) return } resetAdd() await select(conn, 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 } const name = store.editServer.name.trim() || undefined const username = store.editServer.username || undefined const password = store.editServer.password || undefined const existingName = original.displayName if ( normalized === original.http.url && name === existingName && username === original.http.username && password === original.http.password ) { resetEdit() return } setStore("editServer", { busy: true, error: "" }) const conn: ServerConnection.Http = { type: "http", displayName: name, http: { url: normalized, username, password }, } const result = await checkServerHealth(conn.http) setStore("editServer", { busy: false }) if (!result.healthy) { setStore("editServer", { error: language.t("dialog.server.add.error") }) return } if (normalized === original.http.url) { server.add(conn) } else { replaceServer(original, conn) } resetEdit() } const mode = createMemo<"list" | "add" | "edit">(() => { if (store.editServer.id) return "edit" if (store.addServer.showForm) return "add" return "list" }) const editing = createMemo(() => { if (!store.editServer.id) return return items().find((x) => x.type === "http" && x.http.url === store.editServer.id) }) const resetForm = () => { resetAdd() resetEdit() } const startAdd = () => { resetEdit() setStore("addServer", { showForm: true, url: "", name: "", username: DEFAULT_USERNAME, password: "", error: "", status: undefined, }) } const startEdit = (conn: ServerConnection.Http) => { resetAdd() setStore("editServer", { id: conn.http.url, value: conn.http.url, name: conn.displayName ?? "", username: conn.http.username ?? "", password: conn.http.password ?? "", error: "", status: store.status[ServerConnection.key(conn)]?.healthy, busy: false, }) } const submitForm = () => { if (mode() === "add") { void handleAdd(store.addServer.url) return } const original = editing() if (!original) return void handleEdit(original, store.editServer.value) } const isFormMode = createMemo(() => mode() !== "list") const isAddMode = createMemo(() => mode() === "add") const formBusy = createMemo(() => (isAddMode() ? store.addServer.adding : store.editServer.busy)) const formTitle = createMemo(() => { if (!isFormMode()) return language.t("dialog.server.title") return (
{isAddMode() ? language.t("dialog.server.add.title") : language.t("dialog.server.edit.title")}
) }) createEffect(() => { if (!store.editServer.id) return if (editing()) return resetEdit() }) async function handleRemove(url: ServerConnection.Key) { server.remove(url) if ((await platform.getDefaultServer?.()) === url) { platform.setDefaultServer?.(null) } } return (
} > x.http.url} onSelect={(x) => { if (x) 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.delete")}
) }}
{language.t("dialog.server.add.button")} } >
) }