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={ }
>
setTopologyView("hive")}>Hive
setTopologyView("planning")}>Planning
{replaying ?
replay
: currentSession?.live ?
live
: currentSession?.topologies?.active === topologyView ?
active : null}
Loading topology… }>
{replaying
?
: }
replay
:
streaming
}
>
· last 60 min}
headExtra={
cost/min
tokens/min
}
>
>
);
}