@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,11 +1,13 @@
1
1
  import * as Collapsible from "@radix-ui/react-collapsible";
2
2
  import { ChevronRightIcon, CubeIcon } from "@radix-ui/react-icons";
3
- import { Badge, Button, Card, Flex, Heading, Tabs, Text } from "@radix-ui/themes";
3
+ import { Button, Heading, Tabs, Text } from "@radix-ui/themes";
4
4
  import { useEffect, useMemo, useState } from "react";
5
5
  import type { BranchView, Call, Capture, DetailView, EntrySummary, Json, Snapshot } from "../model.js";
6
- import { CallView, Copy, Data, Metadata, Status } from "./components.js";
6
+ import { Copy, Data, Metadata, Status } from "./components.js";
7
7
  import { entryCalls } from "./entry-calls.js";
8
8
  import { count, duration, eventName, output, promptDiff, record, scripts } from "./format.js";
9
+ import { RecordedContent } from "./recorded-content.js";
10
+ import { ToolInspector } from "./tool-inspector.js";
9
11
 
10
12
  export function Inspector({
11
13
  selected,
@@ -22,6 +24,7 @@ export function Inspector({
22
24
  changeTab,
23
25
  select,
24
26
  selectCall,
27
+ preview,
25
28
  }: {
26
29
  selected: string;
27
30
  node?: EntrySummary;
@@ -37,6 +40,7 @@ export function Inspector({
37
40
  changeTab(tab: string): void;
38
41
  select(id: string): void;
39
42
  selectCall(id: string): void;
43
+ preview(): void;
40
44
  }) {
41
45
  const [format, setFormat] = useState("formatted");
42
46
  const raw = record(detail?.raw.value);
@@ -62,24 +66,22 @@ export function Inspector({
62
66
  );
63
67
  const enabled = useMemo(
64
68
  () => [
69
+ "content",
65
70
  "raw",
66
- "prompt",
67
- "tools",
68
- ...(isCall ? [] : ["context"]),
69
- ...(hasSkills ? ["skills"] : []),
71
+ ...(isCall ? [] : ["prompt", "tools", "context", ...(hasSkills ? ["skills"] : [])]),
70
72
  ...(codemode ? ["codemode"] : []),
71
73
  ],
72
74
  [isCall, hasSkills, codemode],
73
75
  );
74
76
  useEffect(() => {
75
- if (!enabled.includes(tab)) changeTab("raw");
77
+ if (!enabled.includes(tab)) changeTab("content");
76
78
  }, [enabled, tab, changeTab]);
77
79
  return (
78
80
  <section className="panel inspector-panel">
79
81
  <div className="panel-heading">
80
82
  <Heading size="3">
81
83
  <CubeIcon />
82
- Event inspector
84
+ Details
83
85
  </Heading>
84
86
  {activeRaw && <Copy key={selected} value={output(activeRaw.value)} />}
85
87
  </div>
@@ -90,7 +92,8 @@ export function Inspector({
90
92
  </Text>
91
93
  )}
92
94
  <Heading size="4">
93
- {call ? "Captured execution" : node ? eventName(node.kind) : "Entry"} · {selected || "no selection"}
95
+ {call ? "Captured execution" : node ? eventName(node.kind) : "Entry"} ·{" "}
96
+ {selected.slice(0, 16) || "no selection"}
94
97
  </Heading>
95
98
  <Text size="2" color="gray">
96
99
  {call?.name ?? node?.name ?? "Session log entry"}
@@ -119,12 +122,96 @@ export function Inspector({
119
122
  )}
120
123
  <Tabs.Root value={tab} onValueChange={changeTab} className="inspector-tabs">
121
124
  <Tabs.List wrap="nowrap" aria-label="Event detail views">
122
- {["raw", "prompt", "tools", "context", "skills", "codemode"].map((tab) => (
125
+ {["content", "raw", "prompt", "tools", "context", "skills", "codemode"].map((tab) => (
123
126
  <Tabs.Trigger key={tab} value={tab} disabled={!enabled.includes(tab)}>
124
127
  {tab}
125
128
  </Tabs.Trigger>
126
129
  ))}
127
130
  </Tabs.List>
131
+ <Tabs.Content value="content">
132
+ {call ? (
133
+ <>
134
+ <Status value={call.status} />
135
+ <Data label="Arguments · redacted display copy" data={call.args} scope={`${call.occurrenceId}-args`} />
136
+ <Data label="Result · observed tool event" data={call.result} scope={`${call.occurrenceId}-result`} />
137
+ <div className="inspector-metadata">
138
+ <Metadata
139
+ rows={[
140
+ ["Source", "Observed tool execution"],
141
+ [
142
+ "Parent relationship",
143
+ call.parentUnavailable ??
144
+ call.parentOccurrenceId ??
145
+ (call.parentId ? "unavailable" : "none (root)"),
146
+ ],
147
+ [
148
+ "Correlation",
149
+ call.correlationUnavailable || !call.branchAnchor
150
+ ? "unavailable"
151
+ : "Recorded anchor only; not request association",
152
+ ],
153
+ ["Reported duration", duration(call.durationMs)],
154
+ ["Occurrence", call.occurrenceId],
155
+ ["Raw call ID", call.id],
156
+ ["Anchor entry", call.branchAnchor ?? undefined],
157
+ [
158
+ "Start observed",
159
+ call.observedStartedAt === undefined ? undefined : new Date(call.observedStartedAt).toISOString(),
160
+ ],
161
+ [
162
+ "End observed",
163
+ call.observedEndedAt === undefined ? undefined : new Date(call.observedEndedAt).toISOString(),
164
+ ],
165
+ [
166
+ "Children",
167
+ String(
168
+ snapshot?.calls.filter((child) => child.parentOccurrenceId === call.occurrenceId).length ?? 0,
169
+ ),
170
+ ],
171
+ ]}
172
+ />
173
+ </div>
174
+ {call.parentId && !snapshot?.calls.some((parent) => parent.occurrenceId === call.parentOccurrenceId) && (
175
+ <Text size="1" color="gray">
176
+ Parent {call.parentId} (not captured)
177
+ </Text>
178
+ )}
179
+ {snapshot?.calls
180
+ .filter(
181
+ (item) =>
182
+ item.parentOccurrenceId === call.occurrenceId || item.occurrenceId === call.parentOccurrenceId,
183
+ )
184
+ .map((item) => (
185
+ <Button
186
+ key={item.occurrenceId}
187
+ size="1"
188
+ variant="ghost"
189
+ onClick={() => selectCall(item.occurrenceId)}
190
+ >
191
+ Related execution · {item.name} · {item.occurrenceId}
192
+ </Button>
193
+ ))}
194
+ {call.branchAnchor && snapshot?.nodes.some((entry) => entry.id === call.branchAnchor) && (
195
+ <Button variant="ghost" size="1" onClick={() => select(call.branchAnchor ?? "")}>
196
+ Inspect recorded anchor →
197
+ </Button>
198
+ )}
199
+ </>
200
+ ) : (
201
+ <>
202
+ <Text as="p" size="1" color="gray">
203
+ Session log entry · bounded recorded content
204
+ </Text>
205
+ {node?.status && <Status value={node.status} />}
206
+ <RecordedContent data={detail?.raw} scope={selected} />
207
+ </>
208
+ )}
209
+ {!detail && !call && !detailError && (
210
+ <Text size="1" color="gray">
211
+ Loading bounded entry details…
212
+ </Text>
213
+ )}
214
+ </Tabs.Content>
128
215
  <Tabs.Content value="raw">
129
216
  <div className="format-toolbar">
130
217
  <div className="view-switch">
@@ -248,60 +335,14 @@ export function Inspector({
248
335
  />
249
336
  <Data label="Historical sections" data={branch?.sections} scope={`${selected}-sections`} />
250
337
  <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
338
  </Tabs.Content>
253
339
  <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
- ))}
340
+ <ToolInspector key={selected} data={branch?.declaredTools} scope={`${selected}-declaredTools`} />
286
341
  </Tabs.Content>
287
342
  <Tabs.Content value="skills">
288
343
  <Text as="p" size="2" color="gray">
289
344
  Advertised or read does not prove model compliance. Historical discovery may be unavailable.
290
345
  </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
346
  <Data label="Evidence on preview branch" data={branch?.skillEvidence} scope={`${selected}-skillEvidence`} />
306
347
  </Tabs.Content>
307
348
  <Tabs.Content value="context">
@@ -309,6 +350,9 @@ export function Inspector({
309
350
  Pi projection applies compaction and context edits. Request-local hooks can still transform it; this is not
310
351
  the final provider HTTP request.
311
352
  </Text>
353
+ <Button size="1" onClick={preview} disabled={!branch}>
354
+ View branch context
355
+ </Button>
312
356
  <Data label="Projected branch entries" data={branch?.projection} scope={`${selected}-projection`} />
313
357
  <Data label="Selected entry contribution" data={detail?.projected} scope={`${selected}-projected`} />
314
358
  </Tabs.Content>
@@ -328,7 +372,9 @@ export function Inspector({
328
372
  />
329
373
  <Data label="Source / output / persisted nestedCalls" data={detail?.raw} />
330
374
  {captured.map((call) => (
331
- <CallView key={call.occurrenceId} call={call} all={captured} group="detail" />
375
+ <Button key={call.occurrenceId} variant="ghost" onClick={() => selectCall(call.occurrenceId)}>
376
+ {call.name} · {call.occurrenceId} · inspect captured execution
377
+ </Button>
332
378
  ))}
333
379
  </Tabs.Content>
334
380
  </Tabs.Root>
@@ -0,0 +1,49 @@
1
+ import type { Capture } from "../model.js";
2
+ import { Data } from "./components.js";
3
+ import { record } from "./format.js";
4
+
5
+ export function RecordedContent({ data, scope }: { data?: Capture; scope: string }) {
6
+ const raw = record(data?.value);
7
+ const message = record(raw?.message);
8
+ const content =
9
+ message && Object.hasOwn(message, "content")
10
+ ? message.content
11
+ : raw && Object.hasOwn(raw, "data")
12
+ ? raw.data
13
+ : (raw?.content ?? raw?.summary ?? raw);
14
+ const blocks = Array.isArray(content) ? content : [content];
15
+ return (
16
+ <div className="recorded-content">
17
+ {blocks.slice(0, 128).map((block, index) => {
18
+ const item = record(block);
19
+ const name = typeof item?.name === "string" ? item.name : "unknown";
20
+ const label =
21
+ item?.type === "thinking"
22
+ ? "Thinking"
23
+ : item?.type === "toolCall"
24
+ ? `Tool call · ${name}`
25
+ : "Recorded content";
26
+ return (
27
+ <Data
28
+ // biome-ignore lint/suspicious/noArrayIndexKey: Historical content blocks are immutable and scoped by entry identity.
29
+ key={`${scope}-${index}`}
30
+ label={label}
31
+ data={
32
+ data && {
33
+ value: item?.text ?? item?.thinking ?? item?.arguments ?? block ?? null,
34
+ truncated: data.truncated || blocks.length > 128,
35
+ }
36
+ }
37
+ scope={`${scope}-content-${index}`}
38
+ />
39
+ );
40
+ })}
41
+ <Data
42
+ label="Metadata and Raw JSON · redacted display copy"
43
+ data={data}
44
+ scope={`${scope}-raw`}
45
+ defaultOpen={false}
46
+ />
47
+ </div>
48
+ );
49
+ }
@@ -0,0 +1,100 @@
1
+ import { Checkbox, Heading, Select, TextField } from "@radix-ui/themes";
2
+ import { useId } from "react";
3
+ import type { Filters } from "./format.js";
4
+
5
+ /** Filters only: ancestry lives in the primary Branch view, never a duplicate sidebar. */
6
+ export function SessionFilters({ filters, change }: { filters: Filters; change(filters: Filters): void }) {
7
+ const id = useId();
8
+ return (
9
+ <aside className="sidebar panel session-filters">
10
+ <div className="panel-heading">
11
+ <Heading size="3">Session filters</Heading>
12
+ </div>
13
+ <TextField.Root
14
+ aria-label="Search session"
15
+ placeholder="Search entries or content…"
16
+ value={filters.query}
17
+ onChange={(event) => change({ ...filters, query: event.target.value })}
18
+ />
19
+ <div className="filter-pills">
20
+ {["All", "Model", "Tool", "Custom"].map((group) => (
21
+ <button
22
+ type="button"
23
+ key={group}
24
+ aria-pressed={
25
+ group === "All" ? !filters.groups.length : filters.groups.length === 1 && filters.groups[0] === group
26
+ }
27
+ onClick={() => change({ ...filters, kind: "all", groups: group === "All" ? [] : [group] })}
28
+ >
29
+ {group}
30
+ </button>
31
+ ))}
32
+ </div>
33
+ <details className="filter-panel">
34
+ <summary>Filters</summary>
35
+ <label className="filter-check" htmlFor={`${id}-errors`}>
36
+ <span>Errors only</span>
37
+ <Checkbox
38
+ id={`${id}-errors`}
39
+ checked={filters.errorsOnly}
40
+ onCheckedChange={(checked) => change({ ...filters, errorsOnly: checked === true })}
41
+ />
42
+ </label>
43
+ <label className="filter-check" htmlFor={`${id}-slow`}>
44
+ <span>Slow tool calls (&gt; 10s)</span>
45
+ <Checkbox
46
+ id={`${id}-slow`}
47
+ checked={filters.slowOnly}
48
+ onCheckedChange={(checked) => change({ ...filters, slowOnly: checked === true })}
49
+ />
50
+ </label>
51
+ {["Tool", "Model", "Custom"].map((group) => (
52
+ <label className="filter-check" key={group} htmlFor={`${id}-${group}`}>
53
+ <span>{group} events</span>
54
+ <Checkbox
55
+ id={`${id}-${group}`}
56
+ checked={filters.groups.includes(group)}
57
+ onCheckedChange={() =>
58
+ change({
59
+ ...filters,
60
+ groups: filters.groups.includes(group)
61
+ ? filters.groups.filter((item) => item !== group)
62
+ : [...filters.groups, group],
63
+ })
64
+ }
65
+ />
66
+ </label>
67
+ ))}
68
+ <label className="filter-check" htmlFor={`${id}-kind`}>
69
+ <span>Entry type</span>
70
+ <Select.Root value={filters.kind} onValueChange={(kind) => change({ ...filters, kind })}>
71
+ <Select.Trigger id={`${id}-kind`} aria-label="Filter entry type" />
72
+ <Select.Content>
73
+ {[
74
+ "all",
75
+ "user",
76
+ "assistant",
77
+ "toolResult",
78
+ "system",
79
+ "custom",
80
+ "custom_message",
81
+ "compaction",
82
+ "branch_summary",
83
+ "model_change",
84
+ "thinking_level_change",
85
+ "context_edit",
86
+ "usage",
87
+ "label",
88
+ "session_info",
89
+ ].map((kind) => (
90
+ <Select.Item key={kind} value={kind}>
91
+ {kind}
92
+ </Select.Item>
93
+ ))}
94
+ </Select.Content>
95
+ </Select.Root>
96
+ </label>
97
+ </details>
98
+ </aside>
99
+ );
100
+ }
@@ -0,0 +1,114 @@
1
+ .observed-context,
2
+ .preview-context,
3
+ .session-primary {
4
+ display: flex;
5
+ flex-direction: column;
6
+ flex: 1;
7
+ min-height: 0;
8
+ min-width: 0;
9
+ }
10
+ .observed-context[hidden],
11
+ .session-primary[hidden],
12
+ .header-actions [hidden] {
13
+ display: none;
14
+ }
15
+ .context-inventory {
16
+ flex-shrink: 0;
17
+ border-top: 1px solid var(--panel-border);
18
+ padding: 6px 10px;
19
+ color: var(--secondary);
20
+ }
21
+ .context-inventory summary,
22
+ .session-filters summary {
23
+ cursor: pointer;
24
+ }
25
+ .inventory-scroll {
26
+ overflow: auto;
27
+ max-height: 38dvh;
28
+ }
29
+ .preview-heading,
30
+ .session-toolbar {
31
+ display: flex;
32
+ align-items: center;
33
+ gap: 8px;
34
+ flex-wrap: wrap;
35
+ padding: 6px 0;
36
+ color: var(--secondary);
37
+ font-size: 12px;
38
+ }
39
+ .preview-heading strong {
40
+ overflow-wrap: anywhere;
41
+ }
42
+ .preview-heading button,
43
+ .session-toolbar > button {
44
+ background: var(--panel-raised);
45
+ color: var(--gray-12);
46
+ border: 1px solid var(--panel-border);
47
+ border-radius: 4px;
48
+ padding: 5px;
49
+ }
50
+ .session-toolbar fieldset {
51
+ margin: 0;
52
+ border: 0;
53
+ flex-wrap: wrap;
54
+ }
55
+ .session-toolbar label {
56
+ display: flex;
57
+ gap: 4px;
58
+ align-items: center;
59
+ }
60
+ .session-filters {
61
+ padding: 8px;
62
+ overflow: auto;
63
+ }
64
+ .trace-history .trace-item {
65
+ border-left: 0;
66
+ }
67
+ .trace-history .trace-item::before {
68
+ display: none;
69
+ }
70
+ .turn-heading {
71
+ padding: 10px 8px 5px;
72
+ color: var(--muted);
73
+ font-weight: 600;
74
+ }
75
+ .execution-primary .live-drawer {
76
+ flex: 1;
77
+ max-height: none;
78
+ min-height: 0;
79
+ display: flex;
80
+ flex-direction: column;
81
+ }
82
+ .execution-primary .live-drawer-body {
83
+ flex: 1;
84
+ min-height: 0;
85
+ max-height: none;
86
+ }
87
+ .execution-primary .live-drawer-toggle {
88
+ flex-wrap: wrap;
89
+ flex-shrink: 0;
90
+ }
91
+ .call-summary {
92
+ flex: 2;
93
+ min-width: 0;
94
+ white-space: nowrap;
95
+ overflow: hidden;
96
+ text-overflow: ellipsis;
97
+ color: var(--muted);
98
+ }
99
+ .live-controls {
100
+ flex-wrap: wrap;
101
+ }
102
+ .recorded-content .code-preview {
103
+ white-space: pre-wrap;
104
+ overflow-wrap: anywhere;
105
+ }
106
+ @media (max-width: 680px) {
107
+ .brand .rt-Heading {
108
+ display: none;
109
+ }
110
+ .session-toolbar {
111
+ font-size: 11px;
112
+ gap: 4px;
113
+ }
114
+ }
package/src/web/style.css CHANGED
@@ -1,6 +1,7 @@
1
1
  @import "./components.css";
2
2
  @import "./panels.css";
3
3
  @import "./trace.css";
4
+ @import "./tool-inspector.css";
4
5
 
5
6
  :root,
6
7
  .radix-themes {
@@ -158,10 +159,10 @@ button:disabled {
158
159
  touch-action: none;
159
160
  grid-row: 1;
160
161
  }
161
- .pane-resizer[aria-label="Resize navigator"] {
162
+ .pane-resizer[aria-label="Resize filters"] {
162
163
  grid-column: 2;
163
164
  }
164
- .pane-resizer[aria-label="Resize inspector"] {
165
+ .pane-resizer[aria-label="Resize details"] {
165
166
  grid-column: 4;
166
167
  }
167
168
  .pane-resizer:hover,