import React from "react";
import {
CheckCircle2, AlertTriangle, ShieldAlert, Bot, ScrollText, Flame, Coins,
Wrench, RefreshCw, Info, Clock, Cpu, GitBranch, Layers, MessageSquare,
Network, Map, Code2, Sparkles, ArrowRight, Link2, ChevronRight, Lightbulb, ShieldCheck,
Rocket, Database, Boxes, Server, Gauge, Scissors,
} from "lucide-react";
import { C, FD, FB, FM, fmt, fmtWhen } from "../theme.js";
import { BinaryLogo, GeneratedTag } from "./Primitives.jsx";
// SESSION REPORT — the executive cover page for a session (the simplified default
// view). It unifies every deterministic signal Her produces — cost, outcome, the
// real binaries run, the high-impact actions + risk, timestamps, and the cited
// recommendations — and is the jumping-off point to the Journey Graph (Mode A) and
// the turn-by-turn detail (Mode B). Numbers are the engine; the only GENERATED
// prose is the "What happened" overview (labelled). Suggest, never assert.
const COST_W = { in: 1.0, cacheCreate: 1.25, cacheRead: 0.1, out: 5.0 };
const RISK = {
High: { c: C.red, note: "high-impact actions detected" },
Medium: { c: C.amber, note: "review the flagged actions" },
Low: { c: C.cyan, note: "minor actions only" },
None: { c: C.muted, note: "nothing high-impact" },
};
const TAG = {
LIVE: { c: C.red, icon: Rocket },
PRODUCTION: { c: C.red, icon: Rocket }, // legacy alias
SECURITY: { c: C.orange, icon: ShieldAlert },
DATA: { c: "#f472b6", icon: Database },
NETWORK: { c: C.cyan, icon: Network },
CONFIG: { c: C.amber, icon: Code2 },
DEV: { c: C.blue, icon: Server },
};
function durationStr(a, b) {
if (!a || !b) return "—";
const ms = new Date(b).getTime() - new Date(a).getTime();
if (isNaN(ms) || ms < 0) return "—";
const s = Math.round(ms / 1000);
const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60), ss = s % 60;
return h ? `${h}h ${m}m` : m ? `${m}m ${ss}s` : `${ss}s`;
}
// A friendly type tag for a binary in "Tools Discovered".
function binKind(b) {
const blob = `${b.product || ""} ${b.blurb || ""} ${b.security || ""}`.toLowerCase();
if (/\b(database|postgres|sql|mongo|redis|sqlite|db client)\b/.test(blob)) return "DB CLI";
if (b.via && b.via !== "direct") return "pkg";
if (/\bcli\b/.test(blob) || b.identified) return "CLI";
return "binary";
}
export default function SessionReport({
session, turns, binaries, entities, impact, recommendations, advice,
overview, narrated, onOpenTurn, onOpenJourney, onOpenRaw, onAskHer, chatOpen,
}) {
const S = session;
const tk = S.tokens || {};
// point-in-time window occupancy ("fuel gauge") — distinct from the cumulative tk.*
const ctx = S.context || { peak: 0, limit: 1e6, peakPct: 0, trajectory: [], compactions: [], overLimit: [] };
const reqs = turns.reduce((s, t) => s + (t.reqs || 0), 0);
const im = impact || { riskLevel: "None", actions: [], outcome: null };
const outcome = im.outcome || { label: "—", detail: "" };
const risk = RISK[im.riskLevel] || RISK.None;
const agentPct = Math.round(100 * (S.indirectRatio || 0));
// cost breakdown (by COST contribution — this is "why this COST so much")
const parts = [
{ label: "Cache re-reads", raw: tk.cacheRead || 0, cost: (tk.cacheRead || 0) * COST_W.cacheRead, c: C.orange },
{ label: "Generated (agent output)", raw: tk.out || 0, cost: (tk.out || 0) * COST_W.out, c: C.cyan },
{ label: "Cache write", raw: tk.cacheCreate || 0, cost: (tk.cacheCreate || 0) * COST_W.cacheCreate, c: C.amber },
{ label: "Fresh input", raw: tk.in || 0, cost: (tk.in || 0) * COST_W.in, c: C.blue },
];
const totalCost = parts.reduce((s, p) => s + p.cost, 0) || 1;
const driver = parts.reduce((a, b) => (b.cost > a.cost ? b : a), parts[0]);
// most important turns — by cost
const ranked = [...turns]
.filter((t) => (t.tokens?.cost ?? 0) > 0)
.sort((a, b) => (b.tokens.cost ?? 0) - (a.tokens.cost ?? 0))
.slice(0, 4);
// tools discovered = binaries (real CLIs/pkgs) + skills/plugins + sub-agents + MCP
const bins = (binaries || []).map((b) => ({ ...b, glyph: "binary", kind: binKind(b) }));
const skills = (entities?.skills || []).map((s) => ({ name: s.name, turns: s.turns, glyph: "skill", kind: "skill" }));
const agents = (entities?.subAgents || []).map((a) => ({ name: a.name, turns: a.turns, glyph: "agent", kind: a.via === "workflow" ? "workflow" : "agent" }));
const mcp = (entities?.mcpServers || []).map((m) => ({ name: m.name, turns: m.turns, glyph: "mcp", kind: "MCP" }));
const tools = [...bins, ...skills, ...agents, ...mcp];
const recs = (advice?.recommendations?.length ? advice.recommendations : recommendations) || [];
function copySummary() {
const lines = [
`Her · session report — ${S.sessionId ? S.sessionId.slice(0, 8) : ""}`,
`${S.cwd || ""}`,
`Outcome: ${outcome.label} (${outcome.detail})`,
`Token cost: ${fmt(S.cost || 0)} cost-weighted tokens (not $) · cache re-reads ${fmt(tk.cacheRead || 0)} cumulative across ${reqs} round-trips · agent-driven ${agentPct}%`,
`Peak context: ${fmt(ctx.peak || 0)} / ${fmt(ctx.limit || 0)} (${Math.round((ctx.peakPct || 0) * 100)}% of the window)${ctx.compactions?.length ? ` · ${ctx.compactions.length} compaction(s)` : ""}`,
`Risk: ${im.riskLevel} (${im.riskReason || ""})`,
`Tools: ${tools.map((t) => t.product || t.name).join(", ")}`,
im.actions?.length ? `Actions: ${im.actions.map((a) => `${a.title} [${a.tag}]`).join("; ")}` : "",
`Started ${fmtWhen(S.startedAt)} · ${durationStr(S.startedAt, S.endedAt)} · ${S.model || ""}`,
].filter(Boolean);
try { navigator.clipboard.writeText(lines.join("\n")); } catch { /* ignore */ }
}
return (
{/* title + actions */}
Session Report
Executive summary for this coding-agent session — what happened, why it cost, and what to review.
{/* stat band */}
{/* main grid */}
{/* WHAT HAPPENED */}
{overview?.text ? (
<>
{sentences(overview.text).map((s, i) => (
{s}
))}
>
) : (
{S.turns} queries · {S.tools} tool calls · {S.humanTurns} human / {S.systemTurns} system turns. (Plain-English summary appears when the local model is running.)
{/* COST & CONTEXT — one widget, two halves of the same token story: cumulative
spend (left, no ceiling) vs the point-in-time window gauge (right, ≤1M). Side
by side so the two quantities can't be mistaken for each other. */}
{/* bottom grid */}
Nothing stands out to change — expensive but clean. No loops, avoidable re-reads, or CLI flailing.
)}
{/* SESSION AT A GLANCE */}
Copy summary
);
}
// ---------- small presentational atoms (Tactical Grey) ----------------------
function ReportBtn({ icon: Icon, text, onClick, primary, active }) {
// `active` (panel open) gives a pressed look so it reads as a toggle — click again to close.
const bg = primary ? (active ? C.orangeHi : C.orange) : (active ? C.orangeMut : "transparent");
const fg = primary ? "#fff" : (active ? C.orange : C.text2);
return (
{text}
);
}
function StatCard({ label, icon: Icon, iconColor, value, valueColor, sub, grad, big }) {
return (
{label}
{Icon && }
{big && Icon && }{value}
{sub &&
{sub}
}
);
}
// PEAK CONTEXT — the "fuel gauge" headline: the fullest the live window ever got,
// over the model's limit. Point-in-time, bounded by the window (≤1M) — the opposite
// kind of number from the cumulative cost/cache stats beside it.
function PeakContextCard({ ctx }) {
const pct = Math.round((ctx.peakPct || 0) * 100);
const over = (ctx.overLimit || []).length > 0;
const near = pct >= 80;
const c = over ? C.red : near ? C.amber : C.cyan;
return (
PEAK CONTEXT
{fmt(ctx.peak || 0)} / {fmt(ctx.limit || 0)}
{over ? "⚠ a request exceeded the window — data suspect" : `${pct}% of the window · point-in-time, not cumulative`}
);
}
// COST & CONTEXT — the unified token story in one card. LEFT: cumulative cost
// breakdown (no ceiling). RIGHT: the point-in-time window gauge + trajectory (≤1M).
// The side-by-side contrast IS the point — cumulative ≠ window occupancy.
function CostContextCard({ S, parts, totalCost, driver, ctx, reqs, onOpenTurn }) {
return (
{/* LEFT — cumulative spend */}
{/* Whole phrase is the hover target — the native `title` lives on this
(a `title` on an inline
{/* RIGHT — point-in-time window */}
);
}
function SubHead({ text }) {
return
{text}
;
}
// CONTEXT WINDOW trajectory — an area chart of how full the live window got per turn
// (the gauge over time), with compaction markers (sharp drops) and the 1M ceiling.
function ContextTrajectory({ ctx, reqs, onOpenTurn }) {
const traj = ctx.trajectory || [];
const limit = ctx.limit || 1e6;
const compactTurns = new Set((ctx.compactions || []).map((c) => c.atTurn));
const W = 300, H = 84, n = traj.length;
if (!n) return ;
const x = (i) => (n === 1 ? W / 2 : (i / (n - 1)) * W);
const y = (v) => H - Math.min(1, v / limit) * H;
const pts = traj.map((e, i) => `${x(i).toFixed(1)},${y(e.end).toFixed(1)}`);
const area = `M0,${H} L${pts.join(" L")} L${W},${H} Z`;
const line = `M${pts.join(" L")}`;
const over = (ctx.overLimit || []).length > 0;
const peakPct = Math.round((ctx.peakPct || 0) * 100);
return (
Peak fill = 80 ? C.amber : C.cyan }}>{fmt(ctx.peak || 0)} of {fmt(limit)} ({peakPct}%). The live window — bounded by {fmt(limit)} — across {reqs} round-trips. Not the cumulative totals.
{ctx.compactions?.length ? (
{ctx.compactions.length} compaction{ctx.compactions.length === 1 ? "" : "s"} detected — the window was trimmed where it dips.{" "}
{ctx.compactions.map((c) => (
onOpenTurn(c.atTurn)} className="lift" style={{ cursor: "pointer", color: C.orange, fontFamily: FM }}>
#{String(c.atTurn).padStart(2, "0")} ({fmt(c.before)}→{fmt(c.after)}){" "}
))}
) : (
No compactions — the window climbed to {peakPct}% and never had to be trimmed.
)}
);
}
function Card({ icon: Icon, title, accent, children }) {
return (
{title}
{children}
);
}
function Glance({ icon: Icon, label, value }) {
return (
{label}{value}
);
}
function AttrBadge({ attribution, scoped }) {
const generally = attribution && attribution !== "Anthropic";
return (
{generally ? "GENERALLY RECOMMENDED" : "ANTHROPIC"}
);
}
function Empty({ text }) {
return
{text}
;
}
// ---------- helpers ---------------------------------------------------------
function sentences(text) {
return String(text || "")
.split(/(?<=[.!?])\s+/)
.map((s) => s.trim())
.filter((s) => s.length > 2)
.slice(0, 6);
}
function firstClause(p) {
const s = String(p || "").replace(/\s+/g, " ").trim();
return s.length > 48 ? s.slice(0, 48) + "…" : s || "(turn)";
}
function clip(s, n) {
s = String(s || "");
return s.length > n ? s.slice(0, n).trimEnd() + "…" : s;
}
function modelLabel(m) {
if (!m) return "—";
return String(m).replace(/^claude-/, "Claude ").replace(/-(\d{8})$/, "").replace(/-/g, " ");
}
// Leading glyph for a Tools-Discovered chip: binaries get their logo (monogram
// fallback); skills/plugins, sub-agents, and MCP servers get a typed icon.
function toolGlyph(t) {
if (t.glyph === "binary") return ;
if (t.glyph === "mcp") return ;
if (t.glyph === "skill") return ;
return ; // agent / workflow
}