@narumitw/pi-inspect 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -2
  3. package/dist/app.css +1 -0
  4. package/dist/app.js +128 -0
  5. package/dist/index.html +1 -0
  6. package/docs/REVIEW.md +189 -0
  7. package/docs/VERIFICATION.md +186 -0
  8. package/docs/viewer.md +74 -0
  9. package/package.json +67 -4
  10. package/src/ancestry.ts +32 -0
  11. package/src/collector.ts +143 -0
  12. package/src/context.ts +115 -0
  13. package/src/correlation.ts +27 -0
  14. package/src/entry-index.ts +17 -0
  15. package/src/entry-summary.ts +34 -0
  16. package/src/extension.ts +259 -0
  17. package/src/feed.ts +145 -0
  18. package/src/identity.ts +112 -0
  19. package/src/index.ts +1 -0
  20. package/src/model.ts +117 -0
  21. package/src/native-projection.ts +28 -0
  22. package/src/privacy.ts +136 -0
  23. package/src/projection.ts +374 -0
  24. package/src/server.ts +202 -0
  25. package/src/system-message.ts +117 -0
  26. package/src/web/api.ts +137 -0
  27. package/src/web/app.tsx +570 -0
  28. package/src/web/captured-executions.tsx +177 -0
  29. package/src/web/components.css +261 -0
  30. package/src/web/components.tsx +241 -0
  31. package/src/web/composition.css +554 -0
  32. package/src/web/context-composition.tsx +329 -0
  33. package/src/web/context-inventory.tsx +65 -0
  34. package/src/web/entry-calls.ts +14 -0
  35. package/src/web/format.ts +94 -0
  36. package/src/web/hierarchy.ts +117 -0
  37. package/src/web/history.ts +56 -0
  38. package/src/web/inline-entry.tsx +124 -0
  39. package/src/web/inspector.tsx +386 -0
  40. package/src/web/json-tree.tsx +74 -0
  41. package/src/web/overview.tsx +67 -0
  42. package/src/web/panels.css +267 -0
  43. package/src/web/panes.tsx +123 -0
  44. package/src/web/recorded-content.tsx +49 -0
  45. package/src/web/search.ts +16 -0
  46. package/src/web/session-filters.tsx +100 -0
  47. package/src/web/session.css +114 -0
  48. package/src/web/style.css +332 -0
  49. package/src/web/styles.d.ts +1 -0
  50. package/src/web/timeline.tsx +33 -0
  51. package/src/web/timing.ts +26 -0
  52. package/src/web/trace.css +411 -0
  53. package/src/web/trace.tsx +313 -0
  54. package/src/web/virtual-context.ts +138 -0
@@ -0,0 +1,124 @@
1
+ import { Button, Tabs, Text } from "@radix-ui/themes";
2
+ import type { EntrySummary } from "../model.js";
3
+ import { useDetail } from "./api.js";
4
+ import { Data, Metadata } from "./components.js";
5
+ import { count, eventName, record } from "./format.js";
6
+ import { label } from "./hierarchy.js";
7
+
8
+ export function InlineEntry({ entry, inspect }: { entry: EntrySummary; inspect(id: string): void }) {
9
+ const { detail, error, retry } = useDetail(entry.id);
10
+ const raw = record(detail?.raw.value);
11
+ const message = record(raw?.message);
12
+ const content = raw && Object.hasOwn(raw, "data") ? raw.data : message?.content;
13
+ const textBlocks =
14
+ Array.isArray(content) &&
15
+ content.every((block) => {
16
+ const item = record(block);
17
+ return item?.type === "text" && typeof item.text === "string";
18
+ })
19
+ ? content.map((block) => record(block)?.text).join("\n")
20
+ : undefined;
21
+ const preview = textBlocks ?? content;
22
+ const inputs = Array.isArray(message?.content)
23
+ ? message.content.flatMap((block) => {
24
+ const item = record(block);
25
+ return item?.type === "toolCall" ? [item.arguments ?? null] : [];
26
+ })
27
+ : [];
28
+ const usage = record(message?.usage);
29
+ const input = typeof usage?.input === "number" && usage.input >= 0 ? usage.input : undefined;
30
+ const output = typeof usage?.output === "number" && usage.output >= 0 ? usage.output : undefined;
31
+ const known = input !== undefined && output !== undefined;
32
+ return (
33
+ <div className="inline-entry">
34
+ <Tabs.Root defaultValue="overview" className="inline-tabs">
35
+ <Tabs.List>
36
+ <Tabs.Trigger value="overview">Overview</Tabs.Trigger>
37
+ <Tabs.Trigger value="content">Content</Tabs.Trigger>
38
+ <Tabs.Trigger value="raw">Raw</Tabs.Trigger>
39
+ <Tabs.Trigger value="metadata">Metadata</Tabs.Trigger>
40
+ </Tabs.List>
41
+ <Tabs.Content value="overview">
42
+ <div className="inline-summary">
43
+ <Metadata
44
+ rows={[
45
+ ["Type", eventName(entry.kind)],
46
+ ["Description", label(entry)],
47
+ ["Parent ID", entry.parentId ?? "root"],
48
+ ["Timestamp", entry.timestamp],
49
+ ...(entry.tokens === undefined ? [] : [["Tokens", count(entry.tokens)] as [string, string]]),
50
+ ]}
51
+ />
52
+ {known && (
53
+ <div className="usage-breakdown">
54
+ <div className="usage-bar">
55
+ <span style={{ width: `${input + output ? (input / (input + output)) * 100 : 0}%` }} />
56
+ <span style={{ width: `${input + output ? (output / (input + output)) * 100 : 0}%` }} />
57
+ </div>
58
+ <span>
59
+ {count(input)} input · {count(output)} output (cache not included)
60
+ </span>
61
+ </div>
62
+ )}
63
+ </div>
64
+ {content !== undefined && (
65
+ <Data
66
+ label={entry.kind === "toolResult" ? "Recorded tool output" : "Recorded content"}
67
+ data={{ value: preview ?? null, truncated: Boolean(detail?.raw.truncated) }}
68
+ scope={`${entry.id}-content`}
69
+ />
70
+ )}
71
+ {inputs.length > 0 && (
72
+ <Data
73
+ label="Recorded tool arguments"
74
+ data={{ value: inputs, truncated: Boolean(detail?.raw.truncated) }}
75
+ scope={`${entry.id}-arguments`}
76
+ defaultOpen={false}
77
+ />
78
+ )}
79
+ <Button size="1" variant="ghost" onClick={() => inspect(entry.id)}>
80
+ Inspect complete entry →
81
+ </Button>
82
+ </Tabs.Content>
83
+ <Tabs.Content value="content">
84
+ <Data
85
+ label="Recorded content"
86
+ data={
87
+ content === undefined ? undefined : { value: preview ?? null, truncated: Boolean(detail?.raw.truncated) }
88
+ }
89
+ scope={`${entry.id}-content`}
90
+ />
91
+ </Tabs.Content>
92
+ <Tabs.Content value="raw">
93
+ <Data label="Raw entry · redacted display copy" data={detail?.raw} scope={`${entry.id}-raw`} />
94
+ </Tabs.Content>
95
+ <Tabs.Content value="metadata">
96
+ <Metadata
97
+ rows={[
98
+ ["ID", entry.id],
99
+ ["Parent ID", entry.parentId ?? "root"],
100
+ ["Timestamp", entry.timestamp],
101
+ ["Source", "Session log entry, not an execution span"],
102
+ ]}
103
+ />
104
+ <Data label="Contribution at this node" data={detail?.projected} scope={`${entry.id}-projection`} />
105
+ </Tabs.Content>
106
+ </Tabs.Root>
107
+ {error && (
108
+ <div role="alert">
109
+ <Text size="1" color="red">
110
+ {error}
111
+ </Text>
112
+ <Button size="1" variant="ghost" onClick={retry}>
113
+ Retry inline details
114
+ </Button>
115
+ </div>
116
+ )}
117
+ {!detail && !error && (
118
+ <Text size="1" color="gray">
119
+ Loading bounded entry details…
120
+ </Text>
121
+ )}
122
+ </div>
123
+ );
124
+ }
@@ -0,0 +1,386 @@
1
+ import * as Collapsible from "@radix-ui/react-collapsible";
2
+ import { ChevronRightIcon, CubeIcon } from "@radix-ui/react-icons";
3
+ import { Button, Heading, Tabs, Text } from "@radix-ui/themes";
4
+ import { useEffect, useMemo, useState } from "react";
5
+ import type { BranchView, Call, Capture, DetailView, EntrySummary, Json, Snapshot } from "../model.js";
6
+ import { Copy, Data, Metadata, Status } from "./components.js";
7
+ import { entryCalls } from "./entry-calls.js";
8
+ import { count, duration, eventName, output, promptDiff, record, scripts } from "./format.js";
9
+ import { RecordedContent } from "./recorded-content.js";
10
+
11
+ export function Inspector({
12
+ selected,
13
+ node,
14
+ detail,
15
+ detailError,
16
+ retryDetails,
17
+ call,
18
+ branch,
19
+ branchError,
20
+ retryBranch,
21
+ snapshot,
22
+ tab,
23
+ changeTab,
24
+ select,
25
+ selectCall,
26
+ preview,
27
+ }: {
28
+ selected: string;
29
+ node?: EntrySummary;
30
+ detail?: DetailView;
31
+ detailError: string;
32
+ retryDetails(): void;
33
+ call?: Call;
34
+ branch?: BranchView;
35
+ branchError: string;
36
+ retryBranch(): void;
37
+ snapshot?: Snapshot;
38
+ tab: string;
39
+ changeTab(tab: string): void;
40
+ select(id: string): void;
41
+ selectCall(id: string): void;
42
+ preview(): void;
43
+ }) {
44
+ const [format, setFormat] = useState("formatted");
45
+ const raw = record(detail?.raw.value);
46
+ const message = record(raw?.message);
47
+ const payload = raw?.data ?? message?.content;
48
+ const related = snapshot?.nodes.filter((entry) => entry.parentId === selected || entry.id === node?.parentId) ?? [];
49
+ const recordedCall = snapshot?.calls.find(
50
+ (item) => item.id === node?.toolCallId && item.branchAnchor === detail?.toolAnchor && !item.parentOccurrenceId,
51
+ );
52
+ const liveRaw: Capture | undefined = call
53
+ ? {
54
+ value: JSON.parse(JSON.stringify(call)) as Json,
55
+ truncated: Boolean(call.args.truncated || call.result?.truncated),
56
+ }
57
+ : undefined;
58
+ const activeRaw = liveRaw ?? detail?.raw;
59
+ const captured = entryCalls(detail?.raw, snapshot?.calls ?? [], detail?.toolAnchor);
60
+ const code = call ? (record(call.args.value)?.code as Json | undefined) : scripts(detail?.raw)?.value;
61
+ const codemode = call?.name === "codemode" || Boolean(scripts(detail?.raw)) || message?.toolName === "codemode";
62
+ const isCall = Boolean(call);
63
+ const hasSkills = Boolean(
64
+ snapshot?.skills.length || (Array.isArray(branch?.skillEvidence.value) && branch.skillEvidence.value.length),
65
+ );
66
+ const enabled = useMemo(
67
+ () => [
68
+ "content",
69
+ "raw",
70
+ ...(isCall ? [] : ["prompt", "tools", "context", ...(hasSkills ? ["skills"] : [])]),
71
+ ...(codemode ? ["codemode"] : []),
72
+ ],
73
+ [isCall, hasSkills, codemode],
74
+ );
75
+ useEffect(() => {
76
+ if (!enabled.includes(tab)) changeTab("content");
77
+ }, [enabled, tab, changeTab]);
78
+ return (
79
+ <section className="panel inspector-panel">
80
+ <div className="panel-heading">
81
+ <Heading size="3">
82
+ <CubeIcon />
83
+ Details
84
+ </Heading>
85
+ {activeRaw && <Copy key={selected} value={output(activeRaw.value)} />}
86
+ </div>
87
+ <div className="inspector-identity">
88
+ {call?.branchAnchor && !snapshot?.nodes.some((entry) => entry.id === call.branchAnchor) && (
89
+ <Text size="1" color="gray">
90
+ Recorded anchor omitted from this bounded inventory; navigator target unavailable.
91
+ </Text>
92
+ )}
93
+ <Heading size="4">
94
+ {call ? "Captured execution" : node ? eventName(node.kind) : "Entry"} ·{" "}
95
+ {selected.slice(0, 16) || "no selection"}
96
+ </Heading>
97
+ <Text size="2" color="gray">
98
+ {call?.name ?? node?.name ?? "Session log entry"}
99
+ </Text>
100
+ </div>
101
+ {detailError && (
102
+ <div role="alert" className="detail-failure">
103
+ {detailError}
104
+ <Button size="1" variant="ghost" onClick={retryDetails}>
105
+ Retry selected details
106
+ </Button>
107
+ </div>
108
+ )}
109
+ {branchError && (
110
+ <div role="alert" className="branch-failure">
111
+ {branchError}
112
+ <Button size="1" variant="ghost" onClick={retryBranch}>
113
+ Retry selected branch
114
+ </Button>
115
+ </div>
116
+ )}
117
+ {detail?.ancestryIssue && (
118
+ <Text role="status" size="1" color="amber">
119
+ Projection unavailable: {detail.ancestryIssue}. Raw entry retained.
120
+ </Text>
121
+ )}
122
+ <Tabs.Root value={tab} onValueChange={changeTab} className="inspector-tabs">
123
+ <Tabs.List wrap="nowrap" aria-label="Event detail views">
124
+ {["content", "raw", "prompt", "tools", "context", "skills", "codemode"].map((tab) => (
125
+ <Tabs.Trigger key={tab} value={tab} disabled={!enabled.includes(tab)}>
126
+ {tab}
127
+ </Tabs.Trigger>
128
+ ))}
129
+ </Tabs.List>
130
+ <Tabs.Content value="content">
131
+ {call ? (
132
+ <>
133
+ <Status value={call.status} />
134
+ <Data label="Arguments · redacted display copy" data={call.args} scope={`${call.occurrenceId}-args`} />
135
+ <Data label="Result · observed tool event" data={call.result} scope={`${call.occurrenceId}-result`} />
136
+ <div className="inspector-metadata">
137
+ <Metadata
138
+ rows={[
139
+ ["Source", "Observed tool execution"],
140
+ [
141
+ "Parent relationship",
142
+ call.parentUnavailable ??
143
+ call.parentOccurrenceId ??
144
+ (call.parentId ? "unavailable" : "none (root)"),
145
+ ],
146
+ [
147
+ "Correlation",
148
+ call.correlationUnavailable || !call.branchAnchor
149
+ ? "unavailable"
150
+ : "Recorded anchor only; not request association",
151
+ ],
152
+ ["Reported duration", duration(call.durationMs)],
153
+ ["Occurrence", call.occurrenceId],
154
+ ["Raw call ID", call.id],
155
+ ["Anchor entry", call.branchAnchor ?? undefined],
156
+ [
157
+ "Start observed",
158
+ call.observedStartedAt === undefined ? undefined : new Date(call.observedStartedAt).toISOString(),
159
+ ],
160
+ [
161
+ "End observed",
162
+ call.observedEndedAt === undefined ? undefined : new Date(call.observedEndedAt).toISOString(),
163
+ ],
164
+ [
165
+ "Children",
166
+ String(
167
+ snapshot?.calls.filter((child) => child.parentOccurrenceId === call.occurrenceId).length ?? 0,
168
+ ),
169
+ ],
170
+ ]}
171
+ />
172
+ </div>
173
+ {call.parentId && !snapshot?.calls.some((parent) => parent.occurrenceId === call.parentOccurrenceId) && (
174
+ <Text size="1" color="gray">
175
+ Parent {call.parentId} (not captured)
176
+ </Text>
177
+ )}
178
+ {snapshot?.calls
179
+ .filter(
180
+ (item) =>
181
+ item.parentOccurrenceId === call.occurrenceId || item.occurrenceId === call.parentOccurrenceId,
182
+ )
183
+ .map((item) => (
184
+ <Button
185
+ key={item.occurrenceId}
186
+ size="1"
187
+ variant="ghost"
188
+ onClick={() => selectCall(item.occurrenceId)}
189
+ >
190
+ Related execution · {item.name} · {item.occurrenceId}
191
+ </Button>
192
+ ))}
193
+ {call.branchAnchor && snapshot?.nodes.some((entry) => entry.id === call.branchAnchor) && (
194
+ <Button variant="ghost" size="1" onClick={() => select(call.branchAnchor ?? "")}>
195
+ Inspect recorded anchor →
196
+ </Button>
197
+ )}
198
+ </>
199
+ ) : (
200
+ <>
201
+ <Text as="p" size="1" color="gray">
202
+ Session log entry · bounded recorded content
203
+ </Text>
204
+ {node?.status && <Status value={node.status} />}
205
+ <RecordedContent data={detail?.raw} scope={selected} />
206
+ </>
207
+ )}
208
+ {!detail && !call && !detailError && (
209
+ <Text size="1" color="gray">
210
+ Loading bounded entry details…
211
+ </Text>
212
+ )}
213
+ </Tabs.Content>
214
+ <Tabs.Content value="raw">
215
+ <div className="format-toolbar">
216
+ <div className="view-switch">
217
+ {["formatted", "json"].map((mode) => (
218
+ <button type="button" key={mode} aria-pressed={format === mode} onClick={() => setFormat(mode)}>
219
+ {mode === "formatted" ? "Overview" : "Raw JSON"}
220
+ </button>
221
+ ))}
222
+ </div>
223
+ </div>
224
+ {format === "formatted" && (
225
+ <div className="inspector-metadata">
226
+ <Metadata
227
+ rows={(call
228
+ ? [
229
+ ["Source", "Observed tool execution"],
230
+ ["Tool", call.name],
231
+ ["ID", call.id],
232
+ ["Parent call", call.parentId],
233
+ ["Reported duration", call.durationMs === undefined ? undefined : duration(call.durationMs)],
234
+ [
235
+ "Start observed",
236
+ call.observedStartedAt === undefined
237
+ ? undefined
238
+ : new Date(call.observedStartedAt).toISOString(),
239
+ ],
240
+ [
241
+ "End observed",
242
+ call.observedEndedAt === undefined ? undefined : new Date(call.observedEndedAt).toISOString(),
243
+ ],
244
+ ["Anchor entry", call.branchAnchor ?? undefined],
245
+ ]
246
+ : [
247
+ ["Source", "Session log entry"],
248
+ ["Type", node?.kind ?? (typeof raw?.type === "string" ? raw.type : undefined)],
249
+ [
250
+ node?.kind === "custom" || node?.kind === "custom_message"
251
+ ? "Custom type"
252
+ : node?.kind === "assistant"
253
+ ? "Model"
254
+ : "Name",
255
+ node?.name,
256
+ ],
257
+ ["ID", selected],
258
+ ["Parent ID", node?.parentId ?? "root"],
259
+ ["Timestamp", node?.timestamp],
260
+ [
261
+ "Indexed children",
262
+ count(snapshot?.nodes.filter((entry) => entry.parentId === selected).length),
263
+ ],
264
+ ["Tokens", node?.tokens === undefined ? undefined : count(node.tokens)],
265
+ [
266
+ "Reported tool duration",
267
+ recordedCall?.durationMs === undefined ? undefined : duration(recordedCall.durationMs),
268
+ ],
269
+ ]
270
+ ).filter((row): row is [string, string] => typeof row[1] === "string")}
271
+ />
272
+ {(call?.status ?? node?.status) && <Status value={call?.status ?? node?.status} />}
273
+ </div>
274
+ )}
275
+ {format === "formatted" && call ? (
276
+ <>
277
+ <Data label="Arguments · redacted display copy" data={call.args} scope={`${call.occurrenceId}-args`} />
278
+ <Data label="Result · observed tool event" data={call.result} scope={`${call.occurrenceId}-result`} />
279
+ {call.branchAnchor && (
280
+ <Button variant="ghost" size="1" onClick={() => select(call.branchAnchor ?? "")}>
281
+ Inspect recorded anchor →
282
+ </Button>
283
+ )}
284
+ </>
285
+ ) : (
286
+ <Data
287
+ label="Raw selected entry · redacted display copy"
288
+ data={
289
+ format === "formatted" && payload !== undefined
290
+ ? { value: payload, truncated: Boolean(detail?.raw.truncated) }
291
+ : activeRaw
292
+ }
293
+ scope={`${selected}-raw`}
294
+ />
295
+ )}
296
+ <Collapsible.Root className="related">
297
+ <Collapsible.Trigger asChild>
298
+ <Button variant="ghost" size="2">
299
+ Related {call ? "calls" : "entries"}
300
+ <ChevronRightIcon />
301
+ </Button>
302
+ </Collapsible.Trigger>
303
+ <Collapsible.Content>
304
+ {call
305
+ ? snapshot?.calls
306
+ .filter(
307
+ (item) =>
308
+ item.parentOccurrenceId === call.occurrenceId || item.occurrenceId === call.parentOccurrenceId,
309
+ )
310
+ .map((item) => (
311
+ <button type="button" key={item.occurrenceId} onClick={() => selectCall(item.occurrenceId)}>
312
+ {item.name} · {item.id}
313
+ </button>
314
+ ))
315
+ : related.map((entry) => (
316
+ <button type="button" key={entry.id} onClick={() => select(entry.id)}>
317
+ {eventName(entry.kind)} · {entry.id}
318
+ </button>
319
+ ))}
320
+ </Collapsible.Content>
321
+ </Collapsible.Root>
322
+ </Tabs.Content>
323
+ <Tabs.Content value="prompt">
324
+ <Data label="Historical prompt · browser preview branch" data={branch?.prompt} scope={`${selected}-prompt`} />
325
+ <Data
326
+ label="Previous-node prompt · compare with selected branch"
327
+ data={branch?.previousPrompt}
328
+ scope={`${selected}-previousPrompt`}
329
+ />
330
+ <Data
331
+ label="Prompt diff · removed / added lines"
332
+ data={promptDiff(branch?.previousPrompt, branch?.prompt)}
333
+ scope={`${selected}-prompt-diff`}
334
+ />
335
+ <Data label="Historical sections" data={branch?.sections} scope={`${selected}-sections`} />
336
+ <Data label="Prompt updates on branch" data={branch?.promptUpdates} scope={`${selected}-promptUpdates`} />
337
+ </Tabs.Content>
338
+ <Tabs.Content value="tools">
339
+ <Data
340
+ label="Historical declared tools · preview branch"
341
+ data={branch?.declaredTools}
342
+ scope={`${selected}-declaredTools`}
343
+ />
344
+ </Tabs.Content>
345
+ <Tabs.Content value="skills">
346
+ <Text as="p" size="2" color="gray">
347
+ Advertised or read does not prove model compliance. Historical discovery may be unavailable.
348
+ </Text>
349
+ <Data label="Evidence on preview branch" data={branch?.skillEvidence} scope={`${selected}-skillEvidence`} />
350
+ </Tabs.Content>
351
+ <Tabs.Content value="context">
352
+ <Text as="p" size="2" color="gray">
353
+ Pi projection applies compaction and context edits. Request-local hooks can still transform it; this is not
354
+ the final provider HTTP request.
355
+ </Text>
356
+ <Button size="1" onClick={preview} disabled={!branch}>
357
+ View branch context
358
+ </Button>
359
+ <Data label="Projected branch entries" data={branch?.projection} scope={`${selected}-projection`} />
360
+ <Data label="Selected entry contribution" data={detail?.projected} scope={`${selected}-projected`} />
361
+ </Tabs.Content>
362
+ <Tabs.Content value="codemode">
363
+ <Text as="p" size="2" color="gray">
364
+ Script source, output and historical nested metadata appear in the selected raw entry. Child results before
365
+ activation are not captured. Sandbox variables, discovery-helper results and model-helper responses are
366
+ unavailable unless the script printed them.
367
+ </Text>
368
+ <Data
369
+ label="JavaScript source"
370
+ data={
371
+ typeof code === "string"
372
+ ? { value: code, truncated: Boolean(call?.args.truncated || detail?.raw.truncated) }
373
+ : undefined
374
+ }
375
+ />
376
+ <Data label="Source / output / persisted nestedCalls" data={detail?.raw} />
377
+ {captured.map((call) => (
378
+ <Button key={call.occurrenceId} variant="ghost" onClick={() => selectCall(call.occurrenceId)}>
379
+ {call.name} · {call.occurrenceId} · inspect captured execution
380
+ </Button>
381
+ ))}
382
+ </Tabs.Content>
383
+ </Tabs.Root>
384
+ </section>
385
+ );
386
+ }
@@ -0,0 +1,74 @@
1
+ import { useState } from "react";
2
+ import type { Json } from "../model.js";
3
+
4
+ const scopes = new Map<string, Map<string, boolean>>();
5
+ function JsonNode({
6
+ value,
7
+ name,
8
+ path,
9
+ state,
10
+ root = false,
11
+ }: {
12
+ value: Json;
13
+ name: string;
14
+ path: string;
15
+ state: Map<string, boolean>;
16
+ root?: boolean;
17
+ }) {
18
+ const [open, setOpen] = useState(() => state.get(path) ?? root);
19
+ const structured = value !== null && typeof value === "object";
20
+ if (!structured)
21
+ return (
22
+ <div className="json-leaf">
23
+ <span className="json-key">{name}</span>:{" "}
24
+ <span className={typeof value === "string" ? "json-string" : "json-number"}>{JSON.stringify(value)}</span>
25
+ </div>
26
+ );
27
+ const entries = Object.entries(value);
28
+ return (
29
+ <details
30
+ className="json-object"
31
+ open={open}
32
+ onToggle={(event) => {
33
+ state.set(path, event.currentTarget.open);
34
+ setOpen(event.currentTarget.open);
35
+ }}
36
+ >
37
+ <summary>
38
+ <span className="json-key">{name}</span>{" "}
39
+ <span className="json-summary">
40
+ {Array.isArray(value) ? `[${entries.length} items]` : `{${entries.length} fields}`}
41
+ </span>
42
+ </summary>
43
+ {
44
+ <div className="json-children">
45
+ {entries.map(([key, child]) => (
46
+ <JsonNode
47
+ key={key}
48
+ value={child}
49
+ name={key}
50
+ state={state}
51
+ path={`${path}/${key.replaceAll("~", "~0").replaceAll("/", "~1")}`}
52
+ />
53
+ ))}
54
+ </div>
55
+ }
56
+ </details>
57
+ );
58
+ }
59
+ export function JsonTree({ value, scope }: { value: Json; scope: string }) {
60
+ let state = scopes.get(scope);
61
+ if (!state) {
62
+ state = new Map();
63
+ scopes.set(scope, state);
64
+ if (scopes.size > 128) {
65
+ const first = scopes.keys().next().value;
66
+ if (first !== undefined) scopes.delete(first);
67
+ }
68
+ }
69
+ return (
70
+ <fieldset className="json-tree" aria-label="Structured display data">
71
+ <JsonNode key={scope} value={value} name="$" path="$" state={state} root />
72
+ </fieldset>
73
+ );
74
+ }
@@ -0,0 +1,67 @@
1
+ import type { Snapshot } from "../model.js";
2
+ import { Data } from "./components.js";
3
+ import { count, duration } from "./format.js";
4
+ import { timestamp } from "./timing.js";
5
+
6
+ export function Overview({ snapshot }: { snapshot?: Snapshot }) {
7
+ const nodes = snapshot?.nodes ?? [];
8
+ const stamps = nodes.map((node) => timestamp(node.timestamp)).filter((value): value is number => value !== undefined);
9
+ const tokens = nodes.filter((node) => node.tokens !== undefined);
10
+ const partial = (snapshot?.totalEntries ?? 0) > nodes.length;
11
+ const metrics = [
12
+ ["Entries", count(snapshot?.totalEntries), "All persisted entries, across branches."],
13
+ [
14
+ "Recorded span",
15
+ duration(stamps.length ? Math.max(...stamps) - Math.min(...stamps) : undefined),
16
+ "Indexed log timestamp range, not execution duration.",
17
+ ],
18
+ [
19
+ "Model messages",
20
+ snapshot ? `${nodes.filter((node) => node.kind === "assistant").length}${partial ? "+" : ""}` : "—",
21
+ "Assistant messages, not provider request count.",
22
+ ],
23
+ ["Captured calls", count(snapshot?.calls.length), "Bounded live records since consent."],
24
+ [
25
+ "Tokens",
26
+ tokens.length ? `${count(tokens.reduce((sum, node) => sum + (node.tokens ?? 0), 0))}${partial ? "+" : ""}` : "—",
27
+ "Persisted assistant totalTokens; excludes auxiliary usage.",
28
+ ],
29
+ [
30
+ "Log errors",
31
+ snapshot ? `${nodes.filter((node) => node.status === "error").length}${partial ? "+" : ""}` : "—",
32
+ "Indexed recorded error entries; live errors are separate.",
33
+ ],
34
+ ];
35
+ return (
36
+ <section className="overview" aria-label="Session overview">
37
+ <div className="overview-metrics">
38
+ {metrics.map(([name, value, hint]) => (
39
+ <div className="metric" key={name} title={hint}>
40
+ <span>{name}</span>
41
+ <strong>{value}</strong>
42
+ </div>
43
+ ))}
44
+ </div>
45
+ {Boolean(snapshot?.invalidEntryCount) && (
46
+ <details className="invalid-entries">
47
+ <summary>
48
+ {snapshot?.invalidEntryCount} invalid or ambiguous entries omitted from navigation; bounded raw evidence
49
+ </summary>
50
+ <Data
51
+ label="Invalid or ambiguous entry evidence · up to 20 indexed records"
52
+ data={{
53
+ value: (snapshot?.invalidEntries ?? []).map((item) => ({
54
+ index: item.index,
55
+ reason: item.reason,
56
+ raw: item.raw.value,
57
+ truncated: item.raw.truncated,
58
+ })),
59
+ truncated: (snapshot?.invalidEntryCount ?? 0) > 20,
60
+ }}
61
+ scope="invalid-identities"
62
+ />
63
+ </details>
64
+ )}
65
+ </section>
66
+ );
67
+ }