her / ui /src /components /DetailPanel.jsx
geekwrestler's picture
Squash history (purge pre-scrub demo session blobs)
5f43c7d
import React from "react";
import { Zap, Box, ArrowRight, CornerDownRight, AlertTriangle, Terminal } from "lucide-react";
import { C, FD, FM, fmt, toolIcon } from "../theme.js";
import { Bar, PanelTitle, GeneratedTag, BinaryRow } from "./Primitives.jsx";
import { turnProse } from "../useAnalysis.js";
// RIGHT panel. Mirrors the Elastic "powershell.exe detail" idea: full execution
// detail of the selected node. Three states below.
export function SessionDetail({ session }) {
const { tokens } = session;
const ctx = session.context;
return (
<div style={{ overflowY: "auto", padding: 16 }}>
<PanelTitle icon={Zap} text="SESSION TOKENS (CUMULATIVE)" />
<Bar label="cache re-reads" v={tokens.cacheRead} max={tokens.cacheRead} c={C.orange} note="summed across every round-trip — the cost driver, not window size" />
<Bar label="cache write" v={tokens.cacheCreate} max={tokens.cacheRead} c={C.amber} />
<Bar label="generated" v={tokens.out} max={tokens.cacheRead} c={C.cyan} />
<Bar label="fresh input" v={tokens.in} max={tokens.cacheRead} c={C.blue} />
<div style={{ fontSize: 11.5, color: C.text2, marginTop: 10, lineHeight: 1.55 }}>
Cache re-reads are <b style={{ color: C.orange }}>~{Math.round(tokens.cacheRead / Math.max(1, tokens.out))}×</b> generated — cumulative, so they vastly exceed the window. Spend tracks tool-call iterations, not answer length.
</div>
{ctx && (
<div style={{ fontSize: 11.5, color: C.text2, marginTop: 10, lineHeight: 1.55, background: C.black, border: `1px solid ${C.border}`, borderRadius: 7, padding: "8px 10px" }}>
Peak context window: <b style={{ color: (ctx.peakPct || 0) >= 0.8 ? C.amber : C.cyan }}>{fmt(ctx.peak)} / {fmt(ctx.limit)}</b> ({Math.round((ctx.peakPct || 0) * 100)}%) — the fuel gauge, bounded by the window. {ctx.compactions?.length ? `${ctx.compactions.length} compaction(s).` : "No compactions."}
</div>
)}
<PanelTitle icon={ArrowRight} text="WHO DROVE THE WORK" mt />
<CausalitySplit direct={session.direct} indirect={session.indirect} />
<div style={{ fontSize: 11, color: C.muted, marginTop: 8, lineHeight: 1.5 }}>
{Math.round((100 * session.indirect) / Math.max(1, session.direct + session.indirect))}% of tool calls were triggered by the agent's own prior output (proven value-flow).
</div>
</div>
);
}
export function TurnDetail({ turn, narrated, binaries }) {
const { tokens } = turn;
const prose = turnProse(narrated, turn.i);
// binaries (real tools behind Bash/npx) that fired in THIS turn — filtered from
// the session-level list by the turns each appeared in.
const turnBins = (binaries || []).filter((b) => (b.turns || []).includes(turn.i));
const a = turn.indirect > turn.direct * 1.5;
const deterministic = [
`This query set off ${turn.reqs} model round-trip${turn.reqs !== 1 ? "s" : ""} and ${turn.tools.length} tool call${turn.tools.length !== 1 ? "s" : ""}.`,
turn.tools.length
? a
? `${turn.indirect} (${Math.round((100 * turn.indirect) / turn.tools.length)}%) were driven by earlier tool output, not your message.`
: `${turn.direct} came from your instruction; ${turn.indirect} followed from earlier output.`
: "",
`Cost is dominated by cache re-reads — ${fmt(tokens.cacheRead)} cached tokens reloaded (cumulative across round-trips), vs ${fmt(tokens.out)} generated.`,
turn.ctxPeak ? `The live context window peaked at ${fmt(turn.ctxPeak)} during this turn (point-in-time, not cumulative).` : "",
].filter(Boolean).join(" ");
return (
<div style={{ overflowY: "auto", padding: 16 }}>
<PanelTitle icon={Zap} text="WHY THESE TOKENS" />
{prose ? (
<div style={{ marginTop: 10 }}>
<div style={{ marginBottom: 6 }}><GeneratedTag cites={`turn ${turn.i}`} /></div>
<div style={{ fontSize: 13, color: C.text2, lineHeight: 1.6 }}>{prose}</div>
</div>
) : (
<div style={{ fontSize: 13, color: C.text2, lineHeight: 1.6, marginTop: 10 }}>{deterministic}</div>
)}
<PanelTitle icon={Box} text="TOKEN BREAKDOWN (CUMULATIVE)" mt />
<Bar label="cache re-reads" v={tokens.cacheRead} max={tokens.cacheRead} c={C.orange} note="volume = long tool loops · summed across round-trips" />
<Bar label="cache write" v={tokens.cacheCreate} max={tokens.cacheRead} c={C.amber} />
<Bar label="generated" v={tokens.out} max={tokens.cacheRead} c={C.cyan} />
<Bar label="fresh input" v={tokens.in} max={tokens.cacheRead} c={C.blue} />
<PanelTitle icon={ArrowRight} text="CAUSALITY SPLIT" mt />
<CausalitySplit direct={turn.direct} indirect={turn.indirect} />
<div style={{ fontSize: 11, color: C.muted, marginTop: 8, lineHeight: 1.5 }}>
Indirect = the tool's input appeared verbatim in an earlier result (proven). Tap a tool to see its trigger.
</div>
{turnBins.length > 0 && (
<>
<PanelTitle icon={Terminal} text="BINARIES RUN HERE" mt />
<div style={{ fontSize: 10.5, color: C.muted, marginTop: 4, lineHeight: 1.5 }}>
The real tools behind this turn's Bash calls — extracted from the command, not just "npx".
</div>
<div style={{ marginTop: 6 }}>
{turnBins.map((b) => <BinaryRow key={b.binary} b={b} />)}
</div>
</>
)}
</div>
);
}
export function ToolDetail({ tool, onBack }) {
const Icon = toolIcon(tool.name);
const ind = tool.provenance === "indirect";
const provenFlow = ind && tool.flowValue;
const inputStr =
tool.input && typeof tool.input === "object"
? JSON.stringify(tool.input, null, 2)
: String(tool.input ?? tool.summary ?? "");
return (
<div style={{ overflowY: "auto", padding: 16 }}>
<PanelTitle icon={Icon} text="TOOL DETAIL" />
<div style={{ fontFamily: FD, fontSize: 15, fontWeight: 600, marginTop: 8, display: "flex", alignItems: "center", gap: 8 }}>
{tool.mcp ? `${tool.mcp.server}:${tool.mcp.tool}` : tool.name}
{tool.errored && (
<span style={{ fontFamily: FM, fontSize: 9, color: C.red, border: `1px solid ${C.red}`, borderRadius: 4, padding: "1px 6px", display: "inline-flex", alignItems: "center", gap: 4 }}>
<AlertTriangle size={10} /> ERRORED
</span>
)}
</div>
{tool.mcp && (
<div style={{ fontFamily: FM, fontSize: 9.5, color: C.cyan, marginTop: 6 }}>MCP · {tool.mcp.server}</div>
)}
{/* full input */}
<div style={{ fontFamily: FM, fontSize: 9.5, color: C.muted, marginTop: 12, marginBottom: 4 }}>FULL INPUT</div>
<pre style={{ fontFamily: FM, fontSize: 11, color: C.text2, margin: 0, background: C.black, padding: 10, borderRadius: 7, border: `1px solid ${C.borderSoft}`, whiteSpace: "pre-wrap", wordBreak: "break-word", maxHeight: 220, overflow: "auto", lineHeight: 1.5 }}>
{inputStr}
</pre>
{/* provenance verdict — proven vs hypothesis separated */}
<div style={{ marginTop: 14 }}>
{provenFlow ? (
<div style={{ background: C.orangeMut, border: `1px solid ${C.orangeBd}`, borderRadius: 8, padding: 12 }}>
<div style={{ color: C.orange, fontWeight: 600, fontSize: 13, display: "flex", alignItems: "center", gap: 7 }}>
<CornerDownRight size={15} /> Indirect — proven value-flow
</div>
<div style={{ fontSize: 12.5, color: C.text2, marginTop: 8, lineHeight: 1.55 }}>
Fired because a value from an earlier <b style={{ color: C.orange }}>{tool.sourceTool}</b> result reappeared <b>verbatim</b> in this input. This is asserted, not inferred.
</div>
<div style={{ marginTop: 10 }}>
<div style={{ fontFamily: FM, fontSize: 9.5, color: C.muted, marginBottom: 4 }}>VALUE THAT FLOWED</div>
<div style={{ fontFamily: FM, fontSize: 11, color: C.orange, background: C.black, padding: "6px 9px", borderRadius: 5, border: `1px solid ${C.orangeBd}`, wordBreak: "break-all" }}>
{tool.flowValue}
</div>
</div>
</div>
) : ind ? (
<div style={{ background: C.card, border: `1px dashed ${C.border}`, borderRadius: 8, padding: 12 }}>
<div style={{ color: C.amber, fontWeight: 600, fontSize: 13 }}>Indirect — proximity (hypothesis)</div>
<div style={{ fontSize: 12.5, color: C.text2, marginTop: 8, lineHeight: 1.55 }}>
Likely driven by an earlier <b>{tool.sourceTool || "tool"}</b> result, but no verbatim value was pinned. A hypothesis for you to judge — not asserted.
</div>
</div>
) : (
<div style={{ background: C.card, border: `1px solid ${C.border}`, borderRadius: 8, padding: 12 }}>
<div style={{ color: C.text, fontWeight: 600, fontSize: 13 }}>Direct — from your instruction</div>
<div style={{ fontSize: 12.5, color: C.text2, marginTop: 8, lineHeight: 1.55 }}>
No earlier tool result fed this input. It traces to your query.
</div>
</div>
)}
</div>
<div onClick={onBack} style={{ marginTop: 14, fontFamily: FM, fontSize: 11, color: C.muted, cursor: "pointer" }}>← back</div>
</div>
);
}
function CausalitySplit({ direct, indirect }) {
const tot = Math.max(1, direct + indirect);
return (
<div style={{ display: "flex", height: 26, borderRadius: 7, overflow: "hidden", border: `1px solid ${C.borderSoft}`, marginTop: 8 }}>
<div style={{ width: `${(100 * direct) / tot}%`, background: C.border, display: "flex", alignItems: "center", justifyContent: "center" }}>
{direct > 0 && <span style={{ fontFamily: FM, fontSize: 10, color: C.text }}>{direct} direct</span>}
</div>
<div style={{ width: `${(100 * indirect) / tot}%`, background: `linear-gradient(90deg,${C.orange},${C.orangeHi})`, display: "flex", alignItems: "center", justifyContent: "center" }}>
{indirect > 0 && <span style={{ fontFamily: FM, fontSize: 10, color: "#fff" }}>{indirect} indirect</span>}
</div>
</div>
);
}