import { lazy, Suspense, useMemo, useState } from "react"; import Kpis from "../components/Kpis"; import Widget from "../components/WidgetModal"; import LiveActivity from "../components/LiveActivity"; import CostTokensChart from "../components/CostTokensChart"; import ModelMix from "../components/ModelMix"; import Replay from "../components/Replay"; import { useHive } from "../store"; import { replayTopoSource } from "../store/replay"; import { absTime } from "../lib/format"; // The topology graph pulls in d3-hierarchy; code-split it so that dependency // only loads when the Overview tab (the sole consumer) actually renders. const TopologyGraph = lazy(() => import("../components/TopologyGraph")); // The four-color status legend shown on the topology header. const LEGEND: { label: string; color: string }[] = [ { label: "running", color: "var(--run)" }, { label: "waiting", color: "var(--wait)" }, { label: "done", color: "var(--done)" }, { label: "error", color: "var(--crit)" }, ]; function StatusLegend() { return (
{LEGEND.map((l) => ( {l.label} ))}
); } export default function Overview() { const [topologyView, setTopologyView] = useState<"hive" | "planning">("hive"); const currentSession = useHive((s) => s.currentSession); const scopedAgents = useHive((s) => s.scopedAgents); const scope = useHive((s) => s.scope); const replay = useHive((s) => s.replay); // K5: when replay is active on THIS session's Overview, the topology/feed/chart // all render from the replay slice (events[0..cursor]); the Replay component // below becomes the transport. SSE keeps updating the live slice underneath. const replaying = replay.active && scope.level === "session" && replay.sessionId === scope.sessionId; const replaySlice = useMemo( () => (replaying ? replay.events.slice(0, replay.cursor + 1) : undefined), [replaying, replay.events, replay.cursor], ); // replayTopoSource reads topologyByHash + the session summary via getState(). // Subscribe to both here so the memo recomputes when a late ensureTopologyDetail // fetch resolves — otherwise the graph is stuck on the live fallback tree until // the user scrubs (M3). const replayHash = useHive((s) => (replaying ? s.sessionSummaries.get(replay.sessionId)?.topologyHash : undefined)); const replayDetail = useHive((s) => (replayHash ? s.topologyByHash.get(replayHash) : undefined)); const replaySource = useMemo( () => (replaySlice ? replayTopoSource(replaySlice) : undefined), [replaySlice, replayHash, replayDetail], ); const replayTs = replaying ? replay.events[replay.cursor]?.ts : undefined; const topoTitle = scope.level === "session" ? "Session topology" : "Agent Topology"; const teamCount = useHive((s) => s.scopedTeamCount); const agentCount = useHive((s) => s.scopedAgentCount); const topoMeta = `${teamCount} teams · ${agentCount} agents`; // First-run / empty state: no session has produced telemetry yet. Show a calm // in-language signature rather than a wall of empty panels. const isEmpty = !currentSession && scopedAgents.length === 0; if (isEmpty) { return (
Waiting for hive telemetry

Start a hive session in your project and this console lights up in real time — topology, activity, cost, and model mix.

pi · run a hive task
); } return ( <> {/* Persistent replay banner (K5). Present whenever replay drives this Overview, naming the session and the current cursor timestamp. */} {replaying && (
Replaying session {scope.sessionId.slice(0, 8)} — {replayTs ? absTime(replayTs) : "…"} · event {replay.cursor + 1} / {replay.events.length}
)} {/* Session replay (Phase F/K5): the transport controls. When engaged it drives the topology/feed/chart above; the live slice is untouched. */} {scope.level === "session" && (
)}
{topoMeta}} headExtra={} >
{replaying ? replay : currentSession?.live ? live : currentSession?.topologies?.active === topologyView ? active : null}
Loading topology…
}> {replaying ? : }
replay : streaming } > · last 60 min} headExtra={ cost/min tokens/min } > ); }