import React from "react";
import { Lightbulb, Sparkles, CheckCircle2, AlertTriangle, ArrowRight, ScrollText } from "lucide-react";
import { C, FD, FM, fmt, intentOf, intentIcon, turnSeverity, turnToolDimmed } from "../theme.js";
import { SecHead, Hero, GeneratedTag } from "./Primitives.jsx";
// MODE A centre: the JOURNEY. One node per turn (query). Node size proportional
// to COST (Anthropic token consumption — what you pay for, not raw cacheRead);
// the heaviest glow orange; over-budget turns get an amber ring; an amber tip
// badge where a guide exists. Sequence edges connect turns. Click a node -> Mode B.
//
// Topology truth: a Claude Code session is a linear spine per turn, NOT a process
// tree. So the journey is honestly drawn as a chain, not a fabricated tree.
export default function SessionGraph({ session, turns, narrated, overview, recommendations, advice, vis, onOpen }) {
const maxCost = Math.max(1, ...turns.map((t) => t.tokens.cost ?? 0));
// Legend visibility filters which turns are shown (by severity).
const visible = turns.filter((t) => vis[turnSeverity(t)]);
const autonomousPct = Math.round(
(100 * session.indirect) / Math.max(1, session.direct + session.indirect)
);
const _cost = (tt) => tt.tokens.cost ?? 0;
const heaviest = session.heavyTurns && session.heavyTurns.length
? session.heavyTurns.reduce((a, b) => (_cost(turns[a]) >= _cost(turns[b]) ? a : b))
: turns.reduce((a, t) => (_cost(t) > _cost(turns[a]) ? t.i : a), 0);
const overBudget = (session.overBudgetTurns || []).length;
// The engine DETECTS the fixable signals (deterministic). The local model WRITES
// the advice, scoped to this session. Prefer the model-scoped list when present;
// fall back to the deterministic recommendations (and per-item, scoped || fix).
const detRecs = Array.isArray(recommendations) ? recommendations : [];
const advRecs = advice && Array.isArray(advice.recommendations) ? advice.recommendations : null;
const recs = advRecs && advRecs.length ? advRecs : detRecs;
const adviceModel = advice && advice.model ? String(advice.model).split("/").pop() : null;
const scopedReady = !!(advRecs && advRecs.some((r) => r && r.scoped));
return (
Session graph
{session.systemTurns} SYSTEM · {session.humanTurns} HUMAN
{/* hero stat band — deterministic numbers only */}
{session.context && }
{/* WHAT HAPPENED — the human, plain-English story of the session, FIRST.
Narrator prose (labelled generated); the numbers above are the engine. */}
{overview && overview.text && (
<>
>
)}
{/* the journey */}
node size = token cost (cost-weighted, not $) · orange = heaviest · amber ring = over budget · = has a tip · click to drill in
{visible.map((x, idx) => {
const II = intentIcon(intentOf(x));
const heat = (x.tokens.cost ?? 0) / maxCost;
const size = 34 + Math.round(26 * heat);
const heavy = x.heavy;
const over = x.overBudget && !x.heavy; // over the absolute floor (amber ring)
const prev = visible[idx - 1];
// Tool-type legend filter: dim turns that use none of the visible tool
// types (e.g. leave only MCP on -> only MCP-using turns stay bright).
const dim = turnToolDimmed(vis, x);
return (
{idx > 0 && (
= 46 ? 28 : 24,
background: `linear-gradient(90deg,${prev.heavy ? C.orange : C.border},${heavy ? C.orange : C.border})`,
}}
/>
)}
onOpen(x.i)}
title={`Query ${String(x.i).padStart(2, "0")} — cost ${fmt(x.tokens.cost ?? 0)} · ${fmt(x.tokens.cacheRead)} cache re-reads (cumulative) · ${fmt(x.tokens.out)} generated${x.ctxPeak ? ` · window peaked ${fmt(x.ctxPeak)}` : ""}${dim ? " · (no visible tool type)" : ""}`}
style={{ display: "flex", flexDirection: "column", alignItems: "center", cursor: "pointer", flexShrink: 0, width: Math.max(58, size + 18), animationDelay: `${idx * 45}ms`, opacity: dim ? 0.26 : 1, filter: dim ? "grayscale(0.6)" : "none", transition: "opacity .15s ease, filter .15s ease" }}
>
{String(x.i).padStart(2, "0")}
{fmt(x.tokens.cost ?? 0)}
);
})}
{visible.length === 0 && (
All turn types hidden — re-enable a type in the legend.
)}
{/* deterministic SHAPE note — the engine's read of cost/heat, below the
journey. Numbers + heaviest picks + causality split, never the model. */}
ENGINE
Cost shape: {session.turns} turns ({session.humanTurns} human, {session.systemTurns} system) · {fmt(session.cost ?? session.tokens?.cost ?? 0)} cost (Anthropic token consumption). Heaviest turns{" "}
{(session.heavyTurns || []).map((n) => String(n).padStart(2, "0")).join(", ")}
{overBudget > 0 && <>; {overBudget} turn{overBudget === 1 ? "" : "s"} cleared the over-budget floor (≥{fmt(500000)} cost), not just the top 3>}.
{" "}(Deterministic — cost-ranked heaviest picks & causality split, not the model.)
{/* heaviest-turn callout */}
Query {String(heaviest).padStart(2, "0")} —
{fmt(turns[heaviest].tokens.cost ?? 0)} cost ({fmt(turns[heaviest].tokens.cacheRead)} cache re-reads, cumulative) across {turns[heaviest].tools.length} tools.{" "}
onOpen(heaviest)} style={{ color: C.orange, cursor: "pointer", fontFamily: FM, display: "inline-flex", alignItems: "center", gap: 3 }}>
open
{/* WHAT COULD HAVE BEEN BETTER — session-level, abstracted from the engine's
FIXABLE signals (re-reads, retry loops, CLI flailing). Each item names the
turn(s) and the cited Anthropic best practice. Deterministic + cited, NOT
model prose. Empty => an honest "expensive but clean". Placed AFTER the
journey: read the story, see the cost shape, then the takeaways. */}
ENGINE DETECTS
{scopedReady ? (
<>
·
{adviceModel || "narrator"} ADVISES
scoped to this session · grounded in Anthropic's best practices · suggest-only
>
) : (
<>the fixable signals; scoping advice to this session{advice === null ? "…" : " (model offline → showing the cited fix)"} · suggest-only>
)}
{recs.length > 0 ? (
recs.map((r, i) => {
const adviceText = r.scoped || r.advice; // model prose, else the cited fix
const isGen = !!r.scoped;
return (
{(r.turns || []).map((tn) => (
onOpen(tn)} title={`open turn ${tn}`}
style={{ cursor: "pointer", fontFamily: FM, fontSize: 10, color: C.orange, border: `1px solid ${C.orangeBd}`, background: C.orangeMut, borderRadius: 5, padding: "2px 7px" }}>
turn {String(tn).padStart(2, "0")}
))}
{r.headline}
{isGen && (
GENERATED
)}
{adviceText}
{r.practice && (
{r.attribution === "Anthropic" ? (
// Cited Anthropic guidance — cyan badge + the best-practices link.
<>
ANTHROPIC
{r.practice}
{r.source && <> ·
best-practices ↗>}
>
) : (
// Custom, non-Anthropic craft — amber badge, NO Anthropic link
// (an optional public source link if the practice carries one).
<>
GENERALLY RECOMMENDED
{r.practice}
{r.source && <> ·
source ↗>}
>
)}
)}
);
})
) : (
Nothing stands out to change — this session was expensive but clean. No retry loops, no avoidable re-reads, no CLI flailing without a skill.
)}
The numbers, the heaviest-turn picks, "no retry loops", and which turns are flagged are the deterministic engine. "What happened" and the scoped "what could have been better" advice are the local model writing prose from those signals + Anthropic's best-practices doc — suggestions, not assertions. Tap any node to drill in.
);
}