import { Button } from "@opencode-ai/ui/button" import { useDialog } from "@opencode-ai/ui/context/dialog" import { Icon } from "@opencode-ai/ui/icon" import { Popover } from "@opencode-ai/ui/popover" import { Switch } from "@opencode-ai/ui/switch" import { Tabs } from "@opencode-ai/ui/tabs" import { showToast } from "@opencode-ai/ui/toast" import { useNavigate } from "@solidjs/router" import { type Accessor, createEffect, createMemo, createSignal, For, type JSXElement, 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 { useSDK } from "@/context/sdk" import { normalizeServerUrl, ServerConnection, useServer } from "@/context/server" import { useSync } from "@/context/sync" import { checkServerHealth, type ServerHealth } from "@/utils/server-health" import { DialogSelectServer } from "./dialog-select-server" const pollMs = 10_000 const pluginEmptyMessage = (value: string, file: string): JSXElement => { const parts = value.split(file) if (parts.length === 1) return value return ( <> {parts[0]} {file} {parts.slice(1).join(file)} > ) } const listServersByHealth = ( list: ServerConnection.Any[], active: ServerConnection.Key | undefined, status: Record, ) => { if (!list.length) return list 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 (ServerConnection.key(a) === active) return -1 if (ServerConnection.key(b) === active) return 1 const diff = rank(status[ServerConnection.key(a)]) - rank(status[ServerConnection.key(b)]) if (diff !== 0) return diff return (order.get(a) ?? 0) - (order.get(b) ?? 0) }) } const useServerHealth = (servers: Accessor, fetcher: typeof fetch) => { const [status, setStatus] = createStore({} as Record) createEffect(() => { const list = servers() let dead = false const refresh = async () => { const results: Record = {} await Promise.all( list.map(async (conn) => { results[ServerConnection.key(conn)] = await checkServerHealth(conn.http, fetcher) }), ) if (dead) return setStatus(reconcile(results)) } void refresh() const id = setInterval(() => void refresh(), pollMs) onCleanup(() => { dead = true clearInterval(id) }) }) return status } const useDefaultServerKey = ( get: (() => string | Promise | null | undefined) | undefined, ) => { const [url, setUrl] = createSignal() const [tick, setTick] = createSignal(0) createEffect(() => { tick() let dead = false const result = get?.() if (!result) { setUrl(undefined) onCleanup(() => { dead = true }) return } if (result instanceof Promise) { void result.then((next) => { if (dead) return setUrl(next ? normalizeServerUrl(next) : undefined) }) onCleanup(() => { dead = true }) return } setUrl(normalizeServerUrl(result)) onCleanup(() => { dead = true }) }) return { key: () => { const u = url() if (!u) return return ServerConnection.key({ type: "http", http: { url: u } }) }, refresh: () => setTick((value) => value + 1), } } const useMcpToggle = (input: { sync: ReturnType sdk: ReturnType language: ReturnType }) => { const [loading, setLoading] = createSignal(null) const toggle = async (name: string) => { if (loading()) return setLoading(name) try { const status = input.sync.data.mcp[name] await (status?.status === "connected" ? input.sdk.client.mcp.disconnect({ name }) : input.sdk.client.mcp.connect({ name })) const result = await input.sdk.client.mcp.status() if (result.data) input.sync.set("mcp", result.data) } catch (err) { showToast({ variant: "error", title: input.language.t("common.requestFailed"), description: err instanceof Error ? err.message : String(err), }) } finally { setLoading(null) } } return { loading, toggle } } export function StatusPopover() { const sync = useSync() const sdk = useSDK() const server = useServer() const platform = usePlatform() const dialog = useDialog() const language = useLanguage() const navigate = useNavigate() const fetcher = platform.fetch ?? globalThis.fetch const servers = createMemo(() => { const current = server.current const list = server.list if (!current) return list if (list.every((item) => ServerConnection.key(item) !== ServerConnection.key(current))) return [current, ...list] return [current, ...list.filter((item) => ServerConnection.key(item) !== ServerConnection.key(current))] }) const health = useServerHealth(servers, fetcher) const sortedServers = createMemo(() => listServersByHealth(servers(), server.key, health)) const mcp = useMcpToggle({ sync, sdk, language }) const defaultServer = useDefaultServerKey(platform.getDefaultServerUrl) const mcpNames = createMemo(() => Object.keys(sync.data.mcp ?? {}).sort((a, b) => a.localeCompare(b))) const mcpStatus = (name: string) => sync.data.mcp?.[name]?.status const mcpConnected = createMemo(() => mcpNames().filter((name) => mcpStatus(name) === "connected").length) const lspItems = createMemo(() => sync.data.lsp ?? []) const lspCount = createMemo(() => lspItems().length) const plugins = createMemo(() => sync.data.config.plugin ?? []) const pluginCount = createMemo(() => plugins().length) const pluginEmpty = createMemo(() => pluginEmptyMessage(language.t("dialog.plugins.empty"), "opencode.json")) const overallHealthy = createMemo(() => { const serverHealthy = server.healthy() === true const anyMcpIssue = mcpNames().some((name) => { const status = mcpStatus(name) return status !== "connected" && status !== "disabled" }) return serverHealthy && !anyMcpIssue }) return ( {language.t("status.popover.trigger")} } class="[&_[data-slot=popover-body]]:p-0 w-[360px] max-w-[calc(100vw-40px)] bg-transparent border-0 shadow-none rounded-xl" gutter={4} placement="bottom-end" shift={-136} > {sortedServers().length > 0 ? `${sortedServers().length} ` : ""} {language.t("status.popover.tab.servers")} {mcpConnected() > 0 ? `${mcpConnected()} ` : ""} {language.t("status.popover.tab.mcp")} {lspCount() > 0 ? `${lspCount()} ` : ""} {language.t("status.popover.tab.lsp")} {pluginCount() > 0 ? `${pluginCount()} ` : ""} {language.t("status.popover.tab.plugins")} {(s) => { const key = ServerConnection.key(s) const isBlocked = () => health[key]?.healthy === false return ( { if (isBlocked()) return server.setActive(key) navigate("/") }} > {language.t("common.default")} } > ) }} dialog.show(() => , defaultServer.refresh)} > {language.t("status.popover.action.manageServers")} 0} fallback={ {language.t("dialog.mcp.empty")} } > {(name) => { const status = () => mcpStatus(name) const enabled = () => status() === "connected" return ( mcp.toggle(name)} disabled={mcp.loading() === name} > {name} event.stopPropagation()}> mcp.toggle(name)} /> ) }} 0} fallback={ {language.t("dialog.lsp.empty")} } > {(item) => ( {item.name || item.id} )} 0} fallback={{pluginEmpty()}} > {(plugin) => ( {plugin} )} ) }
{file}