@llblab/pi-actors 0.41.0 → 0.41.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/AGENTS.md +2 -2
- package/BACKLOG.md +0 -19
- package/CHANGELOG.md +7 -0
- package/dist/index.js +51 -8
- package/dist/lib/inspector-overlay.d.ts +11 -0
- package/dist/lib/inspector-overlay.js +321 -75
- package/dist/lib/inspector.js +4 -1
- package/dist/lib/observability.d.ts +38 -3
- package/dist/lib/observability.js +151 -33
- package/dist/lib/tools-response.d.ts +1 -1
- package/dist/lib/tools-response.js +5 -7
- package/dist/skills/actors/SKILL.md +3 -7
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/actor-inspector.md +18 -13
- package/docs/async-runs.md +1 -1
- package/index.ts +49 -8
- package/lib/inspector-overlay.ts +418 -82
- package/lib/inspector.ts +6 -3
- package/lib/observability.ts +205 -39
- package/lib/tools-response.ts +5 -6
- package/package.json +1 -1
- package/skills/actors/SKILL.md +3 -7
- package/skills/swarm/SKILL.md +1 -1
package/lib/inspector-overlay.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
matchesKey,
|
|
13
13
|
truncateToWidth,
|
|
14
14
|
visibleWidth,
|
|
15
|
+
wrapTextWithAnsi,
|
|
15
16
|
type TUI,
|
|
16
17
|
} from "@earendil-works/pi-tui";
|
|
17
18
|
|
|
@@ -19,6 +20,10 @@ import * as Inspector from "./inspector.ts";
|
|
|
19
20
|
|
|
20
21
|
export type ActorInspectorOverlayTab = "communications" | "turns";
|
|
21
22
|
|
|
23
|
+
type DetailLogicalLine = string | { sectionBreak: true };
|
|
24
|
+
|
|
25
|
+
const DETAIL_SECTION_BREAK: DetailLogicalLine = { sectionBreak: true };
|
|
26
|
+
|
|
22
27
|
export interface ActorInspectorOverlayOptions {
|
|
23
28
|
done: () => void;
|
|
24
29
|
ownerId: string;
|
|
@@ -43,6 +48,7 @@ export class ActorInspectorOverlay {
|
|
|
43
48
|
private detailOpen = false;
|
|
44
49
|
private detailScroll = 0;
|
|
45
50
|
private detailTurn?: Inspector.ActorInspectorTurnItem;
|
|
51
|
+
private detailView: "evidence" | "transcript" = "evidence";
|
|
46
52
|
private readonly readKeys = new Set<string>();
|
|
47
53
|
private focus: "runs" | "tabs" | "list" | "detail" | "select" = "tabs";
|
|
48
54
|
private filterControlIndex = 0;
|
|
@@ -76,7 +82,7 @@ export class ActorInspectorOverlay {
|
|
|
76
82
|
this.menuLevel = "filter";
|
|
77
83
|
this.selectorIndex = this.filterControlIndex;
|
|
78
84
|
} else this.focus = this.menuLevel === "run" ? "runs" : "tabs";
|
|
79
|
-
} else if (this.focus === "detail") this.
|
|
85
|
+
} else if (this.focus === "detail") this.backDetail();
|
|
80
86
|
else this.done();
|
|
81
87
|
this.tui.requestRender();
|
|
82
88
|
return;
|
|
@@ -99,12 +105,22 @@ export class ActorInspectorOverlay {
|
|
|
99
105
|
if (this.focus === "detail") {
|
|
100
106
|
if (matchesKey(data, "up")) this.detailScroll = Math.max(0, this.detailScroll - 1);
|
|
101
107
|
else if (matchesKey(data, "down")) this.detailScroll += 1;
|
|
102
|
-
else if (matchesKey(data, "left")) this.
|
|
108
|
+
else if (matchesKey(data, "left")) this.backDetail();
|
|
109
|
+
else if (
|
|
110
|
+
(matchesKey(data, "right") || matchesKey(data, "return")) &&
|
|
111
|
+
this.detailView === "evidence" &&
|
|
112
|
+
this.tab === "turns"
|
|
113
|
+
) {
|
|
114
|
+
this.detailView = "transcript";
|
|
115
|
+
this.detailScroll = 0;
|
|
116
|
+
}
|
|
103
117
|
this.tui.requestRender();
|
|
104
118
|
return;
|
|
105
119
|
}
|
|
106
120
|
if (this.focus === "runs") {
|
|
107
|
-
if (matchesKey(data, "
|
|
121
|
+
if (matchesKey(data, "left")) this.cycleRun(runs, -1);
|
|
122
|
+
else if (matchesKey(data, "right")) this.cycleRun(runs, 1);
|
|
123
|
+
else if (matchesKey(data, "down")) this.focus = "tabs";
|
|
108
124
|
else if (matchesKey(data, "return") && runs.length > 0) this.openRunMenu();
|
|
109
125
|
} else if (this.focus === "tabs") {
|
|
110
126
|
if (matchesKey(data, "left") || matchesKey(data, "right")) {
|
|
@@ -138,8 +154,16 @@ export class ActorInspectorOverlay {
|
|
|
138
154
|
let selector = this.focus === "select"
|
|
139
155
|
? this.menuLevel === "run"
|
|
140
156
|
? this.renderMenuBox(
|
|
141
|
-
this.runs().map(
|
|
157
|
+
this.runs().map(
|
|
158
|
+
(run, index, runs) =>
|
|
159
|
+
`#${runs.length - index} ${run.run} ${run.status}`,
|
|
160
|
+
),
|
|
142
161
|
this.selectorIndex,
|
|
162
|
+
undefined,
|
|
163
|
+
undefined,
|
|
164
|
+
false,
|
|
165
|
+
false,
|
|
166
|
+
this.contentViewportRows(),
|
|
143
167
|
)
|
|
144
168
|
: this.renderFilterMenus(innerWidth)
|
|
145
169
|
: [];
|
|
@@ -175,11 +199,13 @@ export class ActorInspectorOverlay {
|
|
|
175
199
|
: this.selectedRun
|
|
176
200
|
? this.renderTimeline(innerWidth)
|
|
177
201
|
: this.renderRunSelector(innerWidth);
|
|
178
|
-
const
|
|
202
|
+
const contentViewportRows = this.contentViewportRows();
|
|
179
203
|
const selectorRows = selector.slice(1);
|
|
180
|
-
for (let index = 0; index < Math.max(content.length,
|
|
204
|
+
for (let index = 0; index < Math.max(content.length, contentViewportRows); index += 1) {
|
|
181
205
|
const base = content[index] ?? "";
|
|
182
|
-
const stripeIndex =
|
|
206
|
+
const stripeIndex = index < content.length
|
|
207
|
+
? this.contentStripeIndices[index] ?? index
|
|
208
|
+
: 0;
|
|
183
209
|
const popup = selectorRows[index];
|
|
184
210
|
if (!popup) {
|
|
185
211
|
lines.push(this.stripedRow(base, innerWidth, stripeIndex));
|
|
@@ -223,6 +249,33 @@ export class ActorInspectorOverlay {
|
|
|
223
249
|
if (!this.selectedRun) this.selectedRun = runs[0]?.run;
|
|
224
250
|
}
|
|
225
251
|
|
|
252
|
+
private contentViewportRows(): number {
|
|
253
|
+
const overlayRows = Math.floor(this.tui.terminal.rows * 0.94);
|
|
254
|
+
return Math.max(4, Math.min(16, overlayRows - 7));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private selectRun(run: string, index: number): void {
|
|
258
|
+
this.selectedRun = run;
|
|
259
|
+
this.runIndex = index;
|
|
260
|
+
this.rowIndex = 0;
|
|
261
|
+
this.subagentIndex = 0;
|
|
262
|
+
this.communicationFrom = "all";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private cycleRun(
|
|
266
|
+
runs: Inspector.ActorInspectorRunItem[],
|
|
267
|
+
direction: -1 | 1,
|
|
268
|
+
): void {
|
|
269
|
+
if (runs.length === 0) return;
|
|
270
|
+
const currentIndex = Math.max(
|
|
271
|
+
0,
|
|
272
|
+
runs.findIndex((run) => run.run === this.selectedRun),
|
|
273
|
+
);
|
|
274
|
+
const nextIndex = (currentIndex + direction + runs.length) % runs.length;
|
|
275
|
+
const next = runs[nextIndex];
|
|
276
|
+
if (next) this.selectRun(next.run, nextIndex);
|
|
277
|
+
}
|
|
278
|
+
|
|
226
279
|
private listItemCount(): number {
|
|
227
280
|
return this.tab === "communications"
|
|
228
281
|
? this.communicationPreviews().length
|
|
@@ -232,7 +285,7 @@ export class ActorInspectorOverlay {
|
|
|
232
285
|
private selectorAnchor(): number {
|
|
233
286
|
if (this.menuLevel === "run") {
|
|
234
287
|
const runControl = stripVTControlCharacters(this.renderRunControl());
|
|
235
|
-
return Math.max(0, runControl.indexOf("Run:") -
|
|
288
|
+
return Math.max(0, runControl.indexOf("Run:") - 2);
|
|
236
289
|
}
|
|
237
290
|
const label = this.tab === "communications" ? "Messages" : "Turns";
|
|
238
291
|
const tabs = stripVTControlCharacters(this.renderTabs());
|
|
@@ -247,18 +300,24 @@ export class ActorInspectorOverlay {
|
|
|
247
300
|
? ` ${hint("↑↓", "option")} ${hint("enter", "apply")} ${hint("←/esc", "back")}`
|
|
248
301
|
: ` ${hint("↑↓", "option")} ${hint("→/enter", "open")} ${hint("←/esc", "back")}`;
|
|
249
302
|
if (this.focus === "detail")
|
|
250
|
-
return
|
|
303
|
+
return this.tab === "turns" && this.detailView === "evidence"
|
|
304
|
+
? ` ${hint("↑↓", "scroll")} ${hint("→/enter", "readable")} ${hint("←/esc", "back")}`
|
|
305
|
+
: ` ${hint("↑↓", "scroll")} ${hint("←/esc", this.detailView === "transcript" ? "evidence" : "back")}`;
|
|
251
306
|
if (this.focus === "list")
|
|
252
307
|
return ` ${hint("↑↓", "row")} ${hint("→/enter", "open")} ${hint("esc", "close")}`;
|
|
308
|
+
if (this.focus === "runs")
|
|
309
|
+
return ` ${hint("←→", "run")} ${hint("↓", "tabs")} ${hint("enter", "list")} ${hint("esc", "close")}`;
|
|
253
310
|
return ` ${hint("←→", "navigate")} ${hint("↑↓", "change row")} ${hint("enter", "select")} ${hint("esc", "close")}`;
|
|
254
311
|
}
|
|
255
312
|
|
|
256
313
|
private renderRunControl(): string {
|
|
257
|
-
const
|
|
314
|
+
const runs = this.runs();
|
|
315
|
+
const runIndex = runs.findIndex((item) => item.run === this.selectedRun);
|
|
316
|
+
const run = runs[runIndex];
|
|
258
317
|
const focused = this.focus === "runs";
|
|
259
318
|
const active = focused || (this.focus === "select" && this.menuLevel === "run");
|
|
260
|
-
const prefix = active ? this.theme.fg("accent", "
|
|
261
|
-
const suffix = active ? this.theme.fg("accent", "
|
|
319
|
+
const prefix = active ? this.theme.fg("accent", " ← ") : " ";
|
|
320
|
+
const suffix = active ? this.theme.fg("accent", " → ") : " ";
|
|
262
321
|
if (!run) {
|
|
263
322
|
const empty = this.theme.fg("muted", `${prefix}Run: none${suffix}`);
|
|
264
323
|
return focused ? this.theme.bg("selectedBg", empty) : empty;
|
|
@@ -270,7 +329,8 @@ export class ActorInspectorOverlay {
|
|
|
270
329
|
: run.status === "done"
|
|
271
330
|
? "success"
|
|
272
331
|
: "muted";
|
|
273
|
-
const
|
|
332
|
+
const sequence = runs.length - runIndex;
|
|
333
|
+
const value = `${prefix}${this.theme.fg("muted", "Run: ")}${this.theme.fg("text", `#${sequence}`)} ${this.theme.fg("accent", run.run)} ${this.theme.fg(statusColor, run.status)}${suffix}`;
|
|
274
334
|
return focused ? this.theme.bg("selectedBg", value) : value;
|
|
275
335
|
}
|
|
276
336
|
|
|
@@ -297,8 +357,8 @@ export class ActorInspectorOverlay {
|
|
|
297
357
|
};
|
|
298
358
|
const messageFilters = this.activeMessageFilters();
|
|
299
359
|
const subagent = this.subagents()[this.subagentIndex] ?? "all";
|
|
300
|
-
const messagesLabel = `Messages${messageFilters.length > 0 ? `
|
|
301
|
-
const turnsLabel = `Turns${subagent === "all" ? "" : `
|
|
360
|
+
const messagesLabel = `Messages${messageFilters.length > 0 ? ` (${messageFilters.join(", ")})` : ""}`;
|
|
361
|
+
const turnsLabel = `Turns${subagent === "all" ? "" : ` (${subagent})`}`;
|
|
302
362
|
return `${tab("communications", messagesLabel)} ${tab("turns", turnsLabel)}`;
|
|
303
363
|
}
|
|
304
364
|
|
|
@@ -306,10 +366,10 @@ export class ActorInspectorOverlay {
|
|
|
306
366
|
const runs = this.runs();
|
|
307
367
|
if (runs.length === 0) return [this.theme.fg("muted", " No owned actor runs")];
|
|
308
368
|
this.runIndex = Math.min(this.runIndex, runs.length - 1);
|
|
309
|
-
return runs.slice(0,
|
|
369
|
+
return runs.slice(0, this.contentViewportRows()).map((run, index) => {
|
|
310
370
|
const selected = index === this.runIndex;
|
|
311
371
|
const prefix = selected ? this.theme.fg("accent", " ▶ ") : " ";
|
|
312
|
-
const label = `${prefix}run:${run.run}`;
|
|
372
|
+
const label = `${prefix}#${runs.length - index} run:${run.run}`;
|
|
313
373
|
const status = ` ${run.status}`;
|
|
314
374
|
return selected
|
|
315
375
|
? this.theme.fg("accent", `${label}${status}`)
|
|
@@ -337,7 +397,7 @@ export class ActorInspectorOverlay {
|
|
|
337
397
|
this.theme.fg(
|
|
338
398
|
"muted",
|
|
339
399
|
filtered
|
|
340
|
-
? " No results for the active filters
|
|
400
|
+
? " No results for the active filters. Enter on the tab to adjust."
|
|
341
401
|
: this.tab === "communications"
|
|
342
402
|
? " No messages in this run"
|
|
343
403
|
: " No persisted turns in this run",
|
|
@@ -345,12 +405,23 @@ export class ActorInspectorOverlay {
|
|
|
345
405
|
];
|
|
346
406
|
}
|
|
347
407
|
this.rowIndex = Math.min(this.rowIndex, rows.length - 1);
|
|
408
|
+
const viewportRows = this.contentViewportRows();
|
|
348
409
|
const start = Math.max(
|
|
349
410
|
0,
|
|
350
|
-
Math.min(
|
|
411
|
+
Math.min(
|
|
412
|
+
this.rowIndex - Math.floor(viewportRows / 2),
|
|
413
|
+
Math.max(0, rows.length - viewportRows),
|
|
414
|
+
),
|
|
415
|
+
);
|
|
416
|
+
const stripeIndices = this.tab === "communications"
|
|
417
|
+
? this.communicationPreviews().map((preview, index) =>
|
|
418
|
+
Math.max(0, (preview.sequence ?? index + 1) - 1),
|
|
419
|
+
)
|
|
420
|
+
: this.turnItems().map((turn) => Math.max(0, turn.index - 1));
|
|
421
|
+
const visibleRows = rows.slice(start, start + viewportRows);
|
|
422
|
+
this.contentStripeIndices = visibleRows.map(
|
|
423
|
+
(_, offset) => stripeIndices[start + offset] ?? start + offset,
|
|
351
424
|
);
|
|
352
|
-
const visibleRows = rows.slice(start, start + 15);
|
|
353
|
-
this.contentStripeIndices = visibleRows.map((_, offset) => start + offset);
|
|
354
425
|
return visibleRows.map((row, offset) => {
|
|
355
426
|
const index = start + offset;
|
|
356
427
|
const selected = this.focus === "list" && index === this.rowIndex;
|
|
@@ -373,12 +444,14 @@ export class ActorInspectorOverlay {
|
|
|
373
444
|
readKeys: this.readKeys,
|
|
374
445
|
run: this.selectedRun,
|
|
375
446
|
unreadOnly: this.communicationUnread,
|
|
376
|
-
})
|
|
377
|
-
(
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
this.
|
|
381
|
-
|
|
447
|
+
})
|
|
448
|
+
.filter(
|
|
449
|
+
(preview) =>
|
|
450
|
+
this.communicationFrom === "all" ||
|
|
451
|
+
this.communicationActorLabel(preview.from_display ?? preview.from) ===
|
|
452
|
+
this.communicationFrom,
|
|
453
|
+
)
|
|
454
|
+
.reverse();
|
|
382
455
|
}
|
|
383
456
|
|
|
384
457
|
private communicationActorLabel(actor?: string): string {
|
|
@@ -403,7 +476,7 @@ export class ActorInspectorOverlay {
|
|
|
403
476
|
const actor = preview.from_display ?? preview.from ?? "unknown";
|
|
404
477
|
const target = preview.channel === "room" ? "#all" : preview.to;
|
|
405
478
|
const text = preview.summary ?? preview.body_preview ?? "(no body)";
|
|
406
|
-
return
|
|
479
|
+
return `#${preview.sequence ?? "?"} ${preview.needs_response ? "! " : ""}${actor} → ${target} ${preview.type} ${text}`;
|
|
407
480
|
});
|
|
408
481
|
}
|
|
409
482
|
|
|
@@ -435,44 +508,242 @@ export class ActorInspectorOverlay {
|
|
|
435
508
|
}
|
|
436
509
|
const turn = this.detailTurn;
|
|
437
510
|
if (!turn) return [this.theme.fg("warning", " Turn row is no longer available")];
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
511
|
+
const logicalLines = this.detailView === "transcript"
|
|
512
|
+
? this.turnTranscriptLines(turn)
|
|
513
|
+
: this.turnEvidenceLines(turn);
|
|
514
|
+
const wrapped = this.wrapDetailLines(logicalLines, width);
|
|
515
|
+
const viewportRows = this.contentViewportRows();
|
|
516
|
+
const maxStart = Math.max(0, wrapped.lines.length - viewportRows);
|
|
517
|
+
this.detailScroll = Math.min(this.detailScroll, maxStart);
|
|
518
|
+
this.contentStripeIndices = wrapped.stripeIndices.slice(
|
|
519
|
+
this.detailScroll,
|
|
520
|
+
this.detailScroll + viewportRows,
|
|
521
|
+
);
|
|
522
|
+
return wrapped.lines.slice(
|
|
523
|
+
this.detailScroll,
|
|
524
|
+
this.detailScroll + viewportRows,
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private wrapDetailLines(
|
|
529
|
+
lines: DetailLogicalLine[],
|
|
530
|
+
width: number,
|
|
531
|
+
): { lines: string[]; stripeIndices: number[] } {
|
|
532
|
+
const wrappedLines: string[] = [];
|
|
533
|
+
const stripeIndices: number[] = [];
|
|
534
|
+
let sectionIndex = 0;
|
|
535
|
+
for (const line of lines) {
|
|
536
|
+
if (typeof line !== "string") {
|
|
537
|
+
sectionIndex += 1;
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
const physicalLines = line.replaceAll("\t", " ").split(/\r?\n/);
|
|
541
|
+
for (const physicalLine of physicalLines) {
|
|
542
|
+
if (!physicalLine.trim()) continue;
|
|
543
|
+
const leading = physicalLine.match(/^ */)?.[0] ?? "";
|
|
544
|
+
const content = physicalLine.slice(leading.length);
|
|
545
|
+
const contentWidth = Math.max(1, width - visibleWidth(leading));
|
|
546
|
+
const wrapped = visibleWidth(physicalLine) <= width
|
|
547
|
+
? [physicalLine]
|
|
548
|
+
: wrapTextWithAnsi(content, contentWidth).map(
|
|
549
|
+
(visualLine) => `${leading}${visualLine}`,
|
|
550
|
+
);
|
|
551
|
+
for (const visualLine of wrapped) {
|
|
552
|
+
if (!visualLine.trim()) continue;
|
|
553
|
+
wrappedLines.push(visualLine);
|
|
554
|
+
stripeIndices.push(sectionIndex);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return { lines: wrappedLines, stripeIndices };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private detailSection(
|
|
562
|
+
title: string,
|
|
563
|
+
content: string[],
|
|
564
|
+
): DetailLogicalLine[] {
|
|
565
|
+
if (content.length === 0) return [];
|
|
566
|
+
return [
|
|
567
|
+
` ${title}`,
|
|
568
|
+
...content.flatMap((line) =>
|
|
569
|
+
line
|
|
570
|
+
.split(/\r?\n/)
|
|
571
|
+
.filter((physicalLine) => physicalLine.trim().length > 0)
|
|
572
|
+
.map((physicalLine) => ` ${physicalLine}`),
|
|
573
|
+
),
|
|
574
|
+
DETAIL_SECTION_BREAK,
|
|
575
|
+
];
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
private readableValueLines(
|
|
579
|
+
value: unknown,
|
|
580
|
+
indent = "",
|
|
581
|
+
depth = 0,
|
|
582
|
+
): string[] {
|
|
583
|
+
if (depth > 8) return [`${indent}[nested value]`];
|
|
584
|
+
if (typeof value === "string") {
|
|
585
|
+
const trimmed = value.trim();
|
|
586
|
+
if (/^[{[]/.test(trimmed)) {
|
|
587
|
+
try {
|
|
588
|
+
return this.readableValueLines(JSON.parse(trimmed), indent, depth + 1);
|
|
589
|
+
} catch {
|
|
590
|
+
// Keep non-JSON text as written.
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return value.split(/\r?\n/).map((line) => `${indent}${line}`);
|
|
594
|
+
}
|
|
595
|
+
if (value === null || value === undefined)
|
|
596
|
+
return [`${indent}${String(value)}`];
|
|
597
|
+
if (typeof value !== "object") return [`${indent}${String(value)}`];
|
|
598
|
+
if (Array.isArray(value)) {
|
|
599
|
+
if (value.length === 0) return [`${indent}(empty)`];
|
|
600
|
+
return value.flatMap((item) => {
|
|
601
|
+
if (
|
|
602
|
+
item &&
|
|
603
|
+
typeof item === "object" &&
|
|
604
|
+
(item as { type?: unknown }).type === "text" &&
|
|
605
|
+
typeof (item as { text?: unknown }).text === "string"
|
|
606
|
+
) {
|
|
607
|
+
return this.readableValueLines(
|
|
608
|
+
(item as { text: string }).text,
|
|
609
|
+
indent,
|
|
610
|
+
depth + 1,
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
if (!item || typeof item !== "object")
|
|
614
|
+
return [`${indent}- ${String(item)}`];
|
|
615
|
+
return [
|
|
616
|
+
`${indent}-`,
|
|
617
|
+
...this.readableValueLines(item, `${indent} `, depth + 1),
|
|
618
|
+
];
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
622
|
+
if (entries.length === 0) return [`${indent}(empty)`];
|
|
623
|
+
return entries.flatMap(([key, item]) => {
|
|
624
|
+
if (!item || typeof item !== "object") {
|
|
625
|
+
const scalar = typeof item === "string" ? item : String(item);
|
|
626
|
+
const [first = "", ...rest] = scalar.split(/\r?\n/);
|
|
627
|
+
return [
|
|
628
|
+
`${indent}${key}: ${first}`,
|
|
629
|
+
...rest.map((line) => `${indent} ${line}`),
|
|
630
|
+
];
|
|
631
|
+
}
|
|
632
|
+
return [
|
|
633
|
+
`${indent}${key}:`,
|
|
634
|
+
...this.readableValueLines(item, `${indent} `, depth + 1),
|
|
635
|
+
];
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private turnEvidenceLines(
|
|
640
|
+
turn: Inspector.ActorInspectorTurnItem,
|
|
641
|
+
): DetailLogicalLine[] {
|
|
642
|
+
const lines: DetailLogicalLine[] = [];
|
|
643
|
+
const commandNumber = /^command-(\d+)$/.exec(turn.commandId)?.[1];
|
|
644
|
+
const subagent = commandNumber
|
|
645
|
+
? String(Number.parseInt(commandNumber, 10))
|
|
646
|
+
: turn.commandId;
|
|
647
|
+
const stage =
|
|
648
|
+
turn.stage && turn.stage !== "subagent" && turn.stage !== "command"
|
|
649
|
+
? ` (${turn.stage})`
|
|
650
|
+
: "";
|
|
651
|
+
lines.push(` Subagent ${subagent}${stage}`, DETAIL_SECTION_BREAK);
|
|
652
|
+
if (turn.userText)
|
|
653
|
+
lines.push(...this.detailSection("User", [turn.userText]));
|
|
654
|
+
if (turn.thinking)
|
|
655
|
+
lines.push(...this.detailSection("Thinking", [turn.thinking]));
|
|
656
|
+
if (turn.assistantText)
|
|
657
|
+
lines.push(...this.detailSection("Assistant", [turn.assistantText]));
|
|
658
|
+
for (const [index, tool] of turn.toolCalls.entries()) {
|
|
659
|
+
const content = [
|
|
660
|
+
...(tool.arguments === undefined
|
|
661
|
+
? []
|
|
662
|
+
: ["Input", ...this.readableValueLines(tool.arguments, " ")]),
|
|
663
|
+
...(tool.result === undefined
|
|
664
|
+
? []
|
|
665
|
+
: ["Result", ...this.readableValueLines(tool.result, " ")]),
|
|
666
|
+
`call id: ${tool.id}`,
|
|
667
|
+
];
|
|
668
|
+
lines.push(
|
|
669
|
+
...this.detailSection(
|
|
670
|
+
`Tool ${index + 1} (${tool.name}${tool.resultError ? ", Error" : ""})`,
|
|
671
|
+
content,
|
|
672
|
+
),
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
const execution = [
|
|
676
|
+
...(!turn.thinking ? ["thinking: not persisted"] : []),
|
|
677
|
+
...(turn.model ? [`model: ${turn.model}`] : []),
|
|
678
|
+
...(turn.stopReason ? [`stop reason: ${turn.stopReason}`] : []),
|
|
679
|
+
...(turn.error ? [`error: ${turn.error}`] : []),
|
|
680
|
+
...(turn.usage === undefined
|
|
681
|
+
? []
|
|
682
|
+
: ["Usage", ...this.readableValueLines(turn.usage, " ")]),
|
|
683
|
+
...(turn.unmatchedToolResults > 0
|
|
684
|
+
? [`unmatched tool results: ${turn.unmatchedToolResults}`]
|
|
445
685
|
: []),
|
|
446
|
-
|
|
447
|
-
|
|
686
|
+
];
|
|
687
|
+
if (execution.length > 0)
|
|
688
|
+
lines.push(...this.detailSection("Execution", execution));
|
|
689
|
+
if (turn.diagnostics.length > 0)
|
|
690
|
+
lines.push(...this.detailSection("Diagnostics", turn.diagnostics));
|
|
691
|
+
const provenance = [
|
|
692
|
+
"Session",
|
|
693
|
+
` ${turn.sessionFile}`,
|
|
694
|
+
...(turn.sessionTruncated ? [" turn list truncated"] : []),
|
|
695
|
+
...(turn.promptFile
|
|
696
|
+
? [
|
|
697
|
+
"Prompt",
|
|
698
|
+
` ${turn.promptFile}${turn.promptBytes ? ` (${turn.promptBytes} bytes)` : ""}`,
|
|
699
|
+
]
|
|
448
700
|
: []),
|
|
449
|
-
...(turn.
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
...(turn.error ? [` error ${turn.error}`] : []),
|
|
456
|
-
` tools ${turn.toolCalls.length}`,
|
|
701
|
+
...(turn.recipeContext === undefined
|
|
702
|
+
? []
|
|
703
|
+
: [
|
|
704
|
+
"Recipe Context",
|
|
705
|
+
...this.readableValueLines(turn.recipeContext, " "),
|
|
706
|
+
]),
|
|
457
707
|
];
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
708
|
+
lines.push(...this.detailSection("Provenance", provenance));
|
|
709
|
+
return lines.length > 0 ? lines : [" No persisted turn evidence"];
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
private readablePromptText(text: string): string {
|
|
713
|
+
const wrapped = /^\s*<file\b[^>]*>\s*([\s\S]*?)\s*<\/file>\s*$/i.exec(text);
|
|
714
|
+
return wrapped?.[1]?.trim() || text;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
private turnTranscriptLines(
|
|
718
|
+
turn: Inspector.ActorInspectorTurnItem,
|
|
719
|
+
): DetailLogicalLine[] {
|
|
720
|
+
const lines: DetailLogicalLine[] = [];
|
|
721
|
+
if (turn.userText)
|
|
722
|
+
lines.push(
|
|
723
|
+
...this.detailSection("User", [this.readablePromptText(turn.userText)]),
|
|
724
|
+
);
|
|
725
|
+
if (turn.thinking)
|
|
726
|
+
lines.push(...this.detailSection("Thinking", [turn.thinking]));
|
|
727
|
+
if (turn.assistantText)
|
|
728
|
+
lines.push(...this.detailSection("Assistant", [turn.assistantText]));
|
|
729
|
+
for (const tool of turn.toolCalls) {
|
|
730
|
+
const content = [
|
|
731
|
+
...(tool.arguments === undefined
|
|
732
|
+
? []
|
|
733
|
+
: ["Input", ...this.readableValueLines(tool.arguments, " ")]),
|
|
734
|
+
...(tool.result === undefined
|
|
735
|
+
? []
|
|
736
|
+
: ["Result", ...this.readableValueLines(tool.result, " ")]),
|
|
737
|
+
];
|
|
738
|
+
lines.push(
|
|
739
|
+
...this.detailSection(
|
|
740
|
+
`Tool (${tool.name}${tool.resultError ? ", Error" : ""})`,
|
|
741
|
+
content,
|
|
742
|
+
),
|
|
461
743
|
);
|
|
462
|
-
if (tool.arguments !== undefined)
|
|
463
|
-
fields.push(` arguments ${value(tool.arguments)}`);
|
|
464
|
-
if (tool.result !== undefined) fields.push(` result ${value(tool.result)}`);
|
|
465
744
|
}
|
|
466
|
-
if (turn.
|
|
467
|
-
|
|
468
|
-
if (turn.sessionTruncated) fields.push(" session turn list truncated");
|
|
469
|
-
for (const diagnostic of turn.diagnostics)
|
|
470
|
-
fields.push(` diagnostic ${diagnostic}`);
|
|
471
|
-
const maxStart = Math.max(0, fields.length - 16);
|
|
472
|
-
this.detailScroll = Math.min(this.detailScroll, maxStart);
|
|
473
|
-
return fields
|
|
474
|
-
.slice(this.detailScroll, this.detailScroll + 16)
|
|
475
|
-
.map((field) => truncateToWidth(field, width, "…"));
|
|
745
|
+
if (turn.error) lines.push(...this.detailSection("Error", [turn.error]));
|
|
746
|
+
return lines.length > 0 ? lines : [" No readable turn content"];
|
|
476
747
|
}
|
|
477
748
|
|
|
478
749
|
private turnItems(): Inspector.ActorInspectorTurnItem[] {
|
|
@@ -480,19 +751,44 @@ export class ActorInspectorOverlay {
|
|
|
480
751
|
const selectedSubagent = this.subagents()[this.subagentIndex];
|
|
481
752
|
return Inspector.readActorInspectorTurns(
|
|
482
753
|
path.join(this.stateRoot, this.selectedRun),
|
|
483
|
-
)
|
|
484
|
-
(
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
754
|
+
)
|
|
755
|
+
.filter(
|
|
756
|
+
(turn) =>
|
|
757
|
+
!selectedSubagent ||
|
|
758
|
+
selectedSubagent === "all" ||
|
|
759
|
+
turn.commandId === selectedSubagent,
|
|
760
|
+
)
|
|
761
|
+
.reverse();
|
|
489
762
|
}
|
|
490
763
|
|
|
491
764
|
private turnRows(): string[] {
|
|
492
|
-
return this.turnItems().map(
|
|
493
|
-
(turn)
|
|
494
|
-
|
|
495
|
-
|
|
765
|
+
return this.turnItems().map((turn) => {
|
|
766
|
+
const commandNumber = /^command-(\d+)$/.exec(turn.commandId)?.[1];
|
|
767
|
+
const subagent = commandNumber
|
|
768
|
+
? `Subagent ${Number.parseInt(commandNumber, 10)}`
|
|
769
|
+
: `Subagent ${turn.commandId}`;
|
|
770
|
+
const stage =
|
|
771
|
+
turn.stage && turn.stage !== "subagent" && turn.stage !== "command"
|
|
772
|
+
? ` (${turn.stage})`
|
|
773
|
+
: "";
|
|
774
|
+
const toolNames = [...new Set(turn.toolCalls.map((tool) => tool.name))];
|
|
775
|
+
const action = turn.toolCalls.length === 0
|
|
776
|
+
? ""
|
|
777
|
+
: turn.toolCalls.length === 1
|
|
778
|
+
? ` (${toolNames[0]})`
|
|
779
|
+
: toolNames.length === 1
|
|
780
|
+
? ` (${toolNames[0]}, ${turn.toolCalls.length} calls)`
|
|
781
|
+
: turn.toolCalls.length <= 2
|
|
782
|
+
? ` (${toolNames.join(", ")})`
|
|
783
|
+
: ` (${turn.toolCalls.length} tools)`;
|
|
784
|
+
const failed = turn.error || turn.toolCalls.some((tool) => tool.resultError)
|
|
785
|
+
? " (error)"
|
|
786
|
+
: "";
|
|
787
|
+
const preview = this.readablePromptText(
|
|
788
|
+
turn.assistantText ?? turn.userText ?? "(no text)",
|
|
789
|
+
).replaceAll(/\s+/g, " ").trim();
|
|
790
|
+
return `#${turn.index} ${subagent}${stage} ${turn.model ?? "unknown model"}${action}${failed} ${preview}`;
|
|
791
|
+
});
|
|
496
792
|
}
|
|
497
793
|
|
|
498
794
|
private subagents(): string[] {
|
|
@@ -548,11 +844,10 @@ export class ActorInspectorOverlay {
|
|
|
548
844
|
private applyMenuOption(value?: string): void {
|
|
549
845
|
if (!value) return;
|
|
550
846
|
if (this.menuLevel === "run") {
|
|
551
|
-
this.
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
this.communicationFrom = "all";
|
|
847
|
+
this.selectRun(
|
|
848
|
+
value,
|
|
849
|
+
Math.max(0, this.runs().findIndex((run) => run.run === value)),
|
|
850
|
+
);
|
|
556
851
|
this.focus = "runs";
|
|
557
852
|
return;
|
|
558
853
|
}
|
|
@@ -594,16 +889,37 @@ export class ActorInspectorOverlay {
|
|
|
594
889
|
parentIndex?: number,
|
|
595
890
|
omitLeftBorder = false,
|
|
596
891
|
omitRightBorder = false,
|
|
892
|
+
maxVisibleOptions = options.length,
|
|
597
893
|
): string[] {
|
|
598
|
-
const
|
|
599
|
-
const
|
|
894
|
+
const visibleLimit = Math.max(1, Math.min(maxVisibleOptions, options.length));
|
|
895
|
+
const anchorIndex = Math.max(
|
|
896
|
+
0,
|
|
897
|
+
Math.min(
|
|
898
|
+
options.length - 1,
|
|
899
|
+
focusedIndex >= 0 ? focusedIndex : (parentIndex ?? 0),
|
|
900
|
+
),
|
|
901
|
+
);
|
|
902
|
+
const maxStart = Math.max(0, options.length - visibleLimit);
|
|
903
|
+
const start = Math.max(
|
|
904
|
+
0,
|
|
905
|
+
Math.min(anchorIndex - Math.floor(visibleLimit / 2), maxStart),
|
|
906
|
+
);
|
|
907
|
+
const visibleOptions = options.slice(start, start + visibleLimit);
|
|
908
|
+
const hiddenAbove = start > 0;
|
|
909
|
+
const hiddenBelow = start + visibleOptions.length < options.length;
|
|
910
|
+
const contentWidth = Math.max(
|
|
911
|
+
4,
|
|
912
|
+
...options.map((option) => visibleWidth(option) + 4),
|
|
913
|
+
);
|
|
914
|
+
const border = (left: string, right: string, marker = "") =>
|
|
600
915
|
this.theme.fg(
|
|
601
916
|
"border",
|
|
602
|
-
`${omitLeftBorder ? "" : left}${"─".repeat(contentWidth)}${omitRightBorder ? "" : right}`,
|
|
917
|
+
`${omitLeftBorder ? "" : left}${marker}${"─".repeat(Math.max(0, contentWidth - visibleWidth(marker)))}${omitRightBorder ? "" : right}`,
|
|
603
918
|
);
|
|
604
919
|
return [
|
|
605
|
-
border("╭", "╮"),
|
|
606
|
-
...
|
|
920
|
+
border("╭", "╮", hiddenAbove ? "↑" : ""),
|
|
921
|
+
...visibleOptions.map((option, localIndex) => {
|
|
922
|
+
const index = start + localIndex;
|
|
607
923
|
const focused = index === focusedIndex;
|
|
608
924
|
const row = this.fit(`${focused ? " ▶ " : " "}${option}`, contentWidth);
|
|
609
925
|
const colored = focused || option === current || index === parentIndex
|
|
@@ -614,7 +930,7 @@ export class ActorInspectorOverlay {
|
|
|
614
930
|
: colored;
|
|
615
931
|
return `${omitLeftBorder ? "" : this.theme.fg("border", "│")}${styled}${omitRightBorder ? "" : this.theme.fg("border", "│")}`;
|
|
616
932
|
}),
|
|
617
|
-
border("╰", "╯"),
|
|
933
|
+
border("╰", "╯", hiddenBelow ? "↓" : ""),
|
|
618
934
|
];
|
|
619
935
|
}
|
|
620
936
|
|
|
@@ -622,12 +938,16 @@ export class ActorInspectorOverlay {
|
|
|
622
938
|
const filters = this.filterOptions().map(
|
|
623
939
|
(filter, index) => `${filter}: ${this.filterValue(index)}`,
|
|
624
940
|
);
|
|
941
|
+
const maxVisibleOptions = Math.max(1, this.contentViewportRows() - 1);
|
|
625
942
|
const parentFocus = this.menuLevel === "filter" ? this.selectorIndex : -1;
|
|
626
943
|
const parent = this.renderMenuBox(
|
|
627
944
|
filters,
|
|
628
945
|
parentFocus,
|
|
629
946
|
undefined,
|
|
630
947
|
this.menuLevel === "value" ? this.filterControlIndex : undefined,
|
|
948
|
+
false,
|
|
949
|
+
false,
|
|
950
|
+
maxVisibleOptions,
|
|
631
951
|
);
|
|
632
952
|
if (this.menuLevel !== "value") return parent;
|
|
633
953
|
const parentShared = this.renderMenuBox(
|
|
@@ -637,11 +957,16 @@ export class ActorInspectorOverlay {
|
|
|
637
957
|
this.filterControlIndex,
|
|
638
958
|
false,
|
|
639
959
|
true,
|
|
960
|
+
maxVisibleOptions,
|
|
640
961
|
);
|
|
641
962
|
const child = this.renderMenuBox(
|
|
642
963
|
this.valueOptions(),
|
|
643
964
|
this.selectorIndex,
|
|
644
965
|
this.currentFilterValue(),
|
|
966
|
+
undefined,
|
|
967
|
+
false,
|
|
968
|
+
false,
|
|
969
|
+
maxVisibleOptions,
|
|
645
970
|
);
|
|
646
971
|
return Array.from({ length: Math.max(parent.length, child.length) }, (_, index) =>
|
|
647
972
|
child[index]
|
|
@@ -657,15 +982,26 @@ export class ActorInspectorOverlay {
|
|
|
657
982
|
if (preview) this.readKeys.add(Inspector.inspectorPreviewReadKey(preview));
|
|
658
983
|
} else this.detailTurn = this.turnItems()[this.rowIndex];
|
|
659
984
|
this.detailScroll = 0;
|
|
985
|
+
this.detailView = "evidence";
|
|
660
986
|
this.detailOpen = Boolean(this.detailCommunication || this.detailTurn);
|
|
661
987
|
if (this.detailOpen) this.focus = "detail";
|
|
662
988
|
}
|
|
663
989
|
|
|
990
|
+
private backDetail(): void {
|
|
991
|
+
if (this.detailView === "transcript") {
|
|
992
|
+
this.detailView = "evidence";
|
|
993
|
+
this.detailScroll = 0;
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
this.closeDetail();
|
|
997
|
+
}
|
|
998
|
+
|
|
664
999
|
private closeDetail(): void {
|
|
665
1000
|
this.detailOpen = false;
|
|
666
1001
|
this.detailCommunication = undefined;
|
|
667
1002
|
this.detailTurn = undefined;
|
|
668
1003
|
this.detailScroll = 0;
|
|
1004
|
+
this.detailView = "evidence";
|
|
669
1005
|
this.focus = "list";
|
|
670
1006
|
}
|
|
671
1007
|
|