@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.
- package/LICENSE +21 -0
- package/README.md +107 -2
- package/dist/app.css +1 -0
- package/dist/app.js +129 -0
- package/dist/index.html +1 -0
- package/docs/REVIEW.md +189 -0
- package/docs/VERIFICATION.md +186 -0
- package/docs/viewer.md +63 -0
- package/package.json +67 -4
- package/src/ancestry.ts +32 -0
- package/src/collector.ts +143 -0
- package/src/context.ts +115 -0
- package/src/correlation.ts +27 -0
- package/src/entry-index.ts +17 -0
- package/src/extension.ts +259 -0
- package/src/feed.ts +145 -0
- package/src/identity.ts +112 -0
- package/src/index.ts +1 -0
- package/src/model.ts +113 -0
- package/src/native-projection.ts +28 -0
- package/src/privacy.ts +136 -0
- package/src/projection.ts +361 -0
- package/src/server.ts +202 -0
- package/src/system-message.ts +117 -0
- package/src/web/api.ts +137 -0
- package/src/web/app.tsx +439 -0
- package/src/web/components.css +261 -0
- package/src/web/components.tsx +289 -0
- package/src/web/composition.css +554 -0
- package/src/web/context-composition.tsx +324 -0
- package/src/web/entry-calls.ts +14 -0
- package/src/web/format.ts +92 -0
- package/src/web/hierarchy.ts +117 -0
- package/src/web/inline-entry.tsx +124 -0
- package/src/web/inspector.tsx +337 -0
- package/src/web/json-tree.tsx +74 -0
- package/src/web/live-drawer.tsx +175 -0
- package/src/web/overview.tsx +67 -0
- package/src/web/panels.css +267 -0
- package/src/web/panes.tsx +123 -0
- package/src/web/search.ts +16 -0
- package/src/web/style.css +332 -0
- package/src/web/styles.d.ts +1 -0
- package/src/web/timeline.tsx +33 -0
- package/src/web/timing.ts +26 -0
- package/src/web/trace.css +410 -0
- package/src/web/trace.tsx +273 -0
- package/src/web/tree.tsx +281 -0
- 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
|
+
}
|