import { createEffect, createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js" import { createStore, reconcile } from "solid-js/store" import { useNavigate } from "@solidjs/router" import { useDialog } from "@opencode-ai/ui/context/dialog" import { Popover } from "@opencode-ai/ui/popover" import { Tabs } from "@opencode-ai/ui/tabs" import { Button } from "@opencode-ai/ui/button" import { Switch } from "@opencode-ai/ui/switch" import { Icon } from "@opencode-ai/ui/icon" import { Tooltip } from "@opencode-ai/ui/tooltip" import { useSync } from "@/context/sync" import { useSDK } from "@/context/sdk" import { normalizeServerUrl, serverDisplayName, useServer } from "@/context/server" import { usePlatform } from "@/context/platform" import { useLanguage } from "@/context/language" import { createOpencodeClient } from "@opencode-ai/sdk/v2/client" import { DialogSelectServer } from "./dialog-select-server" import { showToast } from "@opencode-ai/ui/toast" type ServerStatus = { healthy: boolean; version?: string } async function checkHealth(url: string, platform: ReturnType): Promise { const signal = (AbortSignal as unknown as { timeout?: (ms: number) => AbortSignal }).timeout?.(3000) const sdk = createOpencodeClient({ baseUrl: url, fetch: platform.fetch, signal, }) return sdk.global .health() .then((x) => ({ healthy: x.data?.healthy === true, version: x.data?.version })) .catch(() => ({ healthy: false })) } 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 [store, setStore] = createStore({ status: {} as Record, loading: null as string | null, defaultServerUrl: undefined as string | undefined, }) const servers = 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 sortedServers = createMemo(() => { const list = servers() if (!list.length) return list const active = server.url 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( servers().map(async (url) => { results[url] = await checkHealth(url, platform) }), ) setStore("status", reconcile(results)) } createEffect(() => { servers() refreshHealth() const interval = setInterval(refreshHealth, 10_000) onCleanup(() => clearInterval(interval)) }) const mcpItems = createMemo(() => Object.entries(sync.data.mcp ?? {}) .map(([name, status]) => ({ name, status: status.status })) .sort((a, b) => a.name.localeCompare(b.name)), ) const mcpConnected = createMemo(() => mcpItems().filter((i) => i.status === "connected").length) const toggleMcp = async (name: string) => { if (store.loading) return setStore("loading", name) try { const status = sync.data.mcp[name] await (status?.status === "connected" ? sdk.client.mcp.disconnect({ name }) : sdk.client.mcp.connect({ name })) const result = await sdk.client.mcp.status() if (result.data) sync.set("mcp", result.data) } catch (err) { showToast({ variant: "error", title: language.t("common.requestFailed"), description: err instanceof Error ? err.message : String(err), }) } finally { setStore("loading", null) } } const lspItems = createMemo(() => sync.data.lsp ?? []) const lspCount = createMemo(() => lspItems().length) const plugins = createMemo(() => sync.data.config.plugin ?? []) const pluginCount = createMemo(() => plugins().length) const overallHealthy = createMemo(() => { const serverHealthy = server.healthy() === true const anyMcpIssue = mcpItems().some((m) => m.status !== "connected" && m.status !== "disabled") return serverHealthy && !anyMcpIssue }) const serverCount = createMemo(() => sortedServers().length) const refreshDefaultServerUrl = () => { const result = platform.getDefaultServerUrl?.() if (!result) { setStore("defaultServerUrl", undefined) return } if (result instanceof Promise) { result.then((url) => setStore("defaultServerUrl", url ? normalizeServerUrl(url) : undefined)) return } setStore("defaultServerUrl", normalizeServerUrl(result)) } createEffect(() => { refreshDefaultServerUrl() }) 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={6} placement="bottom-end" shift={-136} >
{serverCount() > 0 ? `${serverCount()} ` : ""} {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")}
{(url) => { const isActive = () => url === server.url const isDefault = () => url === store.defaultServerUrl const status = () => store.status[url] const isBlocked = () => status()?.healthy === false const [truncated, setTruncated] = createSignal(false) let nameRef: HTMLSpanElement | undefined let versionRef: HTMLSpanElement | undefined onMount(() => { const check = () => { const nameTruncated = nameRef ? nameRef.scrollWidth > nameRef.clientWidth : false const versionTruncated = versionRef ? versionRef.scrollWidth > versionRef.clientWidth : false setTruncated(nameTruncated || versionTruncated) } check() window.addEventListener("resize", check) onCleanup(() => window.removeEventListener("resize", check)) }) const tooltipValue = () => { const name = serverDisplayName(url) const version = status()?.version return ( {name} {version} ) } return ( ) }}
0} fallback={
{language.t("dialog.mcp.empty")}
} > {(item) => { const enabled = () => item.status === "connected" return ( ) }}
0} fallback={
{language.t("dialog.lsp.empty")}
} > {(item) => (
{item.name || item.id}
)}
0} fallback={
{(() => { const value = language.t("dialog.plugins.empty") const file = "opencode.json" const parts = value.split(file) if (parts.length === 1) return value return ( <> {parts[0]} {file} {parts.slice(1).join(file)} ) })()}
} > {(plugin) => (
{plugin}
)}
) }