import { Button } from "@kirincode-ai/ui/button" import { useDialog } from "@kirincode-ai/ui/context/dialog" import { Icon } from "@kirincode-ai/ui/icon" import { Switch } from "@kirincode-ai/ui/switch" import { Tabs } from "@kirincode-ai/ui/tabs" import { showToast } from "@/utils/toast" import { useNavigate } from "@solidjs/router" import { type Accessor, createEffect, createMemo, For, type JSXElement, onCleanup, Show } from "solid-js" import { createStore } from "solid-js/store" import { ServerHealthIndicator, ServerRow } from "@/components/server/server-row" import { useLanguage } from "@/context/language" import { usePlatform } from "@/context/platform" import { ServerConnection, useServer } from "@/context/server" import { useSync } from "@/context/sync" import { type ServerHealth } from "@/utils/server-health" import { useGlobal } from "@/context/global" import { useSettings } from "@/context/settings" import { useMcpToggle } from "@/context/mcp" 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 useDefaultServerKey = ( get: (() => string | Promise | null | undefined) | undefined, ) => { const [state, setState] = createStore({ key: undefined as ServerConnection.Key | undefined, tick: 0, }) createEffect(() => { state.tick let dead = false const result = get?.() if (!result) { setState("key", undefined) onCleanup(() => { dead = true }) return } if (result instanceof Promise) { void result.then((next) => { if (dead) return setState("key", next ?? undefined) }) onCleanup(() => { dead = true }) return } setState("key", ServerConnection.Key.make(result)) onCleanup(() => { dead = true }) }) return { key: () => { return state.key }, refresh: () => setState("tick", (value) => value + 1), } } type ServerStatusState = { servers: () => ServerStatusItem[] defaultKey: () => ServerConnection.Key | undefined ariaLabel: string serversLabel: string defaultLabel: string manageLabel: string onManage: () => void } type ServerStatusItem = { key: ServerConnection.Key conn: ServerConnection.Any health?: ServerHealth blocked: boolean active: boolean onSelect: () => void } export function StatusPopoverServerBody() { const global = useGlobal() const server = useServer() const platform = usePlatform() const dialog = useDialog() const language = useLanguage() const navigate = useNavigate() let dialogRun = 0 let dialogDead = false onCleanup(() => { dialogDead = true dialogRun += 1 }) const sortedServers = createMemo(() => listServersByHealth(global.servers.list(), server.key, global.servers.health)) const defaultServer = useDefaultServerKey(platform.getDefaultServer) const serverItems = createMemo(() => sortedServers().map((conn) => { const key = ServerConnection.key(conn) return { key, conn, health: global.servers.health[key], blocked: global.servers.health[key]?.healthy === false, active: !!server.current && key === ServerConnection.key(server.current), onSelect: () => { navigate("/") queueMicrotask(() => server.setActive(key)) }, } }), ) return ( { const run = ++dialogRun void import("./dialog-select-server").then((x) => { if (dialogDead || dialogRun !== run) return dialog.show(() => , defaultServer.refresh) }) }, }} /> ) } function ServerStatusPopoverView(props: { state: ServerStatusState }) { return ( {props.state.servers().length > 0 ? `${props.state.servers().length} ` : ""} {props.state.serversLabel} ) } function ServerStatusList(props: { state: ServerStatusState }) { return ( {(item) => { return ( { if (item.blocked) return item.onSelect() }} > {props.state.defaultLabel} } > ) }} {props.state.manageLabel} ) } export function StatusPopoverBody(props: { shown: Accessor }) { const sync = useSync() const global = useGlobal() const server = useServer() const platform = usePlatform() const dialog = useDialog() const language = useLanguage() const navigate = useNavigate() const settings = useSettings() const fail = (err: unknown) => { showToast({ variant: "error", title: language.t("common.requestFailed"), description: err instanceof Error ? err.message : String(err), }) } createEffect(() => { if (!props.shown()) return }) let dialogRun = 0 let dialogDead = false onCleanup(() => { dialogDead = true dialogRun += 1 }) const sortedServers = createMemo(() => listServersByHealth(global.servers.list(), server.key, global.servers.health)) const toggleMcp = useMcpToggle() const defaultServer = useDefaultServerKey(platform.getDefaultServer) 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 ?? []).map((item) => (typeof item === "string" ? item : item[0])), ) const pluginCount = createMemo(() => plugins().length) const pluginEmpty = createMemo(() => pluginEmptyMessage(language.t("dialog.plugins.empty"), "kirincode.json")) return ( {!settings.general.newLayoutDesigns() && ( {global.servers.list().length > 0 ? `${global.servers.list().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")} {!settings.general.newLayoutDesigns() && ( {(s) => { const key = ServerConnection.key(s) const blocked = () => global.servers.health[key]?.healthy === false return ( { if (blocked()) return navigate("/") queueMicrotask(() => server.setActive(key)) }} > {language.t("common.default")} } > ) }} { const run = ++dialogRun void import("./dialog-select-server").then((x) => { if (dialogDead || dialogRun !== run) return 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 ( { if (toggleMcp.isPending) return toggleMcp.mutate(name) }} disabled={toggleMcp.isPending && toggleMcp.variables === name} > {name} {language.t("mcp.auth.clickToAuthenticate")} event.stopPropagation()}> { if (toggleMcp.isPending) return toggleMcp.mutate(name) }} /> ) }} 0} fallback={ {language.t("dialog.lsp.empty")} } > {(item) => ( {item.name || item.id} )} 0} fallback={{pluginEmpty()}} > {(plugin) => ( {plugin} )} ) }
{file}