import { useCallback, useEffect, useMemo, useState } from "react"; import { DataTableStackView, KeyValueRow, SectionHeading, Spinner, usePaneFooter, type DataTableCell, type DataTableColumn } from "../../../components"; import type { ConnectionHealthState, ConnectionHealthStatus, } from "../../../core/connection-health"; import { colors } from "../../../theme/colors"; import type { PaneProps } from "../../../types/plugin"; import { Box, ScrollBox, Text, TextAttributes } from "../../../ui"; import { truncateToDisplayWidth } from "../../../utils/format"; import { formatRelativeAge } from "../../../utils/relative-time"; import { useConnectionHealth, usePluginPaneState } from "../../runtime"; interface ConnectionColumn extends DataTableColumn { id: "service" | "status" | "request" | "latency" | "last"; } const SORT_COLUMNS: ConnectionColumn["id"][] = ["status", "service", "request", "latency", "last"]; const STATUS_ORDER: Record = { error: 0, disconnected: 1, connecting: 2, connected: 3, idle: 4, }; function statusLabel(status: ConnectionHealthStatus): string { if (status === "connected") return "Connected"; if (status === "connecting") return "Connecting"; if (status === "disconnected") return "Disconnected"; if (status === "error") return "Error"; return "Idle"; } function statusColor(status: ConnectionHealthStatus): string { if (status === "connected") return colors.positive; if (status === "connecting") return colors.warning; if (status === "disconnected" || status === "error") return colors.negative; return colors.textMuted; } function formatLatency(latencyMs: number | null): string { if (latencyMs == null) return "-"; return latencyMs < 1000 ? `${Math.round(latencyMs)}ms` : `${(latencyMs / 1000).toFixed(1)}s`; } function columnsForWidth(width: number): ConnectionColumn[] { // SERVICE takes the room left over; the kit spreads it, so the fixed columns // (and the gaps between them) always fit. const showRequest = width >= 72; return [ { id: "service", label: "SERVICE", width: 16, align: "left", flexGrow: 1 }, { id: "status", label: "STATUS", width: 13, align: "left" }, ...(showRequest ? [{ id: "request", label: "REQUEST", width: 18, align: "left" } as ConnectionColumn] : []), { id: "latency", label: "LATENCY", width: 8, align: "right" }, { id: "last", label: "LAST", width: 9, align: "right" }, ]; } function ConnectionDetail({ source, width, now }: { source: ConnectionHealthState; width: number; now: number }) { const lineWidth = Math.max(24, width - 2); return ( {source.ownerId ? : null} {source.socketState ? : null} {source.lastTransitionAt ? ( ) : null} {source.currentDetail ? ( ) : null} {source.lastSuccess ? ( ) : null} {source.lastError ? ( ) : null} {source.recentRequests.length > 0 ? ( <> {source.recentRequests.slice(0, 8).map((request, index) => ( {request.success ? "ok" : "error"} {formatLatency(request.latencyMs)} {formatRelativeAge(request.at, now)} {truncateToDisplayWidth(request.error ?? request.operation, Math.max(8, lineWidth - 26))} ))} ) : null} ); } export function ConnectionsPane({ focused, width, height }: PaneProps) { const health = useConnectionHealth(); const [snapshot, setSnapshot] = useState(() => health.getSnapshot()); // Selection and the open source are pane state, so a reload or a shared // layout comes back to the same connection. const [selectedId, setSelectedId] = usePluginPaneState("selectedId", null); const [detailOpen, setDetailOpen] = usePluginPaneState("detailOpen", false); const [sort, setSort] = useState<{ columnId: ConnectionColumn["id"]; direction: "asc" | "desc" }>({ columnId: "status", direction: "asc", }); const [now, setNow] = useState(Date.now()); // Sources register asynchronously at boot, so an empty first snapshot is a load, // not an answer. const [settled, setSettled] = useState(() => health.getSnapshot().sources.length > 0); useEffect(() => { if (settled) return; const timer = setTimeout(() => setSettled(true), 2000); return () => clearTimeout(timer); }, [settled]); useEffect(() => health.subscribe(() => { setSnapshot(health.getSnapshot()); setSettled(true); }), [health]); useEffect(() => { const timer = setInterval(() => { setNow(Date.now()); setSnapshot(health.getSnapshot()); }, 5000); return () => clearInterval(timer); }, [health]); const sources = useMemo(() => [...snapshot.sources].sort((left, right) => { let comparison = 0; if (sort.columnId === "status") comparison = STATUS_ORDER[left.status] - STATUS_ORDER[right.status]; else if (sort.columnId === "service") comparison = left.name.localeCompare(right.name); else if (sort.columnId === "request") comparison = (left.lastOperation ?? "").localeCompare(right.lastOperation ?? ""); else if (sort.columnId === "latency") comparison = (left.lastLatencyMs ?? Number.POSITIVE_INFINITY) - (right.lastLatencyMs ?? Number.POSITIVE_INFINITY); else comparison = (left.lastRequestAt ?? 0) - (right.lastRequestAt ?? 0); return (sort.direction === "asc" ? comparison : -comparison) || (left.priority ?? 1000) - (right.priority ?? 1000) || left.name.localeCompare(right.name); }), [snapshot, sort]); const selected = sources.find((source) => source.id === selectedId) ?? sources[0] ?? null; const issues = sources.filter((source) => source.status === "error" || source.status === "disconnected").length; const connecting = sources.filter((source) => source.status === "connecting").length; useEffect(() => { if (!selectedId && sources[0]) setSelectedId(sources[0].id); if (selectedId && !sources.some((source) => source.id === selectedId)) { setSelectedId(sources[0]?.id ?? null); setDetailOpen(false); } }, [selectedId, sources]); const cycleSort = useCallback(() => { setSort((current) => { const currentIndex = SORT_COLUMNS.indexOf(current.columnId); return current.direction === "asc" ? { ...current, direction: "desc" } : { columnId: SORT_COLUMNS[(currentIndex + 1) % SORT_COLUMNS.length]!, direction: "asc" }; }); }, []); // The [s]ort hint is the binding: plain s only, and gone while a detail is open. usePaneFooter("connections", () => ({ info: [ ...(issues > 0 ? [{ id: "issues", parts: [{ text: `${issues} issue${issues === 1 ? "" : "s"}`, tone: "warning" as const }] }] : []), ...(connecting > 0 ? [{ id: "connecting", parts: [{ text: `${connecting} connecting`, tone: "muted" as const }] }] : []), ], hints: detailOpen ? [] : [{ id: "sort", key: "s", label: "ort", onPress: cycleSort }], }), [connecting, cycleSort, detailOpen, issues]); const renderCell = useCallback((source: ConnectionHealthState, column: ConnectionColumn): DataTableCell => { if (column.id === "service") return { text: source.name, color: colors.text }; if (column.id === "status") return { text: statusLabel(source.status), color: statusColor(source.status) }; if (column.id === "request") return { text: source.lastOperation ?? "-", color: colors.textDim }; if (column.id === "latency") return { text: formatLatency(source.lastLatencyMs), color: source.status === "error" ? colors.negative : colors.textMuted }; return { text: source.lastRequestAt ? formatRelativeAge(source.lastRequestAt, now) : "-", color: colors.textMuted }; }, [now]); return ( focused={focused} detailOpen={detailOpen && !!selected} onBack={() => setDetailOpen(false)} detailTitle={selected?.name} detailContent={selected ? : } rootWidth={width} rootHeight={height} selection={{ kind: "id", selectedId: selected?.id ?? null, getId: (source) => source.id, onChange: (id) => setSelectedId(id), }} onActivate={(source) => { setSelectedId(source.id); setDetailOpen(true); }} columns={columnsForWidth(width)} items={sources} sortColumnId={sort.columnId} sortDirection={sort.direction} onHeaderClick={(columnId) => { setSort((current) => current.columnId === columnId ? { ...current, direction: current.direction === "asc" ? "desc" : "asc" } : { columnId: columnId as ConnectionColumn["id"], direction: "asc" }); }} getItemKey={(source) => source.id} renderCell={renderCell} emptyContent={settled ? undefined : ( )} emptyStateTitle="No connection activity yet." emptyStateHint="Sources appear when providers and services register." /> ); }