@narumitw/pi-inspect 0.0.0-stage → 0.1.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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +107 -2
  3. package/dist/app.css +1 -0
  4. package/dist/app.js +129 -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 +63 -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/extension.ts +259 -0
  16. package/src/feed.ts +145 -0
  17. package/src/identity.ts +112 -0
  18. package/src/index.ts +1 -0
  19. package/src/model.ts +113 -0
  20. package/src/native-projection.ts +28 -0
  21. package/src/privacy.ts +136 -0
  22. package/src/projection.ts +361 -0
  23. package/src/server.ts +202 -0
  24. package/src/system-message.ts +117 -0
  25. package/src/web/api.ts +137 -0
  26. package/src/web/app.tsx +439 -0
  27. package/src/web/components.css +261 -0
  28. package/src/web/components.tsx +289 -0
  29. package/src/web/composition.css +554 -0
  30. package/src/web/context-composition.tsx +324 -0
  31. package/src/web/entry-calls.ts +14 -0
  32. package/src/web/format.ts +92 -0
  33. package/src/web/hierarchy.ts +117 -0
  34. package/src/web/inline-entry.tsx +124 -0
  35. package/src/web/inspector.tsx +337 -0
  36. package/src/web/json-tree.tsx +74 -0
  37. package/src/web/live-drawer.tsx +175 -0
  38. package/src/web/overview.tsx +67 -0
  39. package/src/web/panels.css +267 -0
  40. package/src/web/panes.tsx +123 -0
  41. package/src/web/search.ts +16 -0
  42. package/src/web/style.css +332 -0
  43. package/src/web/styles.d.ts +1 -0
  44. package/src/web/timeline.tsx +33 -0
  45. package/src/web/timing.ts +26 -0
  46. package/src/web/trace.css +410 -0
  47. package/src/web/trace.tsx +273 -0
  48. package/src/web/tree.tsx +281 -0
  49. package/src/web/virtual-context.ts +138 -0
@@ -0,0 +1,337 @@
1
+ import * as Collapsible from "@radix-ui/react-collapsible";
2
+ import { ChevronRightIcon, CubeIcon } from "@radix-ui/react-icons";
3
+ import { Badge, Button, Card, Flex, 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 { CallView, 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
+
10
+ export function Inspector({
11
+ selected,
12
+ node,
13
+ detail,
14
+ detailError,
15
+ retryDetails,
16
+ call,
17
+ branch,
18
+ branchError,
19
+ retryBranch,
20
+ snapshot,
21
+ tab,
22
+ changeTab,
23
+ select,
24
+ selectCall,
25
+ }: {
26
+ selected: string;
27
+ node?: EntrySummary;
28
+ detail?: DetailView;
29
+ detailError: string;
30
+ retryDetails(): void;
31
+ call?: Call;
32
+ branch?: BranchView;
33
+ branchError: string;
34
+ retryBranch(): void;
35
+ snapshot?: Snapshot;
36
+ tab: string;
37
+ changeTab(tab: string): void;
38
+ select(id: string): void;
39
+ selectCall(id: string): void;
40
+ }) {
41
+ const [format, setFormat] = useState("formatted");
42
+ const raw = record(detail?.raw.value);
43
+ const message = record(raw?.message);
44
+ const payload = raw?.data ?? message?.content;
45
+ const related = snapshot?.nodes.filter((entry) => entry.parentId === selected || entry.id === node?.parentId) ?? [];
46
+ const recordedCall = snapshot?.calls.find(
47
+ (item) => item.id === node?.toolCallId && item.branchAnchor === detail?.toolAnchor && !item.parentOccurrenceId,
48
+ );
49
+ const liveRaw: Capture | undefined = call
50
+ ? {
51
+ value: JSON.parse(JSON.stringify(call)) as Json,
52
+ truncated: Boolean(call.args.truncated || call.result?.truncated),
53
+ }
54
+ : undefined;
55
+ const activeRaw = liveRaw ?? detail?.raw;
56
+ const captured = entryCalls(detail?.raw, snapshot?.calls ?? [], detail?.toolAnchor);
57
+ const code = call ? (record(call.args.value)?.code as Json | undefined) : scripts(detail?.raw)?.value;
58
+ const codemode = call?.name === "codemode" || Boolean(scripts(detail?.raw)) || message?.toolName === "codemode";
59
+ const isCall = Boolean(call);
60
+ const hasSkills = Boolean(
61
+ snapshot?.skills.length || (Array.isArray(branch?.skillEvidence.value) && branch.skillEvidence.value.length),
62
+ );
63
+ const enabled = useMemo(
64
+ () => [
65
+ "raw",
66
+ "prompt",
67
+ "tools",
68
+ ...(isCall ? [] : ["context"]),
69
+ ...(hasSkills ? ["skills"] : []),
70
+ ...(codemode ? ["codemode"] : []),
71
+ ],
72
+ [isCall, hasSkills, codemode],
73
+ );
74
+ useEffect(() => {
75
+ if (!enabled.includes(tab)) changeTab("raw");
76
+ }, [enabled, tab, changeTab]);
77
+ return (
78
+ <section className="panel inspector-panel">
79
+ <div className="panel-heading">
80
+ <Heading size="3">
81
+ <CubeIcon />
82
+ Event inspector
83
+ </Heading>
84
+ {activeRaw && <Copy key={selected} value={output(activeRaw.value)} />}
85
+ </div>
86
+ <div className="inspector-identity">
87
+ {call?.branchAnchor && !snapshot?.nodes.some((entry) => entry.id === call.branchAnchor) && (
88
+ <Text size="1" color="gray">
89
+ Recorded anchor omitted from this bounded inventory; navigator target unavailable.
90
+ </Text>
91
+ )}
92
+ <Heading size="4">
93
+ {call ? "Captured execution" : node ? eventName(node.kind) : "Entry"} · {selected || "no selection"}
94
+ </Heading>
95
+ <Text size="2" color="gray">
96
+ {call?.name ?? node?.name ?? "Session log entry"}
97
+ </Text>
98
+ </div>
99
+ {detailError && (
100
+ <div role="alert" className="detail-failure">
101
+ {detailError}
102
+ <Button size="1" variant="ghost" onClick={retryDetails}>
103
+ Retry selected details
104
+ </Button>
105
+ </div>
106
+ )}
107
+ {branchError && (
108
+ <div role="alert" className="branch-failure">
109
+ {branchError}
110
+ <Button size="1" variant="ghost" onClick={retryBranch}>
111
+ Retry selected branch
112
+ </Button>
113
+ </div>
114
+ )}
115
+ {detail?.ancestryIssue && (
116
+ <Text role="status" size="1" color="amber">
117
+ Projection unavailable: {detail.ancestryIssue}. Raw entry retained.
118
+ </Text>
119
+ )}
120
+ <Tabs.Root value={tab} onValueChange={changeTab} className="inspector-tabs">
121
+ <Tabs.List wrap="nowrap" aria-label="Event detail views">
122
+ {["raw", "prompt", "tools", "context", "skills", "codemode"].map((tab) => (
123
+ <Tabs.Trigger key={tab} value={tab} disabled={!enabled.includes(tab)}>
124
+ {tab}
125
+ </Tabs.Trigger>
126
+ ))}
127
+ </Tabs.List>
128
+ <Tabs.Content value="raw">
129
+ <div className="format-toolbar">
130
+ <div className="view-switch">
131
+ {["formatted", "json"].map((mode) => (
132
+ <button type="button" key={mode} aria-pressed={format === mode} onClick={() => setFormat(mode)}>
133
+ {mode === "formatted" ? "Overview" : "Raw JSON"}
134
+ </button>
135
+ ))}
136
+ </div>
137
+ </div>
138
+ {format === "formatted" && (
139
+ <div className="inspector-metadata">
140
+ <Metadata
141
+ rows={(call
142
+ ? [
143
+ ["Source", "Observed tool execution"],
144
+ ["Tool", call.name],
145
+ ["ID", call.id],
146
+ ["Parent call", call.parentId],
147
+ ["Reported duration", call.durationMs === undefined ? undefined : duration(call.durationMs)],
148
+ [
149
+ "Start observed",
150
+ call.observedStartedAt === undefined
151
+ ? undefined
152
+ : new Date(call.observedStartedAt).toISOString(),
153
+ ],
154
+ [
155
+ "End observed",
156
+ call.observedEndedAt === undefined ? undefined : new Date(call.observedEndedAt).toISOString(),
157
+ ],
158
+ ["Anchor entry", call.branchAnchor ?? undefined],
159
+ ]
160
+ : [
161
+ ["Source", "Session log entry"],
162
+ ["Type", node?.kind ?? (typeof raw?.type === "string" ? raw.type : undefined)],
163
+ [
164
+ node?.kind === "custom" || node?.kind === "custom_message"
165
+ ? "Custom type"
166
+ : node?.kind === "assistant"
167
+ ? "Model"
168
+ : "Name",
169
+ node?.name,
170
+ ],
171
+ ["ID", selected],
172
+ ["Parent ID", node?.parentId ?? "root"],
173
+ ["Timestamp", node?.timestamp],
174
+ [
175
+ "Indexed children",
176
+ count(snapshot?.nodes.filter((entry) => entry.parentId === selected).length),
177
+ ],
178
+ ["Tokens", node?.tokens === undefined ? undefined : count(node.tokens)],
179
+ [
180
+ "Reported tool duration",
181
+ recordedCall?.durationMs === undefined ? undefined : duration(recordedCall.durationMs),
182
+ ],
183
+ ]
184
+ ).filter((row): row is [string, string] => typeof row[1] === "string")}
185
+ />
186
+ {(call?.status ?? node?.status) && <Status value={call?.status ?? node?.status} />}
187
+ </div>
188
+ )}
189
+ {format === "formatted" && call ? (
190
+ <>
191
+ <Data label="Arguments · redacted display copy" data={call.args} scope={`${call.occurrenceId}-args`} />
192
+ <Data label="Result · observed tool event" data={call.result} scope={`${call.occurrenceId}-result`} />
193
+ {call.branchAnchor && (
194
+ <Button variant="ghost" size="1" onClick={() => select(call.branchAnchor ?? "")}>
195
+ Inspect recorded anchor →
196
+ </Button>
197
+ )}
198
+ </>
199
+ ) : (
200
+ <Data
201
+ label="Raw selected entry · redacted display copy"
202
+ data={
203
+ format === "formatted" && payload !== undefined
204
+ ? { value: payload, truncated: Boolean(detail?.raw.truncated) }
205
+ : activeRaw
206
+ }
207
+ scope={`${selected}-raw`}
208
+ />
209
+ )}
210
+ <Collapsible.Root className="related">
211
+ <Collapsible.Trigger asChild>
212
+ <Button variant="ghost" size="2">
213
+ Related {call ? "calls" : "entries"}
214
+ <ChevronRightIcon />
215
+ </Button>
216
+ </Collapsible.Trigger>
217
+ <Collapsible.Content>
218
+ {call
219
+ ? snapshot?.calls
220
+ .filter(
221
+ (item) =>
222
+ item.parentOccurrenceId === call.occurrenceId || item.occurrenceId === call.parentOccurrenceId,
223
+ )
224
+ .map((item) => (
225
+ <button type="button" key={item.occurrenceId} onClick={() => selectCall(item.occurrenceId)}>
226
+ {item.name} · {item.id}
227
+ </button>
228
+ ))
229
+ : related.map((entry) => (
230
+ <button type="button" key={entry.id} onClick={() => select(entry.id)}>
231
+ {eventName(entry.kind)} · {entry.id}
232
+ </button>
233
+ ))}
234
+ </Collapsible.Content>
235
+ </Collapsible.Root>
236
+ </Tabs.Content>
237
+ <Tabs.Content value="prompt">
238
+ <Data label="Historical prompt · browser preview branch" data={branch?.prompt} scope={`${selected}-prompt`} />
239
+ <Data
240
+ label="Previous-node prompt · compare with selected branch"
241
+ data={branch?.previousPrompt}
242
+ scope={`${selected}-previousPrompt`}
243
+ />
244
+ <Data
245
+ label="Prompt diff · removed / added lines"
246
+ data={promptDiff(branch?.previousPrompt, branch?.prompt)}
247
+ scope={`${selected}-prompt-diff`}
248
+ />
249
+ <Data label="Historical sections" data={branch?.sections} scope={`${selected}-sections`} />
250
+ <Data label="Prompt updates on branch" data={branch?.promptUpdates} scope={`${selected}-promptUpdates`} />
251
+ <Data label="Current runtime effective prompt · may not yet be sent" data={snapshot?.currentPrompt} />
252
+ </Tabs.Content>
253
+ <Tabs.Content value="tools">
254
+ <Data
255
+ label="Historical declared tools · preview branch"
256
+ data={branch?.declaredTools}
257
+ scope={`${selected}-declaredTools`}
258
+ />
259
+ <Text as="p" size="2" color="gray">
260
+ Current active ≠ provider-visible. MCP connection status is unavailable.
261
+ </Text>
262
+ {snapshot?.tools.map((tool) => (
263
+ <Card key={tool.name} className="inventory-card">
264
+ <Text weight="bold">{tool.name}</Text>
265
+ <Flex gap="1" wrap="wrap">
266
+ <Badge>{tool.namespace ?? "tool"}</Badge>
267
+ <Badge>{tool.exposure}</Badge>
268
+ <Badge>{tool.active ? "active" : "inactive"}</Badge>
269
+ <Badge>{tool.callable ? "callable" : "not callable"}</Badge>
270
+ </Flex>
271
+ <Text as="p" size="2" color="gray">
272
+ {tool.description}
273
+ </Text>
274
+ <Collapsible.Root>
275
+ <Collapsible.Trigger asChild>
276
+ <Button size="1" variant="ghost">
277
+ Schema · {tool.name}
278
+ </Button>
279
+ </Collapsible.Trigger>
280
+ <Collapsible.Content>
281
+ <Data label="Schema" data={tool.schema} />
282
+ </Collapsible.Content>
283
+ </Collapsible.Root>
284
+ </Card>
285
+ ))}
286
+ </Tabs.Content>
287
+ <Tabs.Content value="skills">
288
+ <Text as="p" size="2" color="gray">
289
+ Advertised or read does not prove model compliance. Historical discovery may be unavailable.
290
+ </Text>
291
+ {snapshot?.skills.map((skill) => (
292
+ <Card key={skill.path} className="inventory-card">
293
+ <Flex gap="2">
294
+ <Text weight="bold">{skill.name}</Text>
295
+ <Badge>advertised</Badge>
296
+ </Flex>
297
+ <Text as="p" size="2">
298
+ {skill.description}
299
+ </Text>
300
+ <Text size="1" color="gray">
301
+ {skill.path}
302
+ </Text>
303
+ </Card>
304
+ ))}
305
+ <Data label="Evidence on preview branch" data={branch?.skillEvidence} scope={`${selected}-skillEvidence`} />
306
+ </Tabs.Content>
307
+ <Tabs.Content value="context">
308
+ <Text as="p" size="2" color="gray">
309
+ Pi projection applies compaction and context edits. Request-local hooks can still transform it; this is not
310
+ the final provider HTTP request.
311
+ </Text>
312
+ <Data label="Projected branch entries" data={branch?.projection} scope={`${selected}-projection`} />
313
+ <Data label="Selected entry contribution" data={detail?.projected} scope={`${selected}-projected`} />
314
+ </Tabs.Content>
315
+ <Tabs.Content value="codemode">
316
+ <Text as="p" size="2" color="gray">
317
+ Script source, output and historical nested metadata appear in the selected raw entry. Child results before
318
+ activation are not captured. Sandbox variables, discovery-helper results and model-helper responses are
319
+ unavailable unless the script printed them.
320
+ </Text>
321
+ <Data
322
+ label="JavaScript source"
323
+ data={
324
+ typeof code === "string"
325
+ ? { value: code, truncated: Boolean(call?.args.truncated || detail?.raw.truncated) }
326
+ : undefined
327
+ }
328
+ />
329
+ <Data label="Source / output / persisted nestedCalls" data={detail?.raw} />
330
+ {captured.map((call) => (
331
+ <CallView key={call.occurrenceId} call={call} all={captured} group="detail" />
332
+ ))}
333
+ </Tabs.Content>
334
+ </Tabs.Root>
335
+ </section>
336
+ );
337
+ }
@@ -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,175 @@
1
+ import { ChevronDownIcon, ChevronRightIcon } from "@radix-ui/react-icons";
2
+ import { Button, Text } from "@radix-ui/themes";
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+ import type { Call, EntrySummary } from "../model.js";
5
+ import { CallView, Status } from "./components.js";
6
+ import { duration, type Filters, matchesCall } from "./format.js";
7
+ import { flatten, hierarchy, reveal, withAncestors } from "./hierarchy.js";
8
+ import { searchNeedle } from "./search.js";
9
+ import { TimelineMark } from "./timeline.js";
10
+ import { axis } from "./timing.js";
11
+
12
+ export function LiveDrawer({
13
+ calls,
14
+ entries,
15
+ selected,
16
+ select,
17
+ open,
18
+ changeOpen,
19
+ view,
20
+ dropped,
21
+ invalidEvents,
22
+ filters,
23
+ }: {
24
+ calls: Call[];
25
+ entries: EntrySummary[];
26
+ selected?: string;
27
+ select(id: string): void;
28
+ open: boolean;
29
+ changeOpen(open: boolean): void;
30
+ view: string;
31
+ dropped: number;
32
+ invalidEvents: number;
33
+ filters: Filters;
34
+ }) {
35
+ const preparedFilters = useMemo(() => ({ ...filters, query: searchNeedle(filters.query) }), [filters]);
36
+ const tree = useMemo(
37
+ () =>
38
+ hierarchy(
39
+ calls.map((call) => ({
40
+ ...call,
41
+ id: call.occurrenceId,
42
+ rawId: call.id,
43
+ rawParentId: call.parentId,
44
+ parentId: call.parentOccurrenceId ?? null,
45
+ })),
46
+ ),
47
+ [calls],
48
+ );
49
+ const keep = useMemo(
50
+ () =>
51
+ withAncestors(
52
+ tree,
53
+ new Set(calls.filter((call) => matchesCall(call, preparedFilters)).map((call) => call.occurrenceId)),
54
+ ),
55
+ [tree, calls, preparedFilters],
56
+ );
57
+ const [expanded, setExpanded] = useState(new Set<string>());
58
+ const previous = useRef<string | undefined>(undefined);
59
+ useEffect(() => {
60
+ const chain: string[] = [];
61
+ let current = selected;
62
+ const visited = new Set<string>();
63
+ while (current && tree.nodes.has(current) && !visited.has(current)) {
64
+ visited.add(current);
65
+ chain.push(current);
66
+ current = tree.nodes.get(current)?.parentId ?? undefined;
67
+ }
68
+ const identity = chain.join("/");
69
+ if (selected && identity !== previous.current && tree.nodes.has(selected)) {
70
+ previous.current = identity;
71
+ setExpanded((old) => reveal(tree, old, selected));
72
+ }
73
+ }, [selected, tree]);
74
+ const rows = flatten(tree, expanded, keep);
75
+ const range = axis(entries, calls);
76
+ return (
77
+ <section className={`live-drawer ${open ? "drawer-open" : ""}`}>
78
+ <button type="button" className="live-drawer-toggle" aria-expanded={open} onClick={() => changeOpen(!open)}>
79
+ {open ? <ChevronDownIcon /> : <ChevronRightIcon />}
80
+ <strong>Live calls</strong>
81
+ <span>
82
+ {calls.length} captured · {calls.filter((call) => call.status === "error").length} errors · {dropped} evicted
83
+ </span>
84
+ <span className="drawer-scope">Session-wide · observed events</span>
85
+ </button>
86
+ {open && (
87
+ <div className="live-drawer-body">
88
+ {invalidEvents > 0 && (
89
+ <Text size="1" color="gray">
90
+ {invalidEvents} invalid live events omitted; correlation unavailable
91
+ </Text>
92
+ )}
93
+ <div className="live-controls">
94
+ <Text size="1" color="gray">
95
+ Reported durations are monotonic execute() timings; bars are observed callback intervals.
96
+ </Text>
97
+ <Button size="1" variant="ghost" disabled={!keep.size} onClick={() => setExpanded(new Set(keep))}>
98
+ Expand calls
99
+ </Button>
100
+ <Button size="1" variant="ghost" onClick={() => setExpanded(new Set())}>
101
+ Collapse calls
102
+ </Button>
103
+ </div>
104
+ {!rows.length && <div className="empty-state">No captured executions match these filters.</div>}
105
+ {rows.map(({ node: call, depth }) => (
106
+ <div
107
+ key={call.id}
108
+ id={`live-call-${call.occurrenceId}`}
109
+ className="live-trace-item"
110
+ data-parent-id={call.parentId ?? ""}
111
+ data-raw-id={call.rawId}
112
+ style={{ marginLeft: depth * 12 }}
113
+ >
114
+ <div className={`live-call-row ${selected === call.id ? "selected" : ""}`}>
115
+ <button
116
+ type="button"
117
+ className="expand-button"
118
+ aria-label={`Expand call ${call.id}`}
119
+ aria-expanded={expanded.has(call.id)}
120
+ onClick={() =>
121
+ setExpanded((old) => {
122
+ const next = new Set(old);
123
+ if (next.has(call.id)) next.delete(call.id);
124
+ else next.add(call.id);
125
+ return next;
126
+ })
127
+ }
128
+ >
129
+ {expanded.has(call.id) ? <ChevronDownIcon /> : <ChevronRightIcon />}
130
+ </button>
131
+ <button
132
+ type="button"
133
+ className="call-trigger"
134
+ aria-label={`${call.name} · ${call.status}`}
135
+ onClick={() => select(call.id)}
136
+ >
137
+ <strong title={call.name}>{call.name}</strong>
138
+ <code title={call.occurrenceId}>{call.rawId}</code>
139
+ <Status value={call.status} />
140
+ <span>{duration(call.durationMs)}</span>
141
+ </button>
142
+ {view === "timeline" && (
143
+ <TimelineMark
144
+ range={range}
145
+ call={{
146
+ ...call,
147
+ id: call.rawId,
148
+ parentId: call.parentOccurrenceId
149
+ ? (calls.find((parent) => parent.occurrenceId === call.parentOccurrenceId)?.id ??
150
+ call.rawParentId)
151
+ : call.rawParentId,
152
+ }}
153
+ />
154
+ )}
155
+ </div>
156
+ {expanded.has(call.id) && (
157
+ <CallView
158
+ call={{
159
+ ...call,
160
+ id: call.rawId,
161
+ parentId: call.parentOccurrenceId
162
+ ? (calls.find((parent) => parent.occurrenceId === call.parentOccurrenceId)?.id ??
163
+ call.rawParentId)
164
+ : call.rawParentId,
165
+ }}
166
+ all={calls}
167
+ />
168
+ )}
169
+ </div>
170
+ ))}
171
+ </div>
172
+ )}
173
+ </section>
174
+ );
175
+ }
@@ -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
+ }