File size: 10,215 Bytes
5f43c7d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
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>
  );
}