import { useEffect, useState } from "react";
import { fetchAgentStatus, type AgentStatus } from "../api";
import { createHighlighter } from "shiki";
import { Pulse, HardDrives, Shield, Cpu, Clock, CaretDown, CaretRight, CheckCircle, XCircle, Heartbeat, Lightning } from "@phosphor-icons/react";
import { useLocale } from "../hooks/useLocale";
function StatusCard({ title, icon: Icon, children, className = "" }: any) {
return (
);
}
function StatusRow({ label, value, sub }: any) {
return (
);
}
function TimeAgo({ date }: { date: string | number }) {
if (!date) return -;
const d = new Date(date);
const now = new Date();
const diff = Math.floor((now.getTime() - d.getTime()) / 1000);
let text = "";
if (diff < 60) text = `${diff}s ago`;
else if (diff < 3600) text = `${Math.floor(diff / 60)}m ago`;
else if (diff < 86400) text = `${Math.floor(diff / 3600)}h ago`;
else text = `${Math.floor(diff / 86400)}d ago`;
return {text};
}
// Module-level cache so switching tabs doesn't re-fetch
let _cache: { data: AgentStatus; html: string } | null = null;
export function AgentStatusPage() {
const [data, setData] = useState(_cache?.data ?? null);
const [loading, setLoading] = useState(!_cache);
const [html, setHtml] = useState(_cache?.html ?? "");
const [configExpanded, setConfigExpanded] = useState(false);
const { t } = useLocale();
useEffect(() => {
if (_cache) return; // Already have data, skip fetch
fetchAgentStatus()
.then(d => {
setData(d);
setLoading(false);
// Highlight config with dual theme
createHighlighter({
themes: ['github-dark', 'github-light'],
langs: ['json']
}).then(highlighter => {
const code = JSON.stringify(d.config, null, 2);
const out = highlighter.codeToHtml(code, {
lang: 'json',
themes: { dark: 'github-dark', light: 'github-light' },
defaultColor: false,
});
setHtml(out);
_cache = { data: d, html: out };
});
})
.catch(err => {
console.error(err);
setLoading(false);
});
}, []);
if (loading) {
return (
{/* Header skeleton */}
{/* Cards skeleton */}
{[1, 2, 3].map((i) => (
))}
{/* Config skeleton */}
);
}
if (!data) {
return (
{t("agent.error")}
);
}
const gw = data.gateway || {};
// Support both flat (gw.runtime) and nested (gw.service.runtime) structures
const runtime = gw.runtime || gw.service?.runtime || {};
const gwInfo = gw.gateway || {};
const isGwRunning = runtime.status === "running" || runtime.state === "active" || (runtime.pid && runtime.pid > 0);
const hb = data.heartbeat || {};
const checks = hb.checks || {};
return (
{/* Header */}
{t("agent.title")}
v{data.config.version || "0.0.0"}
{data.config.update?.channel || "stable"} {t("agent.channel")}
{/* Gateway Card */}
{isGwRunning ? t("agent.running") : t("agent.stopped")}
{isGwRunning && gw.gateway?.uptime &&
{t("agent.uptime")}}
{/* Models Card */}
{t("agent.primary")}
{data.config.agents?.defaults?.model?.primary || "default"}
{/* If we had more model stats they would go here */}
{t("agent.ready")}
{/* Heartbeat Card */}
{hb.lastRun ? (
<>
{t("agent.lastActivity")}
>
) : (
{t("agent.noHeartbeat")}
)}
{Object.entries(checks).map(([k, v]) => (
: "-"} />
))}
{/* Config Section */}
{configExpanded && (
);
}