@narumitw/pi-inspect 0.1.0 → 0.2.1

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.
@@ -1,36 +1,38 @@
1
1
  import { ChevronDownIcon, ChevronRightIcon } from "@radix-ui/react-icons";
2
- import { Button, Text } from "@radix-ui/themes";
2
+ import { Button, Heading, Text } from "@radix-ui/themes";
3
3
  import { useEffect, useMemo, useRef, useState } from "react";
4
4
  import type { Call, EntrySummary } from "../model.js";
5
- import { CallView, Status } from "./components.js";
6
- import { duration, type Filters, matchesCall } from "./format.js";
5
+ import { Status } from "./components.js";
6
+ import { duration, type Filters, matchesCall, output } from "./format.js";
7
7
  import { flatten, hierarchy, reveal, withAncestors } from "./hierarchy.js";
8
8
  import { searchNeedle } from "./search.js";
9
9
  import { TimelineMark } from "./timeline.js";
10
10
  import { axis } from "./timing.js";
11
11
 
12
- export function LiveDrawer({
12
+ export function CapturedExecutions({
13
13
  calls,
14
14
  entries,
15
15
  selected,
16
16
  select,
17
17
  open,
18
- changeOpen,
19
18
  view,
19
+ changeView,
20
20
  dropped,
21
21
  invalidEvents,
22
22
  filters,
23
+ revealSelected,
23
24
  }: {
24
25
  calls: Call[];
25
26
  entries: EntrySummary[];
26
27
  selected?: string;
27
28
  select(id: string): void;
28
29
  open: boolean;
29
- changeOpen(open: boolean): void;
30
30
  view: string;
31
+ changeView(view: string): void;
31
32
  dropped: number;
32
33
  invalidEvents: number;
33
34
  filters: Filters;
35
+ revealSelected: boolean;
34
36
  }) {
35
37
  const preparedFilters = useMemo(() => ({ ...filters, query: searchNeedle(filters.query) }), [filters]);
36
38
  const tree = useMemo(
@@ -50,9 +52,12 @@ export function LiveDrawer({
50
52
  () =>
51
53
  withAncestors(
52
54
  tree,
53
- new Set(calls.filter((call) => matchesCall(call, preparedFilters)).map((call) => call.occurrenceId)),
55
+ new Set([
56
+ ...calls.filter((call) => matchesCall(call, preparedFilters)).map((call) => call.occurrenceId),
57
+ ...(revealSelected && selected ? [selected] : []),
58
+ ]),
54
59
  ),
55
- [tree, calls, preparedFilters],
60
+ [tree, calls, preparedFilters, revealSelected, selected],
56
61
  );
57
62
  const [expanded, setExpanded] = useState(new Set<string>());
58
63
  const previous = useRef<string | undefined>(undefined);
@@ -72,17 +77,17 @@ export function LiveDrawer({
72
77
  }
73
78
  }, [selected, tree]);
74
79
  const rows = flatten(tree, expanded, keep);
80
+ const indent = Math.min(12, 140 / Math.max(1, ...rows.map((row) => row.depth)));
75
81
  const range = axis(entries, calls);
76
82
  return (
77
83
  <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>
84
+ <div className="live-drawer-toggle">
85
+ <Heading size="3">Captured executions</Heading>
81
86
  <span>
82
87
  {calls.length} captured · {calls.filter((call) => call.status === "error").length} errors · {dropped} evicted
83
88
  </span>
84
89
  <span className="drawer-scope">Session-wide · observed events</span>
85
- </button>
90
+ </div>
86
91
  {open && (
87
92
  <div className="live-drawer-body">
88
93
  {invalidEvents > 0 && (
@@ -94,6 +99,13 @@ export function LiveDrawer({
94
99
  <Text size="1" color="gray">
95
100
  Reported durations are monotonic execute() timings; bars are observed callback intervals.
96
101
  </Text>
102
+ <div className="view-switch">
103
+ {["list", "timeline"].map((mode) => (
104
+ <button type="button" key={mode} aria-pressed={view === mode} onClick={() => changeView(mode)}>
105
+ {mode === "list" ? "List" : "Timeline"}
106
+ </button>
107
+ ))}
108
+ </div>
97
109
  <Button size="1" variant="ghost" disabled={!keep.size} onClick={() => setExpanded(new Set(keep))}>
98
110
  Expand calls
99
111
  </Button>
@@ -109,7 +121,7 @@ export function LiveDrawer({
109
121
  className="live-trace-item"
110
122
  data-parent-id={call.parentId ?? ""}
111
123
  data-raw-id={call.rawId}
112
- style={{ marginLeft: depth * 12 }}
124
+ style={{ marginLeft: depth * indent }}
113
125
  >
114
126
  <div className={`live-call-row ${selected === call.id ? "selected" : ""}`}>
115
127
  <button
@@ -135,7 +147,10 @@ export function LiveDrawer({
135
147
  onClick={() => select(call.id)}
136
148
  >
137
149
  <strong title={call.name}>{call.name}</strong>
138
- <code title={call.occurrenceId}>{call.rawId}</code>
150
+ <span className="call-summary" title={output(call.args.value)}>
151
+ {output(call.args.value).replace(/\s+/g, " ").slice(0, 120)}
152
+ </span>
153
+ <code title={call.rawId}>{call.rawId.slice(0, 16)}</code>
139
154
  <Status value={call.status} />
140
155
  <span>{duration(call.durationMs)}</span>
141
156
  </button>
@@ -153,19 +168,6 @@ export function LiveDrawer({
153
168
  />
154
169
  )}
155
170
  </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
171
  </div>
170
172
  ))}
171
173
  </div>
@@ -11,10 +11,10 @@ import {
11
11
  PersonIcon,
12
12
  StackIcon,
13
13
  } from "@radix-ui/react-icons";
14
- import { Badge, Button, Flex, Text } from "@radix-ui/themes";
14
+ import { Badge, Button, Flex } from "@radix-ui/themes";
15
15
  import { useEffect, useRef, useState } from "react";
16
- import type { Call, Capture, Json } from "../model.js";
17
- import { duration, group, output } from "./format.js";
16
+ import type { Capture, Json } from "../model.js";
17
+ import { group, output } from "./format.js";
18
18
  import { JsonTree } from "./json-tree.js";
19
19
 
20
20
  export function Glyph({ kind }: { kind: string }) {
@@ -178,50 +178,52 @@ export function Data({
178
178
  {data && <Copy key={value} value={value} />}
179
179
  </Flex>
180
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}
181
+ {open && (
182
+ <div>
183
+ {structured && (
184
+ <div className="data-mode">
185
+ {["tree", "text"].map((mode) => (
186
+ <button type="button" key={mode} aria-pressed={format === mode} onClick={() => setFormat(mode)}>
187
+ {mode === "tree" ? "Objects" : "JSON text"}
188
+ </button>
189
+ ))}
190
+ <span>Redacted display copy</span>
191
+ </div>
192
+ )}
193
+ {structured && format === "tree" && data ? (
194
+ <JsonTree value={data.value} scope={scope} />
195
+ ) : (
196
+ <pre className="code-preview">
197
+ {lines.slice(0, 1000).map((line, index) => {
198
+ const start = sourceOffset;
199
+ sourceOffset += line.length + 1;
200
+ return (
201
+ <span className="code-line" key={`line-${start}`}>
202
+ <span className="line-number" aria-hidden="true">
203
+ {index + 1}
204
+ </span>
205
+ <code>{colored(line, budget)}</code>
203
206
  </span>
204
- <code>{colored(line, budget)}</code>
207
+ );
208
+ })}
209
+ {lines.length > 1000 && (
210
+ <span className="preview-limit">
211
+ Preview limited to 1,000 lines; Copy includes the bounded display value.
205
212
  </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>
213
+ )}
214
+ </pre>
215
+ )}
216
+ {data &&
217
+ images(data.value).map((image) => (
218
+ <img
219
+ key={`${image.mimeType}-${image.data}`}
220
+ className="preview-image"
221
+ src={`data:${image.mimeType};base64,${image.data}`}
222
+ alt="Captured raster tool output"
223
+ />
224
+ ))}
225
+ </div>
226
+ )}
225
227
  </section>
226
228
  );
227
229
  }
@@ -237,53 +239,3 @@ export function Metadata({ rows }: { rows: [string, string | undefined][] }) {
237
239
  </dl>
238
240
  );
239
241
  }
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
- }
@@ -19,9 +19,11 @@ const names: Record<string, string> = {
19
19
  export function ContextComposition({
20
20
  context,
21
21
  payload,
22
+ preview = false,
22
23
  }: {
23
24
  context?: Composition;
24
25
  payload?: Snapshot["providerObservation"];
26
+ preview?: boolean;
25
27
  }) {
26
28
  const [query, search] = useState("");
27
29
  const [category, filter] = useState("all");
@@ -111,15 +113,18 @@ export function ContextComposition({
111
113
  </TextField.Root>
112
114
  </div>
113
115
  <div className="context-provenance" role="status">
114
- {context?.source === "observed-pi-context"
115
- ? "Last observed Pi context · context_with_system stage; later hooks and provider serialization may differ."
116
- : "Session-derived context · native active-leaf projection; request-local hooks and provider serialization are unavailable."}
116
+ {preview
117
+ ? "Session-derived preview · selected branch projection; not a captured historical request."
118
+ : context?.source === "observed-pi-context"
119
+ ? "Last observed Pi context · context_with_system stage; later hooks and provider serialization may differ."
120
+ : "Session-derived context · native active-leaf projection; request-local hooks and provider serialization are unavailable."}
117
121
  {context?.observedAt && (
118
122
  <span>
119
123
  {" "}
120
124
  Observed {new Date(context.observedAt).toLocaleTimeString()} · leaf {context.leafId ?? "unavailable"}
121
125
  </span>
122
126
  )}
127
+ {!context?.observedAt && <span> Leaf {context?.leafId ?? "unavailable"}.</span>}
123
128
  {context?.incomplete && (
124
129
  <strong> Bounded capture is incomplete; only available captured content is searchable.</strong>
125
130
  )}
@@ -0,0 +1,65 @@
1
+ import { Badge, Card, Flex, Text } from "@radix-ui/themes";
2
+ import { useState } from "react";
3
+ import type { Snapshot } from "../model.js";
4
+ import { Data } from "./components.js";
5
+
6
+ export function ContextInventory({ snapshot }: { snapshot?: Snapshot }) {
7
+ const [open, setOpen] = useState(false);
8
+ const [toolsOpen, setToolsOpen] = useState(false);
9
+ const [skillsOpen, setSkillsOpen] = useState(false);
10
+ return (
11
+ <details className="context-inventory" open={open} onToggle={(event) => setOpen(event.currentTarget.open)}>
12
+ <summary>Current runtime information · not historical request evidence</summary>
13
+ {open && (
14
+ <div className="inventory-scroll">
15
+ <Data
16
+ label="Current runtime effective prompt · may not yet be sent"
17
+ data={snapshot?.currentPrompt}
18
+ defaultOpen={false}
19
+ />
20
+ <Text as="p" size="2" color="gray">
21
+ Current active ≠ provider-visible. MCP connection status is unavailable.
22
+ </Text>
23
+ <details open={toolsOpen} onToggle={(event) => setToolsOpen(event.currentTarget.open)}>
24
+ <summary>Current tools</summary>
25
+ {toolsOpen &&
26
+ snapshot?.tools.map((tool) => (
27
+ <Card key={tool.name} className="inventory-card">
28
+ <Text weight="bold">{tool.name}</Text>
29
+ <Flex gap="1" wrap="wrap">
30
+ <Badge>{tool.namespace ?? "tool"}</Badge>
31
+ <Badge>{tool.exposure}</Badge>
32
+ <Badge>{tool.active ? "active" : "inactive"}</Badge>
33
+ <Badge>{tool.callable ? "callable" : "not callable"}</Badge>
34
+ </Flex>
35
+ <Text as="p" size="2" color="gray">
36
+ {tool.description}
37
+ </Text>
38
+ <Data label={`Schema · ${tool.name}`} data={tool.schema} defaultOpen={false} />
39
+ </Card>
40
+ ))}
41
+ </details>
42
+ <details open={skillsOpen} onToggle={(event) => setSkillsOpen(event.currentTarget.open)}>
43
+ <summary>Currently advertised skills</summary>
44
+ <Text as="p" size="2" color="gray">
45
+ Advertised or read does not prove model compliance. Historical catalogs are unavailable.
46
+ </Text>
47
+ {skillsOpen &&
48
+ snapshot?.skills.map((skill) => (
49
+ <Card key={skill.path} className="inventory-card">
50
+ <Text weight="bold">{skill.name}</Text>
51
+ <Badge>advertised</Badge>
52
+ <Text as="p" size="2">
53
+ {skill.description}
54
+ </Text>
55
+ <Text size="1" color="gray">
56
+ {skill.path}
57
+ </Text>
58
+ </Card>
59
+ ))}
60
+ </details>
61
+ </div>
62
+ )}
63
+ </details>
64
+ );
65
+ }
package/src/web/format.ts CHANGED
@@ -47,7 +47,9 @@ export function matches(entry: EntrySummary, filters: Filters, calls: Call[]): b
47
47
  (!filters.groups.length || filters.groups.includes(group(entry.kind))) &&
48
48
  (!filters.errorsOnly || entry.status === "error" || call?.status === "error") &&
49
49
  (!filters.slowOnly || (call?.durationMs ?? 0) > 10000) &&
50
- `${entry.kind} ${entry.name ?? ""} ${entry.label} ${entry.id}`.toLowerCase().includes(filters.query)
50
+ `${entry.kind} ${entry.name ?? ""} ${entry.label} ${entry.summary ?? ""} ${entry.id}`
51
+ .toLowerCase()
52
+ .includes(filters.query)
51
53
  );
52
54
  }
53
55
  export function matchesCall(call: Call, filters: Filters): boolean {
@@ -0,0 +1,56 @@
1
+ import type { EntrySummary } from "../model.js";
2
+ import { label } from "./hierarchy.js";
3
+
4
+ /** Pi bookkeeping entries do not contribute messages; custom_message always does. */
5
+ export function internalEntry(entry: EntrySummary): boolean {
6
+ return !entry.status && entry.internal === true;
7
+ }
8
+ export interface HistoryRow {
9
+ node: EntrySummary;
10
+ turn: string;
11
+ heading?: string;
12
+ }
13
+ export function history(entries: EntrySummary[], leaf: string | null): { rows: HistoryRow[]; issue?: string } {
14
+ const byId = new Map(entries.map((entry) => [entry.id, entry]));
15
+ const path: EntrySummary[] = [];
16
+ const seen = new Set<string>();
17
+ let id = leaf;
18
+ let issue: string | undefined;
19
+ while (id) {
20
+ if (seen.has(id)) {
21
+ issue = "Recorded parent cycle; partial history only";
22
+ break;
23
+ }
24
+ if (path.length >= 10000) {
25
+ issue = "History inspection budget exceeded";
26
+ break;
27
+ }
28
+ const entry = byId.get(id);
29
+ if (!entry) {
30
+ issue = "Parent or branch leaf outside indexed data; partial history only";
31
+ break;
32
+ }
33
+ seen.add(id);
34
+ path.push(entry);
35
+ id = entry.parentId;
36
+ }
37
+ if (!leaf) issue = "Active branch leaf unavailable; use Branch view to inspect indexed entries";
38
+ const rows: HistoryRow[] = [];
39
+ let turn = "before-user";
40
+ let first = true;
41
+ for (const node of path.reverse()) {
42
+ const starts = first || node.kind === "user";
43
+ if (node.kind === "user") turn = node.id;
44
+ rows.push({
45
+ node,
46
+ turn,
47
+ heading: starts ? (node.kind === "user" ? "User turn" : "Before first user turn") : undefined,
48
+ });
49
+ first = false;
50
+ }
51
+ return { rows, issue };
52
+ }
53
+
54
+ export function historySummary(entry: EntrySummary): string {
55
+ return entry.summary || (entry.kind === "system" ? "System prompt update" : label(entry));
56
+ }