@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,261 @@
1
+ .event-glyph {
2
+ width: 20px;
3
+ height: 20px;
4
+ flex-shrink: 0;
5
+ display: inline-grid;
6
+ place-items: center;
7
+ color: var(--secondary);
8
+ }
9
+ .event-glyph svg {
10
+ width: 15px;
11
+ height: 15px;
12
+ }
13
+ .glyph-model {
14
+ color: #b2a4dd;
15
+ }
16
+ .glyph-tool {
17
+ color: #72b4c2;
18
+ }
19
+ .glyph-custom {
20
+ color: #af9ecb;
21
+ }
22
+ .glyph-thinking {
23
+ color: #78bdab;
24
+ }
25
+ .status {
26
+ display: inline-flex;
27
+ align-items: center;
28
+ gap: 4px;
29
+ font-size: 10px;
30
+ line-height: 15px;
31
+ color: var(--muted);
32
+ white-space: nowrap;
33
+ }
34
+ .status svg {
35
+ width: 11px;
36
+ height: 11px;
37
+ }
38
+ .status-success,
39
+ .status-ok {
40
+ color: #32c48d;
41
+ }
42
+ .status-error,
43
+ .status-cancelled {
44
+ color: #ed7373;
45
+ }
46
+ .status-running {
47
+ color: #e5b45c;
48
+ }
49
+ .status-dot {
50
+ width: 5px;
51
+ height: 5px;
52
+ border-radius: 50%;
53
+ display: inline-block;
54
+ background: currentColor;
55
+ }
56
+ .view-switch {
57
+ display: inline-flex;
58
+ gap: 1px;
59
+ padding: 2px;
60
+ border-radius: 4px;
61
+ background: var(--code-bg);
62
+ }
63
+ .view-switch button {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 5px;
67
+ padding: 4px 8px;
68
+ border: 0;
69
+ border-radius: 3px;
70
+ color: var(--secondary);
71
+ background: transparent;
72
+ font-size: 11px;
73
+ }
74
+ .view-switch button[aria-pressed="true"] {
75
+ background: var(--selected-bg);
76
+ color: var(--blue-11);
77
+ }
78
+ .metadata {
79
+ margin: 0;
80
+ font-size: 11px;
81
+ line-height: 17px;
82
+ }
83
+ .metadata > div {
84
+ display: grid;
85
+ grid-template-columns: minmax(85px, 36%) minmax(0, 1fr);
86
+ gap: 10px;
87
+ padding: 4px 0;
88
+ }
89
+ .metadata dt {
90
+ color: var(--secondary);
91
+ }
92
+ .metadata dd {
93
+ margin: 0;
94
+ min-width: 0;
95
+ font-variant-numeric: tabular-nums;
96
+ overflow-wrap: anywhere;
97
+ }
98
+ .call {
99
+ padding: 8px 10px;
100
+ background: var(--code-bg);
101
+ border-left: 1px solid var(--panel-border);
102
+ }
103
+ .call .rt-Text {
104
+ font-size: 12px;
105
+ }
106
+ .parent-link {
107
+ display: flex;
108
+ align-items: center;
109
+ gap: 5px;
110
+ color: var(--blue-11);
111
+ font-size: 11px;
112
+ margin: 7px 0;
113
+ }
114
+ .data {
115
+ border-top: 1px solid var(--panel-border);
116
+ margin-top: 7px;
117
+ min-width: 0;
118
+ }
119
+ .data-toolbar {
120
+ display: flex;
121
+ justify-content: space-between;
122
+ align-items: center;
123
+ gap: 6px;
124
+ padding: 5px 0;
125
+ min-width: 0;
126
+ }
127
+ .section-toggle {
128
+ display: flex;
129
+ flex: 1;
130
+ min-width: 0;
131
+ align-items: center;
132
+ gap: 5px;
133
+ background: none;
134
+ border: 0;
135
+ color: var(--secondary);
136
+ text-align: left;
137
+ font-size: 11px;
138
+ line-height: 16px;
139
+ padding: 2px 0;
140
+ }
141
+ .section-toggle svg {
142
+ width: 12px;
143
+ height: 12px;
144
+ flex-shrink: 0;
145
+ }
146
+ .chevron-open {
147
+ transform: rotate(90deg);
148
+ }
149
+ .copy-button {
150
+ color: var(--muted);
151
+ font-size: 10px;
152
+ }
153
+ .data-mode {
154
+ display: flex;
155
+ align-items: center;
156
+ gap: 3px;
157
+ font-size: 10px;
158
+ margin-bottom: 4px;
159
+ color: var(--muted);
160
+ }
161
+ .data-mode button {
162
+ border: 0;
163
+ background: none;
164
+ color: var(--secondary);
165
+ padding: 3px 5px;
166
+ font-size: 10px;
167
+ }
168
+ .data-mode button[aria-pressed="true"] {
169
+ color: var(--blue-11);
170
+ background: var(--selected-bg);
171
+ border-radius: 3px;
172
+ }
173
+ .data-mode > span {
174
+ margin-left: auto;
175
+ }
176
+ .code-preview {
177
+ font:
178
+ 11px / 1.6 "Cascadia Code",
179
+ Consolas,
180
+ monospace;
181
+ white-space: pre;
182
+ overflow-x: auto;
183
+ margin: 0;
184
+ padding: 4px 0 8px;
185
+ }
186
+ .code-line {
187
+ display: flex;
188
+ min-width: max-content;
189
+ }
190
+ .line-number {
191
+ width: 32px;
192
+ text-align: right;
193
+ padding-right: 10px;
194
+ flex-shrink: 0;
195
+ color: var(--muted);
196
+ user-select: none;
197
+ }
198
+ .code-line code {
199
+ font: inherit;
200
+ }
201
+ .json-key {
202
+ color: #82b5e8;
203
+ }
204
+ .json-string {
205
+ color: #d8b28c;
206
+ }
207
+ .json-number {
208
+ color: #b7a4d9;
209
+ }
210
+ .light .json-key {
211
+ color: #245585;
212
+ }
213
+ .light .json-string {
214
+ color: #885523;
215
+ }
216
+ .light .json-number {
217
+ color: #6b409c;
218
+ }
219
+ .json-tree {
220
+ font:
221
+ 11px / 1.7 Consolas,
222
+ monospace;
223
+ border: 0;
224
+ margin: 0;
225
+ padding: 2px 0 6px;
226
+ min-width: 0;
227
+ overflow-x: auto;
228
+ }
229
+ .json-object {
230
+ min-width: max-content;
231
+ }
232
+ .json-object summary {
233
+ cursor: pointer;
234
+ white-space: nowrap;
235
+ color: var(--secondary);
236
+ }
237
+ .json-summary {
238
+ color: var(--muted);
239
+ font-size: 10px;
240
+ }
241
+ .json-children {
242
+ padding-left: 16px;
243
+ border-left: 1px solid var(--panel-border);
244
+ margin-left: 5px;
245
+ }
246
+ .json-leaf {
247
+ white-space: pre;
248
+ min-width: max-content;
249
+ }
250
+ .preview-limit {
251
+ display: block;
252
+ padding: 6px;
253
+ color: var(--amber-11);
254
+ white-space: normal;
255
+ }
256
+ .preview-image {
257
+ max-width: 100%;
258
+ max-height: 220px;
259
+ object-fit: contain;
260
+ margin-top: 6px;
261
+ }
@@ -0,0 +1,289 @@
1
+ import {
2
+ ChatBubbleIcon,
3
+ CheckIcon,
4
+ ChevronRightIcon,
5
+ ClipboardCopyIcon,
6
+ CubeIcon,
7
+ ExclamationTriangleIcon,
8
+ FileTextIcon,
9
+ GearIcon,
10
+ LightningBoltIcon,
11
+ PersonIcon,
12
+ StackIcon,
13
+ } from "@radix-ui/react-icons";
14
+ import { Badge, Button, Flex, Text } from "@radix-ui/themes";
15
+ import { useEffect, useRef, useState } from "react";
16
+ import type { Call, Capture, Json } from "../model.js";
17
+ import { duration, group, output } from "./format.js";
18
+ import { JsonTree } from "./json-tree.js";
19
+
20
+ export function Glyph({ kind }: { kind: string }) {
21
+ const Icon =
22
+ kind === "toolResult"
23
+ ? StackIcon
24
+ : kind === "toolCall"
25
+ ? GearIcon
26
+ : kind === "assistant"
27
+ ? ChatBubbleIcon
28
+ : kind === "user"
29
+ ? PersonIcon
30
+ : kind === "thinking_level_change"
31
+ ? GearIcon
32
+ : group(kind) === "Model"
33
+ ? CubeIcon
34
+ : group(kind) === "Tool"
35
+ ? LightningBoltIcon
36
+ : kind === "system"
37
+ ? FileTextIcon
38
+ : CubeIcon;
39
+ return (
40
+ <span
41
+ className={`event-glyph glyph-${group(kind).toLowerCase()} ${kind === "thinking_level_change" ? "glyph-thinking" : ""}`}
42
+ >
43
+ <Icon />
44
+ </span>
45
+ );
46
+ }
47
+ export function Status({ value = "recorded" }: { value?: string }) {
48
+ const label =
49
+ value === "ok" || value === "success"
50
+ ? "Success"
51
+ : value === "error"
52
+ ? "Error"
53
+ : value === "running"
54
+ ? "Running"
55
+ : value === "cancelled"
56
+ ? "Cancelled"
57
+ : value === "unfinished"
58
+ ? "Unfinished"
59
+ : "Recorded";
60
+ return (
61
+ <span className={`status status-${value}`}>
62
+ {label === "Success" ? (
63
+ <CheckIcon />
64
+ ) : label === "Error" ? (
65
+ <ExclamationTriangleIcon />
66
+ ) : (
67
+ <span className="status-dot" />
68
+ )}
69
+ {label}
70
+ </span>
71
+ );
72
+ }
73
+ function images(value: Json, found: { data: string; mimeType: string }[] = []): typeof found {
74
+ if (!value || typeof value !== "object" || found.length >= 4) return found;
75
+ if (
76
+ !Array.isArray(value) &&
77
+ value.type === "image" &&
78
+ typeof value.data === "string" &&
79
+ typeof value.mimeType === "string" &&
80
+ /^(image\/(png|jpeg|gif|webp))$/.test(value.mimeType) &&
81
+ value.data.length <= 16384 &&
82
+ /^[A-Za-z0-9+/]*={0,2}$/.test(value.data)
83
+ ) {
84
+ if (!found.some((image) => image.data === value.data && image.mimeType === value.mimeType))
85
+ found.push({ data: value.data, mimeType: value.mimeType });
86
+ } else for (const child of Object.values(value)) images(child, found);
87
+ return found;
88
+ }
89
+ export function Copy({ value }: { value: string }) {
90
+ const mounted = useRef(false);
91
+ const [feedback, setFeedback] = useState("");
92
+ const [pending, setPending] = useState(false);
93
+ useEffect(() => {
94
+ mounted.current = true;
95
+ return () => {
96
+ mounted.current = false;
97
+ };
98
+ }, []);
99
+ async function copy() {
100
+ setPending(true);
101
+ try {
102
+ await navigator.clipboard.writeText(value);
103
+ if (mounted.current) setFeedback("Copied");
104
+ } catch {
105
+ if (mounted.current) setFeedback("Copy failed");
106
+ } finally {
107
+ if (mounted.current) setPending(false);
108
+ }
109
+ }
110
+ return (
111
+ <Button
112
+ size="1"
113
+ variant="ghost"
114
+ className="copy-button"
115
+ disabled={pending}
116
+ onClick={() => void copy()}
117
+ aria-label="Copy display data"
118
+ >
119
+ <ClipboardCopyIcon />
120
+ {feedback || "Copy"}
121
+ </Button>
122
+ );
123
+ }
124
+ function colored(line: string, budget: { remaining: number }) {
125
+ if (line.length > 2000 || budget.remaining <= 0) return line;
126
+ const fragments = [];
127
+ let cursor = 0;
128
+ for (const match of line.matchAll(/("(?:[^"\\]|\\.)*"|\b(?:true|false|null|\d+(?:\.\d+)?)\b)/g)) {
129
+ if (--budget.remaining < 0) return line;
130
+ if (match.index > cursor) fragments.push(line.slice(cursor, match.index));
131
+ const part = match[0];
132
+ fragments.push(
133
+ <span
134
+ key={match.index}
135
+ className={
136
+ part.startsWith('"')
137
+ ? /^\s*:/.test(line.slice(match.index + part.length))
138
+ ? "json-key"
139
+ : "json-string"
140
+ : "json-number"
141
+ }
142
+ >
143
+ {part}
144
+ </span>,
145
+ );
146
+ cursor = match.index + part.length;
147
+ }
148
+ fragments.push(line.slice(cursor));
149
+ return fragments;
150
+ }
151
+ export function Data({
152
+ data,
153
+ label,
154
+ defaultOpen = true,
155
+ scope = label,
156
+ }: {
157
+ data?: Capture;
158
+ label: string;
159
+ defaultOpen?: boolean;
160
+ scope?: string;
161
+ }) {
162
+ const [open, setOpen] = useState(defaultOpen);
163
+ const [format, setFormat] = useState("tree");
164
+ const value = data ? output(data.value) : "unavailable";
165
+ const lines = value.split("\n");
166
+ const budget = { remaining: 512 };
167
+ let sourceOffset = 0;
168
+ const structured = data?.value !== null && typeof data?.value === "object";
169
+ return (
170
+ <section className="data">
171
+ <div className="data-toolbar">
172
+ <button type="button" className="section-toggle" aria-expanded={open} onClick={() => setOpen(!open)}>
173
+ <ChevronRightIcon className={open ? "chevron-open" : ""} />
174
+ <span>{label}</span>
175
+ </button>
176
+ <Flex align="center" gap="2">
177
+ {data?.truncated && <Badge color="amber">truncated</Badge>}
178
+ {data && <Copy key={value} value={value} />}
179
+ </Flex>
180
+ </div>
181
+ <div hidden={!open}>
182
+ {structured && (
183
+ <div className="data-mode">
184
+ {["tree", "text"].map((mode) => (
185
+ <button type="button" key={mode} aria-pressed={format === mode} onClick={() => setFormat(mode)}>
186
+ {mode === "tree" ? "Objects" : "JSON text"}
187
+ </button>
188
+ ))}
189
+ <span>Redacted display copy</span>
190
+ </div>
191
+ )}
192
+ {structured && format === "tree" && data ? (
193
+ <JsonTree value={data.value} scope={scope} />
194
+ ) : (
195
+ <pre className="code-preview">
196
+ {lines.slice(0, 1000).map((line, index) => {
197
+ const start = sourceOffset;
198
+ sourceOffset += line.length + 1;
199
+ return (
200
+ <span className="code-line" key={`line-${start}`}>
201
+ <span className="line-number" aria-hidden="true">
202
+ {index + 1}
203
+ </span>
204
+ <code>{colored(line, budget)}</code>
205
+ </span>
206
+ );
207
+ })}
208
+ {lines.length > 1000 && (
209
+ <span className="preview-limit">
210
+ Preview limited to 1,000 lines; Copy includes the bounded display value.
211
+ </span>
212
+ )}
213
+ </pre>
214
+ )}
215
+ {data &&
216
+ images(data.value).map((image) => (
217
+ <img
218
+ key={`${image.mimeType}-${image.data}`}
219
+ className="preview-image"
220
+ src={`data:${image.mimeType};base64,${image.data}`}
221
+ alt="Captured raster tool output"
222
+ />
223
+ ))}
224
+ </div>
225
+ </section>
226
+ );
227
+ }
228
+ export function Metadata({ rows }: { rows: [string, string | undefined][] }) {
229
+ return (
230
+ <dl className="metadata">
231
+ {rows.map(([key, value]) => (
232
+ <div key={key}>
233
+ <dt>{key}</dt>
234
+ <dd title={value}>{value || "unavailable"}</dd>
235
+ </div>
236
+ ))}
237
+ </dl>
238
+ );
239
+ }
240
+ export function CallView({ call, all, group: scope = "live" }: { call: Call; all: Call[]; group?: string }) {
241
+ return (
242
+ <div className="call" id={scope === "detail" ? `${scope}-call-${call.occurrenceId}` : undefined}>
243
+ <Flex align="center" gap="2">
244
+ <Glyph kind="toolResult" />
245
+ <Text weight="bold">{call.name}</Text>
246
+ <Status value={call.status} />
247
+ </Flex>
248
+ <Metadata
249
+ rows={[
250
+ ["Call ID", call.id],
251
+ ["Occurrence", call.occurrenceId],
252
+ ["Parent relationship", call.parentUnavailable],
253
+ [
254
+ "Correlation",
255
+ call.correlationUnavailable ? "Reused or overlapping raw IDs; association unavailable" : undefined,
256
+ ],
257
+ ["Source", "Observed tool execution"],
258
+ [
259
+ "Start observed",
260
+ call.observedStartedAt === undefined ? undefined : new Date(call.observedStartedAt).toISOString(),
261
+ ],
262
+ [
263
+ "End observed",
264
+ call.observedEndedAt === undefined ? undefined : new Date(call.observedEndedAt).toISOString(),
265
+ ],
266
+ ["Duration", duration(call.durationMs)],
267
+ ["Children", String(all.filter((child) => child.parentOccurrenceId === call.occurrenceId).length)],
268
+ ]}
269
+ />
270
+ {call.parentId &&
271
+ (all.some((parent) => parent.occurrenceId === call.parentOccurrenceId) ? (
272
+ <a className="parent-link" href={`#${scope}-call-${encodeURIComponent(call.parentOccurrenceId ?? "")}`}>
273
+ Parent {call.parentId}
274
+ <ChevronRightIcon />
275
+ </a>
276
+ ) : (
277
+ <Text size="1" color="gray">
278
+ Parent {call.parentId} (not captured)
279
+ </Text>
280
+ ))}
281
+ <Data label="Arguments" data={call.args} scope={`${call.occurrenceId}-args`} defaultOpen={false} />
282
+ <Data
283
+ label={call.result ? "Result · tool_execution event" : "Result · not captured"}
284
+ data={call.result}
285
+ scope={`${call.occurrenceId}-result`}
286
+ />
287
+ </div>
288
+ );
289
+ }