import React, { useEffect, useState } from "react"; import { ClipboardList, Lightbulb, Map, FolderOpen, Sparkles, Bot, Boxes, Plug, Terminal } from "lucide-react"; import { C, FD, FB, FM, fmt, fmtWhen, intentOf, intentIcon, turnSeverity, shortPrompt } from "./theme.js"; import { useAnalysis, useApi, fetchAdvice, fetchOverview } from "./useAnalysis.js"; import { Chip, Stat, BinaryRow } from "./components/Primitives.jsx"; import Legend, { defaultVis } from "./components/Legend.jsx"; import SessionGraph from "./components/SessionGraph.jsx"; import TurnGraph from "./components/TurnGraph.jsx"; import { SessionDetail, TurnDetail, ToolDetail } from "./components/DetailPanel.jsx"; import SessionBrowser from "./components/SessionBrowser.jsx"; import ChatPanel from "./components/ChatPanel.jsx"; import ProjectView from "./components/ProjectView.jsx"; import ProjectsHome from "./components/ProjectsHome.jsx"; import SessionReport from "./components/SessionReport.jsx"; import DisclaimerModal, { needsDisclaimer } from "./components/DisclaimerModal.jsx"; // Session-level entity inventory (Mode A left rail): the skills, sub-agents, and // MCP servers this ONE session used, each traceable to the turn that launched it — // the same inventory the project view shows, scoped to the session. Renders nothing // when the session used none (honest silence). Sub-agents run in their OWN // transcripts, so this never changes the tool/causality counts in the Legend above. function SessionEntities({ entities, binaries, onOpen }) { const bins = binaries || []; const groups = [ ["subAgents", "Sub-agents", Bot, C.blue], ["skills", "Skills", Boxes, C.amber], ["mcpServers", "MCP servers", Plug, C.cyan], ]; const hasEntities = !!entities && groups.some(([k]) => (entities[k] || []).length); if (!hasEntities && !bins.length) return null; return ( <> {/* BINARIES — real tools run via Bash (npx remotion -> remotion, railway, …), a separate dimension from tool calls. Each row traces to its turn. */} {bins.length > 0 && ( <>
BINARIES RUN
via Bash {bins.length}
{bins.map((b) => )}
extracted from the command (the real binary behind npx/bash) · click to jump to the turn it fired
)} {!hasEntities ? null : ( <>
ENTITIES IN THIS SESSION
{groups.map(([k, label, Icon, color]) => { const rows = entities[k] || []; if (!rows.length) return null; return (
{label} {rows.length}
{rows.map((e) => (
e.turns && e.turns.length && onOpen(e.turns[0])} title={`used in turn(s) ${(e.turns || []).join(", ")}${e.via ? " · via " + e.via : ""}${e.workflow ? " · workflow " + e.workflow : ""}`} style={{ display: "flex", alignItems: "center", gap: 6, padding: "4px 8px", borderRadius: 6, cursor: "pointer", borderLeft: `2px solid ${color}` }}> {e.name} ×{e.count}
))}
); })}
launched from the cited turn · each runs in its own transcript, so it doesn't change the tool or causality counts above
)} ); } export default function App() { // sourcePath: null = no session loaded (welcome) · "__demo__" = the bundled demo // session (click-to-load, never a default) · else a real ~/.claude session, all // analyzed live by the API server. const [sourcePath, setSourcePath] = useState(null); const { status, data, narrated, error } = useAnalysis(sourcePath); const api = useApi(); // view: "report" = executive summary (DEFAULT) · "session" = Mode A journey graph // · "turn" = Mode B drill-in const [view, setView] = useState("report"); const [ti, setTi] = useState(0); // selected turn index const [tool, setTool] = useState(null); // selected tool index in Mode B const [vis, setVis] = useState(defaultVis); const [atHome, setAtHome] = useState(true); // top-level Projects landing is the default const [browserOpen, setBrowserOpen] = useState(false); const [chatOpen, setChatOpen] = useState(false); const [overview, setOverview] = useState(null); // plain-English "what happened" const [advice, setAdvice] = useState(null); // LLM "what could have been better", session-scoped const [projectCwd, setProjectCwd] = useState(null); // project scope (many sessions) const [showDisclaimer, setShowDisclaimer] = useState(needsDisclaimer()); // first-run opt-in const consentModal = showDisclaimer ? setShowDisclaimer(false)} /> : null; const hasApiEarly = api?.hasApi; // The human "what happened" overview shown at the top of Mode A. For the bundled // fixture we reuse the curated narrator outcome; for any real session we ask the // local model to write one (grounded in the turns). Narrator prose only — the // numbers stay deterministic. useEffect(() => { setOverview(null); if (atHome || status !== "ready") return; let alive = true; const curated = narrated && (narrated.session_outcome || narrated.summary || narrated.outcome); const fromCurated = () => curated && setOverview({ text: String(curated).replace(/^Generated summary[^\n]*\n+/i, "").trim(), label: "narrator" }); // Prefer the freshly-generated "what happened" (plain English, doesn't dwell on // tokens) for ANY session; fall back to a curated narrated outcome if the API // or model is down. if (hasApiEarly) { (async () => { try { const j = await fetchOverview(sourcePath); if (!alive) return; if (j && j.overview) setOverview({ text: j.overview, label: j.model ? j.model.split("/").pop() : "narrator" }); else fromCurated(); } catch { if (alive) fromCurated(); } })(); } else { fromCurated(); } return () => { alive = false; }; }, [status, sourcePath, narrated, hasApiEarly, atHome]); // Session-scoped advice ("what could have been better") — the local model writes // it from the engine's fired signals + this session's objective + the Anthropic // codebook. Best-effort: the deterministic recommendations render instantly and // this upgrades them to scoped prose when the model answers. useEffect(() => { setAdvice(null); if (atHome || status !== "ready" || !hasApiEarly) return; let alive = true; (async () => { try { const j = await fetchAdvice(sourcePath); if (alive && j && Array.isArray(j.recommendations)) setAdvice(j); } catch { /* fall back to the deterministic recommendations */ } })(); return () => { alive = false; }; }, [status, sourcePath, hasApiEarly, atHome]); const openTurn = (i, toolIdx = null) => { setView("turn"); setTi(i); setTool(toolIdx); }; const pickSession = (path) => { setAtHome(false); setSourcePath(path); setView("report"); setTi(0); setTool(null); setBrowserOpen(false); setProjectCwd(null); }; const openProject = (cwd) => { setAtHome(false); setProjectCwd(cwd); setBrowserOpen(false); setChatOpen(false); }; const goHome = () => { setAtHome(true); setProjectCwd(null); setChatOpen(false); setBrowserOpen(false); }; // The bundled demo session — loads ONLY on this explicit click (the "__demo__" // sentinel the server resolves to fixtures/demo-session.jsonl). Never a default. const openDemo = () => pickSession("__demo__"); const hasApi = api?.hasApi; // DEFAULT LANDING — the top-level Projects list. From here: project -> sessions // (ProjectView) -> a session (the graph). ProjectsHome handles its own // loading/no-engine states, so we only wait while the API probe is in flight. if (atHome) { if (api == null) return ; return ( <> ); }