@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
|
@@ -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 {
|
|
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
|
|
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(
|
|
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
|
-
<
|
|
79
|
-
|
|
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
|
-
</
|
|
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 *
|
|
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
|
-
<
|
|
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>
|
package/src/web/components.tsx
CHANGED
|
@@ -11,10 +11,10 @@ import {
|
|
|
11
11
|
PersonIcon,
|
|
12
12
|
StackIcon,
|
|
13
13
|
} from "@radix-ui/react-icons";
|
|
14
|
-
import { Badge, Button, Flex
|
|
14
|
+
import { Badge, Button, Flex } from "@radix-ui/themes";
|
|
15
15
|
import { useEffect, useRef, useState } from "react";
|
|
16
|
-
import type {
|
|
17
|
-
import {
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
{mode ===
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
<span className="line
|
|
202
|
-
|
|
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
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
{
|
|
115
|
-
? "
|
|
116
|
-
:
|
|
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}
|
|
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
|
+
}
|