@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.
- package/README.md +4 -1
- package/dist/app.css +1 -1
- package/dist/app.js +31 -29
- package/docs/viewer.md +26 -15
- package/package.json +1 -1
- package/src/entry-summary.ts +34 -0
- package/src/model.ts +4 -0
- package/src/projection.ts +14 -1
- package/src/web/app.tsx +196 -65
- package/src/web/{live-drawer.tsx → captured-executions.tsx} +29 -27
- package/src/web/components.tsx +47 -95
- package/src/web/context-composition.tsx +8 -3
- package/src/web/context-inventory.tsx +65 -0
- package/src/web/format.ts +3 -1
- package/src/web/history.ts +56 -0
- package/src/web/inspector.tsx +104 -58
- package/src/web/recorded-content.tsx +49 -0
- package/src/web/session-filters.tsx +100 -0
- package/src/web/session.css +114 -0
- package/src/web/style.css +3 -2
- package/src/web/tool-inspector.css +175 -0
- package/src/web/tool-inspector.tsx +210 -0
- package/src/web/tool-schema.ts +217 -0
- package/src/web/trace.css +1 -0
- package/src/web/trace.tsx +58 -18
- package/src/web/tree.tsx +0 -281
package/src/web/trace.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import type { Call, EntrySummary } from "../model.js";
|
|
|
5
5
|
import { Glyph, Status } from "./components.js";
|
|
6
6
|
import { count, eventName, time } from "./format.js";
|
|
7
7
|
import { flatten, hierarchy, label, reveal, withAncestors } from "./hierarchy.js";
|
|
8
|
+
import { type HistoryRow, historySummary } from "./history.js";
|
|
8
9
|
import { InlineEntry } from "./inline-entry.js";
|
|
9
10
|
import { TimelineMark } from "./timeline.js";
|
|
10
11
|
import { axis } from "./timing.js";
|
|
@@ -25,6 +26,8 @@ export function Trace({
|
|
|
25
26
|
view,
|
|
26
27
|
changeView,
|
|
27
28
|
calls,
|
|
29
|
+
historyRows,
|
|
30
|
+
active = true,
|
|
28
31
|
}: {
|
|
29
32
|
entries: EntrySummary[];
|
|
30
33
|
matches: Set<string>;
|
|
@@ -35,6 +38,8 @@ export function Trace({
|
|
|
35
38
|
view: string;
|
|
36
39
|
changeView(view: string): void;
|
|
37
40
|
calls: Call[];
|
|
41
|
+
historyRows?: HistoryRow[];
|
|
42
|
+
active?: boolean;
|
|
38
43
|
}) {
|
|
39
44
|
const tree = useMemo(() => hierarchy(entries), [entries]);
|
|
40
45
|
const keep = useMemo(
|
|
@@ -49,7 +54,25 @@ export function Trace({
|
|
|
49
54
|
const focusRequest = useRef(false);
|
|
50
55
|
const scroller = useRef<HTMLDivElement>(null);
|
|
51
56
|
const rowRefs = useRef(new Map<string, HTMLDivElement>());
|
|
52
|
-
const rows = useMemo(
|
|
57
|
+
const rows = useMemo(
|
|
58
|
+
() =>
|
|
59
|
+
historyRows
|
|
60
|
+
? historyRows
|
|
61
|
+
.filter(({ node }) => matches.has(node.id) || (revealSelected && node.id === selected))
|
|
62
|
+
.map((row, index, visible) => ({
|
|
63
|
+
...row,
|
|
64
|
+
depth: 0,
|
|
65
|
+
childCount: 0,
|
|
66
|
+
heading:
|
|
67
|
+
index === 0 || visible[index - 1]?.turn !== row.turn
|
|
68
|
+
? row.turn === "before-user"
|
|
69
|
+
? "Before first user turn"
|
|
70
|
+
: "User turn"
|
|
71
|
+
: undefined,
|
|
72
|
+
}))
|
|
73
|
+
: flatten(tree, expanded, keep),
|
|
74
|
+
[tree, expanded, keep, historyRows, matches, revealSelected, selected],
|
|
75
|
+
);
|
|
53
76
|
const pageRows = rows.slice(offset, offset + 50);
|
|
54
77
|
const visibleFocused = pageRows.some((row) => row.node.id === focused) ? focused : (pageRows[0]?.node.id ?? "");
|
|
55
78
|
const baseDepth = pageRows.length ? Math.min(...pageRows.map((row) => row.depth)) : 0;
|
|
@@ -58,15 +81,16 @@ export function Trace({
|
|
|
58
81
|
const range = useMemo(() => axis(entries, calls), [entries, calls]);
|
|
59
82
|
useEffect(() => {
|
|
60
83
|
if (!tree.nodes.has(selected) || serial === lastReveal.current) return;
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
setExpanded(next);
|
|
64
|
-
const index = flatten(tree, next, keep).findIndex((row) => row.node.id === selected);
|
|
84
|
+
const next = historyRows ? expanded : reveal(tree, expanded, selected);
|
|
85
|
+
const index = (historyRows ? rows : flatten(tree, next, keep)).findIndex((row) => row.node.id === selected);
|
|
65
86
|
if (index >= 0) {
|
|
87
|
+
// Keep reveal pending until the selection belongs to this projection.
|
|
88
|
+
lastReveal.current = serial;
|
|
89
|
+
if (!historyRows) setExpanded(next);
|
|
66
90
|
setOffset(Math.floor(index / 50) * 50);
|
|
67
91
|
setFocused(selected);
|
|
68
92
|
}
|
|
69
|
-
}, [tree, selected, serial, expanded, keep]);
|
|
93
|
+
}, [tree, selected, serial, expanded, keep, historyRows, rows]);
|
|
70
94
|
useEffect(() => {
|
|
71
95
|
const element = rowRefs.current.get(visibleFocused);
|
|
72
96
|
if (!element || !scroller.current) return;
|
|
@@ -97,11 +121,11 @@ export function Trace({
|
|
|
97
121
|
rowRefs.current.get(id)?.focus({ preventScroll: true });
|
|
98
122
|
}
|
|
99
123
|
return (
|
|
100
|
-
<section className={`panel trace-panel trace-${view}`}>
|
|
124
|
+
<section className={`panel trace-panel trace-${view} ${historyRows ? "trace-history" : "trace-branches"}`}>
|
|
101
125
|
<div className="trace-toolbar">
|
|
102
|
-
<Heading size="3">
|
|
126
|
+
<Heading size="3">{historyRows ? "History" : "Branch view"}</Heading>
|
|
103
127
|
<Text size="1" color="gray">
|
|
104
|
-
|
|
128
|
+
{historyRows ? "Conversation groups · not execution parentage" : "Actual session ancestry · not a call stack"}
|
|
105
129
|
</Text>
|
|
106
130
|
<div className="trace-actions">
|
|
107
131
|
<Button
|
|
@@ -129,19 +153,25 @@ export function Trace({
|
|
|
129
153
|
</div>
|
|
130
154
|
</div>
|
|
131
155
|
<div className="trace-hint">
|
|
132
|
-
Row: select
|
|
156
|
+
Row: select · Chevron: toggle only · ↑↓ navigate · ←→ collapse/expand · Enter select · Space toggle
|
|
133
157
|
</div>
|
|
134
158
|
{view === "timeline" && (
|
|
135
159
|
<div className="time-axis">
|
|
136
160
|
<span>{range ? new Date(range.start).toISOString().slice(11, 23) : "No timing"}</span>
|
|
137
|
-
<span>● Log timestamp · ▰ Observed tool interval (
|
|
161
|
+
<span>● Log timestamp · ▰ Observed tool interval (captured executions)</span>
|
|
138
162
|
<span>{range ? new Date(range.end).toISOString().slice(11, 23) : "—"}</span>
|
|
139
163
|
</div>
|
|
140
164
|
)}
|
|
141
165
|
<div className="trace-scroll" ref={scroller}>
|
|
142
166
|
<div role="tree" aria-label="Session trace" className="trace-tree">
|
|
143
|
-
{!rows.length &&
|
|
144
|
-
|
|
167
|
+
{!rows.length && (
|
|
168
|
+
<div className="empty-state">
|
|
169
|
+
{historyRows
|
|
170
|
+
? "No visible entries match in this branch. Check filters or Show internal events."
|
|
171
|
+
: "No entries match. Filters keep actual ancestor context."}
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
{pageRows.map(({ node: entry, depth, childCount, ...group }) => {
|
|
145
175
|
const open = expanded.has(entry.id);
|
|
146
176
|
const selectedRow = selected === entry.id;
|
|
147
177
|
return (
|
|
@@ -154,11 +184,17 @@ export function Trace({
|
|
|
154
184
|
data-state={open ? "open" : "closed"}
|
|
155
185
|
style={{ marginLeft: (depth - baseDepth) * indent }}
|
|
156
186
|
>
|
|
187
|
+
{"heading" in group && typeof group.heading === "string" && (
|
|
188
|
+
<div className="turn-heading">{group.heading}</div>
|
|
189
|
+
)}
|
|
157
190
|
<div
|
|
158
191
|
role="treeitem"
|
|
159
192
|
aria-level={depth + 1}
|
|
160
193
|
aria-expanded={open}
|
|
161
194
|
aria-selected={selectedRow}
|
|
195
|
+
aria-current={selectedRow ? "true" : undefined}
|
|
196
|
+
data-entry-id={entry.id}
|
|
197
|
+
data-match={matches.has(entry.id)}
|
|
162
198
|
tabIndex={visibleFocused === entry.id ? 0 : -1}
|
|
163
199
|
ref={(element) => {
|
|
164
200
|
if (element) rowRefs.current.set(entry.id, element);
|
|
@@ -170,7 +206,6 @@ export function Trace({
|
|
|
170
206
|
onClick={(event) => {
|
|
171
207
|
if (event.target !== event.currentTarget && (event.target as HTMLElement).closest("button")) return;
|
|
172
208
|
select(entry.id);
|
|
173
|
-
toggle(entry.id);
|
|
174
209
|
}}
|
|
175
210
|
onKeyDown={(event) => {
|
|
176
211
|
if (event.target !== event.currentTarget || event.altKey || event.ctrlKey || event.metaKey) return;
|
|
@@ -213,13 +248,16 @@ export function Trace({
|
|
|
213
248
|
<Glyph kind={entry.kind} />
|
|
214
249
|
<span className="row-title">
|
|
215
250
|
<strong>{eventName(entry.kind)}</strong>
|
|
216
|
-
<span title={
|
|
251
|
+
<span title={historySummary(entry)}>
|
|
252
|
+
{historyRows ? historySummary(entry) : label(entry)}
|
|
253
|
+
{entry.summaryTruncated && historyRows ? " · truncated" : ""}
|
|
254
|
+
</span>
|
|
217
255
|
</span>
|
|
218
256
|
<code title={entry.id}>{entry.id.slice(0, 8)}</code>
|
|
219
257
|
{!matches.has(entry.id) && (
|
|
220
258
|
<span className="ancestor-badge">{selectedRow ? "selected outside filters" : "ancestor"}</span>
|
|
221
259
|
)}
|
|
222
|
-
{depth === 0 && entry.parentId && tree.nodes.has(entry.parentId) && (
|
|
260
|
+
{!historyRows && depth === 0 && entry.parentId && tree.nodes.has(entry.parentId) && (
|
|
223
261
|
<span className="ancestor-badge">recorded parent cycle</span>
|
|
224
262
|
)}
|
|
225
263
|
</div>
|
|
@@ -234,7 +272,7 @@ export function Trace({
|
|
|
234
272
|
</div>
|
|
235
273
|
{open && disclosed.has(entry.id) && (
|
|
236
274
|
<div className="entry-expanded">
|
|
237
|
-
<InlineEntry entry={entry} inspect={select} />
|
|
275
|
+
{active && <InlineEntry entry={entry} inspect={select} />}
|
|
238
276
|
{childCount > 0 && (
|
|
239
277
|
<span className="children-label">
|
|
240
278
|
{childCount} visible child {childCount === 1 ? "entry" : "entries"}
|
|
@@ -254,7 +292,9 @@ export function Trace({
|
|
|
254
292
|
<span>
|
|
255
293
|
{baseDepth
|
|
256
294
|
? `Absolute hierarchy level ${baseDepth + 1}+ · page-local indentation`
|
|
257
|
-
:
|
|
295
|
+
: historyRows
|
|
296
|
+
? "Branch-scoped history · browser-only selection"
|
|
297
|
+
: "Original parents retained · browser-only selection"}
|
|
258
298
|
</span>
|
|
259
299
|
<Flex gap="2" align="center">
|
|
260
300
|
<Button size="1" variant="ghost" disabled={!offset} onClick={() => setOffset(Math.max(0, offset - 50))}>
|
package/src/web/tree.tsx
DELETED
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
import * as Collapsible from "@radix-ui/react-collapsible";
|
|
2
|
-
import {
|
|
3
|
-
ChevronDownIcon,
|
|
4
|
-
ChevronRightIcon,
|
|
5
|
-
MagnifyingGlassIcon,
|
|
6
|
-
MixerHorizontalIcon,
|
|
7
|
-
Share2Icon,
|
|
8
|
-
} from "@radix-ui/react-icons";
|
|
9
|
-
import { Checkbox, Flex, Heading, IconButton, Select, Text, TextField } from "@radix-ui/themes";
|
|
10
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
11
|
-
import type { EntrySummary } from "../model.js";
|
|
12
|
-
import { Glyph } from "./components.js";
|
|
13
|
-
import { eventName, type Filters } from "./format.js";
|
|
14
|
-
import { flatten, hierarchy, label, reveal, withAncestors } from "./hierarchy.js";
|
|
15
|
-
import { scrollWithin } from "./trace.js";
|
|
16
|
-
|
|
17
|
-
function Tree({
|
|
18
|
-
nodes,
|
|
19
|
-
matches,
|
|
20
|
-
selected,
|
|
21
|
-
serial,
|
|
22
|
-
revealSelected,
|
|
23
|
-
choose,
|
|
24
|
-
}: {
|
|
25
|
-
nodes: EntrySummary[];
|
|
26
|
-
matches: Set<string>;
|
|
27
|
-
selected: string;
|
|
28
|
-
serial: number;
|
|
29
|
-
revealSelected: boolean;
|
|
30
|
-
choose(id: string): void;
|
|
31
|
-
}) {
|
|
32
|
-
const tree = useMemo(() => hierarchy(nodes), [nodes]);
|
|
33
|
-
const keep = useMemo(
|
|
34
|
-
() => withAncestors(tree, new Set([...matches, ...(revealSelected && selected ? [selected] : [])])),
|
|
35
|
-
[tree, matches, revealSelected, selected],
|
|
36
|
-
);
|
|
37
|
-
const [expanded, setExpanded] = useState(new Set<string>());
|
|
38
|
-
const [offset, setOffset] = useState(0);
|
|
39
|
-
const previous = useRef<{ selected: string; serial: number; revealSelected: boolean } | undefined>(undefined);
|
|
40
|
-
const nav = useRef<HTMLElement>(null);
|
|
41
|
-
const rows = flatten(tree, expanded, keep);
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
const old = previous.current;
|
|
44
|
-
if (
|
|
45
|
-
!tree.nodes.has(selected) ||
|
|
46
|
-
(old?.selected === selected && old.serial === serial && old.revealSelected === revealSelected)
|
|
47
|
-
)
|
|
48
|
-
return;
|
|
49
|
-
previous.current = { selected, serial, revealSelected };
|
|
50
|
-
const next = reveal(tree, expanded, selected);
|
|
51
|
-
setExpanded(next);
|
|
52
|
-
const index = flatten(tree, next, keep).findIndex((row) => row.node.id === selected);
|
|
53
|
-
if (index >= 0) setOffset(Math.floor(index / 100) * 100);
|
|
54
|
-
}, [selected, serial, revealSelected, tree, expanded, keep]);
|
|
55
|
-
const selectedVisible = rows.slice(offset, offset + 100).some((row) => row.node.id === selected);
|
|
56
|
-
const lastScroll = useRef<{ selected: string; offset: number; element: HTMLElement } | undefined>(undefined);
|
|
57
|
-
useEffect(() => {
|
|
58
|
-
const element = nav.current?.querySelector<HTMLElement>('[aria-current="true"]');
|
|
59
|
-
if (!selectedVisible || !nav.current || !element || element.dataset.entryId !== selected) return;
|
|
60
|
-
const last = lastScroll.current;
|
|
61
|
-
if (last?.selected === selected && last.offset === offset && last.element === element) return;
|
|
62
|
-
lastScroll.current = { selected, offset, element };
|
|
63
|
-
scrollWithin(nav.current, element);
|
|
64
|
-
}, [selected, offset, selectedVisible]);
|
|
65
|
-
useEffect(() => {
|
|
66
|
-
if (offset >= rows.length && offset) setOffset(0);
|
|
67
|
-
}, [offset, rows.length]);
|
|
68
|
-
const pageRows = rows.slice(offset, offset + 100);
|
|
69
|
-
const base = pageRows.length ? Math.min(...pageRows.map((row) => row.depth)) : 0;
|
|
70
|
-
const rail = Math.min(8, 64 / Math.max(1, ...pageRows.map((row) => row.depth - base)));
|
|
71
|
-
return (
|
|
72
|
-
<>
|
|
73
|
-
<nav ref={nav} className="tree-scroll" aria-label="Session branches">
|
|
74
|
-
{pageRows.map(({ node, depth, childCount }) => (
|
|
75
|
-
<div key={node.id} className="tree-row" style={{ paddingLeft: (depth - base) * rail }}>
|
|
76
|
-
{childCount ? (
|
|
77
|
-
<IconButton
|
|
78
|
-
variant="ghost"
|
|
79
|
-
size="1"
|
|
80
|
-
aria-label={`Toggle children of ${node.id}`}
|
|
81
|
-
aria-expanded={expanded.has(node.id)}
|
|
82
|
-
onClick={() =>
|
|
83
|
-
setExpanded((previous) => {
|
|
84
|
-
const next = new Set(previous);
|
|
85
|
-
if (next.has(node.id)) next.delete(node.id);
|
|
86
|
-
else next.add(node.id);
|
|
87
|
-
return next;
|
|
88
|
-
})
|
|
89
|
-
}
|
|
90
|
-
>
|
|
91
|
-
{expanded.has(node.id) ? <ChevronDownIcon /> : <ChevronRightIcon />}
|
|
92
|
-
</IconButton>
|
|
93
|
-
) : (
|
|
94
|
-
<span className="tree-spacer" />
|
|
95
|
-
)}
|
|
96
|
-
<button
|
|
97
|
-
type="button"
|
|
98
|
-
className="node"
|
|
99
|
-
data-entry-id={node.id}
|
|
100
|
-
data-match={matches.has(node.id)}
|
|
101
|
-
aria-current={selected === node.id ? "true" : undefined}
|
|
102
|
-
onClick={() => choose(node.id)}
|
|
103
|
-
title={`${node.kind} · ${node.id}\n${label(node)}${node.nameTruncated ? " [truncated; inspect Raw for the complete captured value]" : ""}`}
|
|
104
|
-
>
|
|
105
|
-
<Glyph kind={node.kind} />
|
|
106
|
-
<span className="node-copy">
|
|
107
|
-
<span className="node-title">
|
|
108
|
-
<strong>{eventName(node.kind)}</strong>
|
|
109
|
-
<small>{node.id.slice(0, 8)}</small>
|
|
110
|
-
</span>
|
|
111
|
-
<span className="node-description">{label(node)}</span>
|
|
112
|
-
</span>
|
|
113
|
-
</button>
|
|
114
|
-
</div>
|
|
115
|
-
))}
|
|
116
|
-
</nav>
|
|
117
|
-
{rows.length > 100 && (
|
|
118
|
-
<div className="nav-pagination">
|
|
119
|
-
<button type="button" disabled={!offset} onClick={() => setOffset(Math.max(0, offset - 100))}>
|
|
120
|
-
Previous nodes
|
|
121
|
-
</button>
|
|
122
|
-
<span>
|
|
123
|
-
{offset + 1}–{Math.min(offset + 100, rows.length)}
|
|
124
|
-
</span>
|
|
125
|
-
<button type="button" disabled={offset + 100 >= rows.length} onClick={() => setOffset(offset + 100)}>
|
|
126
|
-
Next nodes
|
|
127
|
-
</button>
|
|
128
|
-
</div>
|
|
129
|
-
)}
|
|
130
|
-
</>
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
export function Sidebar({
|
|
134
|
-
nodes,
|
|
135
|
-
matches,
|
|
136
|
-
total,
|
|
137
|
-
selected,
|
|
138
|
-
serial,
|
|
139
|
-
revealSelected,
|
|
140
|
-
choose,
|
|
141
|
-
filters,
|
|
142
|
-
change,
|
|
143
|
-
}: {
|
|
144
|
-
nodes: EntrySummary[];
|
|
145
|
-
matches: Set<string>;
|
|
146
|
-
total: number;
|
|
147
|
-
selected: string;
|
|
148
|
-
serial: number;
|
|
149
|
-
revealSelected: boolean;
|
|
150
|
-
choose(id: string): void;
|
|
151
|
-
filters: Filters;
|
|
152
|
-
change(filters: Filters): void;
|
|
153
|
-
}) {
|
|
154
|
-
const toggleGroup = (group: string) =>
|
|
155
|
-
change({
|
|
156
|
-
...filters,
|
|
157
|
-
groups: filters.groups.includes(group)
|
|
158
|
-
? filters.groups.filter((item) => item !== group)
|
|
159
|
-
: [...filters.groups, group],
|
|
160
|
-
});
|
|
161
|
-
return (
|
|
162
|
-
<aside className="sidebar">
|
|
163
|
-
<section className="panel tree-panel">
|
|
164
|
-
<div className="panel-heading">
|
|
165
|
-
<Heading size="3">
|
|
166
|
-
<Share2Icon />
|
|
167
|
-
Session tree
|
|
168
|
-
</Heading>
|
|
169
|
-
<Text size="1" color="gray">
|
|
170
|
-
{total} entries
|
|
171
|
-
</Text>
|
|
172
|
-
</div>
|
|
173
|
-
<div className="tree-controls">
|
|
174
|
-
<TextField.Root
|
|
175
|
-
aria-label="Search session"
|
|
176
|
-
placeholder="Search entries or content…"
|
|
177
|
-
value={filters.query}
|
|
178
|
-
onChange={(event) => change({ ...filters, query: event.target.value })}
|
|
179
|
-
>
|
|
180
|
-
<TextField.Slot>
|
|
181
|
-
<MagnifyingGlassIcon />
|
|
182
|
-
</TextField.Slot>
|
|
183
|
-
</TextField.Root>
|
|
184
|
-
<div className="filter-pills">
|
|
185
|
-
{["All", "Model", "Tool", "Custom"].map((group) => (
|
|
186
|
-
<button
|
|
187
|
-
type="button"
|
|
188
|
-
key={group}
|
|
189
|
-
aria-pressed={
|
|
190
|
-
group === "All" ? !filters.groups.length : filters.groups.length === 1 && filters.groups[0] === group
|
|
191
|
-
}
|
|
192
|
-
onClick={() => change({ ...filters, kind: "all", groups: group === "All" ? [] : [group] })}
|
|
193
|
-
>
|
|
194
|
-
{group}
|
|
195
|
-
</button>
|
|
196
|
-
))}
|
|
197
|
-
</div>
|
|
198
|
-
</div>
|
|
199
|
-
<Tree
|
|
200
|
-
nodes={nodes}
|
|
201
|
-
matches={matches}
|
|
202
|
-
selected={selected}
|
|
203
|
-
serial={serial}
|
|
204
|
-
revealSelected={revealSelected}
|
|
205
|
-
choose={choose}
|
|
206
|
-
/>
|
|
207
|
-
<div className="tree-count">
|
|
208
|
-
{matches.size} matches / {total} entries
|
|
209
|
-
</div>
|
|
210
|
-
</section>
|
|
211
|
-
<Collapsible.Root className="panel filter-panel" defaultOpen={false}>
|
|
212
|
-
<Collapsible.Trigger asChild>
|
|
213
|
-
<button className="filters-toggle" type="button">
|
|
214
|
-
<MixerHorizontalIcon />
|
|
215
|
-
Filters
|
|
216
|
-
<ChevronDownIcon />
|
|
217
|
-
</button>
|
|
218
|
-
</Collapsible.Trigger>
|
|
219
|
-
<Collapsible.Content>
|
|
220
|
-
<label className="filter-check" htmlFor="filter-errors">
|
|
221
|
-
<span>Errors only</span>
|
|
222
|
-
<Checkbox
|
|
223
|
-
id="filter-errors"
|
|
224
|
-
checked={filters.errorsOnly}
|
|
225
|
-
onCheckedChange={(checked) => change({ ...filters, errorsOnly: checked === true })}
|
|
226
|
-
/>
|
|
227
|
-
</label>
|
|
228
|
-
<label className="filter-check" htmlFor="filter-slow">
|
|
229
|
-
<span>Slow tool calls (> 10s)</span>
|
|
230
|
-
<Checkbox
|
|
231
|
-
id="filter-slow"
|
|
232
|
-
checked={filters.slowOnly}
|
|
233
|
-
onCheckedChange={(checked) => change({ ...filters, slowOnly: checked === true })}
|
|
234
|
-
/>
|
|
235
|
-
</label>
|
|
236
|
-
{[
|
|
237
|
-
["Tool", "Tool events"],
|
|
238
|
-
["Model", "Model events"],
|
|
239
|
-
["Custom", "Custom events"],
|
|
240
|
-
].map(([group, label]) => (
|
|
241
|
-
<label className="filter-check" key={group} htmlFor={`filter-${group}`}>
|
|
242
|
-
<span>{label}</span>
|
|
243
|
-
<Checkbox
|
|
244
|
-
id={`filter-${group}`}
|
|
245
|
-
checked={filters.groups.includes(group)}
|
|
246
|
-
onCheckedChange={() => toggleGroup(group)}
|
|
247
|
-
/>
|
|
248
|
-
</label>
|
|
249
|
-
))}
|
|
250
|
-
<Flex justify="between" align="center" gap="2">
|
|
251
|
-
<Text size="1" color="gray">
|
|
252
|
-
Entry type
|
|
253
|
-
</Text>
|
|
254
|
-
<Select.Root value={filters.kind} onValueChange={(kind) => change({ ...filters, kind })}>
|
|
255
|
-
<Select.Trigger aria-label="Filter entry type" variant="soft" />
|
|
256
|
-
<Select.Content>
|
|
257
|
-
{[
|
|
258
|
-
"all",
|
|
259
|
-
"user",
|
|
260
|
-
"assistant",
|
|
261
|
-
"toolResult",
|
|
262
|
-
"system",
|
|
263
|
-
"custom",
|
|
264
|
-
"compaction",
|
|
265
|
-
"branch_summary",
|
|
266
|
-
"model_change",
|
|
267
|
-
"thinking_level_change",
|
|
268
|
-
"context_edit",
|
|
269
|
-
].map((kind) => (
|
|
270
|
-
<Select.Item key={kind} value={kind}>
|
|
271
|
-
{kind}
|
|
272
|
-
</Select.Item>
|
|
273
|
-
))}
|
|
274
|
-
</Select.Content>
|
|
275
|
-
</Select.Root>
|
|
276
|
-
</Flex>
|
|
277
|
-
</Collapsible.Content>
|
|
278
|
-
</Collapsible.Root>
|
|
279
|
-
</aside>
|
|
280
|
-
);
|
|
281
|
-
}
|