@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.
- package/LICENSE +21 -0
- package/README.md +110 -2
- package/dist/app.css +1 -0
- package/dist/app.js +128 -0
- package/dist/index.html +1 -0
- package/docs/REVIEW.md +189 -0
- package/docs/VERIFICATION.md +186 -0
- package/docs/viewer.md +74 -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/entry-summary.ts +34 -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 +117 -0
- package/src/native-projection.ts +28 -0
- package/src/privacy.ts +136 -0
- package/src/projection.ts +374 -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 +570 -0
- package/src/web/captured-executions.tsx +177 -0
- package/src/web/components.css +261 -0
- package/src/web/components.tsx +241 -0
- package/src/web/composition.css +554 -0
- package/src/web/context-composition.tsx +329 -0
- package/src/web/context-inventory.tsx +65 -0
- package/src/web/entry-calls.ts +14 -0
- package/src/web/format.ts +94 -0
- package/src/web/hierarchy.ts +117 -0
- package/src/web/history.ts +56 -0
- package/src/web/inline-entry.tsx +124 -0
- package/src/web/inspector.tsx +386 -0
- package/src/web/json-tree.tsx +74 -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/recorded-content.tsx +49 -0
- package/src/web/search.ts +16 -0
- package/src/web/session-filters.tsx +100 -0
- package/src/web/session.css +114 -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 +411 -0
- package/src/web/trace.tsx +313 -0
- 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
|
+
}
|