import { For, Show, createMemo, createSignal, type Component } from "solid-js" import Switch from "@suid/material/Switch" import type { Instance, RawMcpStatus } from "../types/instance" import { useOptionalInstanceMetadataContext } from "../lib/contexts/instance-metadata-context" import { getLogger } from "../lib/logger" const log = getLogger("session") type ServiceSection = "lsp" | "mcp" | "plugins" interface InstanceServiceStatusProps { sections?: ServiceSection[] showSectionHeadings?: boolean class?: string initialInstance?: Instance } type ParsedMcpStatus = { name: string status: "running" | "stopped" | "error" error?: string } function parseMcpStatus(status?: RawMcpStatus): ParsedMcpStatus[] { if (!status || typeof status !== "object") return [] const result: ParsedMcpStatus[] = [] for (const [name, value] of Object.entries(status)) { if (!value || typeof value !== "object") continue const rawStatus = (value as { status?: string }).status if (!rawStatus) continue let mapped: ParsedMcpStatus["status"] if (rawStatus === "connected") mapped = "running" else if (rawStatus === "failed") mapped = "error" else mapped = "stopped" result.push({ name, status: mapped, error: typeof (value as { error?: unknown }).error === "string" ? (value as { error?: string }).error : undefined, }) } return result } const InstanceServiceStatus: Component = (props) => { const metadataContext = useOptionalInstanceMetadataContext() const instance = metadataContext?.instance ?? (() => { if (props.initialInstance) { return props.initialInstance } throw new Error("InstanceServiceStatus requires InstanceMetadataProvider or initialInstance prop") }) const isLoading = metadataContext?.isLoading ?? (() => false) const refreshMetadata = metadataContext?.refreshMetadata ?? (async () => Promise.resolve()) const sections = createMemo(() => props.sections ?? ["lsp", "mcp", "plugins"]) const includeLsp = createMemo(() => sections().includes("lsp")) const includeMcp = createMemo(() => sections().includes("mcp")) const includePlugins = createMemo(() => sections().includes("plugins")) const showHeadings = () => props.showSectionHeadings !== false const metadataAccessor = metadataContext?.metadata ?? (() => instance().metadata) const metadata = createMemo(() => metadataAccessor()) const hasLspMetadata = () => metadata()?.lspStatus !== undefined const hasMcpMetadata = () => metadata()?.mcpStatus !== undefined const hasPluginsMetadata = () => metadata()?.plugins !== undefined const lspServers = createMemo(() => metadata()?.lspStatus ?? []) const mcpServers = createMemo(() => parseMcpStatus(metadata()?.mcpStatus ?? undefined)) const plugins = createMemo(() => metadata()?.plugins ?? []) const isLspLoading = () => isLoading() || !hasLspMetadata() const isMcpLoading = () => isLoading() || !hasMcpMetadata() const isPluginsLoading = () => isLoading() || !hasPluginsMetadata() const [pendingMcpActions, setPendingMcpActions] = createSignal>({}) const setPendingMcpAction = (name: string, action?: "connect" | "disconnect") => { setPendingMcpActions((prev) => { const next = { ...prev } if (action) next[name] = action else delete next[name] return next }) } const toggleMcpServer = async (serverName: string, shouldEnable: boolean) => { const client = instance().client if (!client?.mcp) return const action: "connect" | "disconnect" = shouldEnable ? "connect" : "disconnect" setPendingMcpAction(serverName, action) try { if (shouldEnable) { await client.mcp.connect({ name: serverName }) } else { await client.mcp.disconnect({ name: serverName }) } await refreshMetadata() } catch (error) { log.error("Failed to toggle MCP server", { serverName, action, error }) } finally { setPendingMcpAction(serverName) } } const renderEmptyState = (message: string) => (

{message}

) const renderLspSection = () => (
LSP Servers
0} fallback={renderEmptyState(isLspLoading() ? "Loading LSP servers..." : "No LSP servers detected.")} >
{(server) => (
{server.name ?? server.id} {server.root}
{server.status === "connected" ? "Connected" : "Error"}
)}
) const renderMcpSection = () => (
MCP Servers
0} fallback={renderEmptyState(isMcpLoading() ? "Loading MCP servers..." : "No MCP servers detected.")} >
{(server) => { const pendingAction = () => pendingMcpActions()[server.name] const isPending = () => Boolean(pendingAction()) const isRunning = () => server.status === "running" const switchDisabled = () => isPending() || !instance().client const statusDotClass = () => { if (isPending()) return "status-dot animate-pulse" if (server.status === "running") return "status-dot ready animate-pulse" if (server.status === "error") return "status-dot error" return "status-dot stopped" } const statusDotStyle = () => (isPending() ? { background: "var(--status-warning)" } : undefined) return (
{server.name}
{ if (switchDisabled()) return void toggleMcpServer(server.name, Boolean(checked)) }} />
{(error) => (
{error()}
)}
) }}
) const renderPluginsSection = () => (
Plugins
0} fallback={renderEmptyState(isPluginsLoading() ? "Loading plugins..." : "No plugins configured.")} >
{(plugin) => (
{plugin}
)}
) return (
{renderLspSection()} {renderMcpSection()} {renderPluginsSection()}
) } export default InstanceServiceStatus