@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.
@@ -5,8 +5,9 @@
5
5
  */
6
6
  import * as path from "node:path";
7
7
  import { stripVTControlCharacters } from "node:util";
8
- import { matchesKey, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
8
+ import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
9
9
  import * as Inspector from "./inspector.js";
10
+ const DETAIL_SECTION_BREAK = { sectionBreak: true };
10
11
  export class ActorInspectorOverlay {
11
12
  done;
12
13
  ownerId;
@@ -23,6 +24,7 @@ export class ActorInspectorOverlay {
23
24
  detailOpen = false;
24
25
  detailScroll = 0;
25
26
  detailTurn;
27
+ detailView = "evidence";
26
28
  readKeys = new Set();
27
29
  focus = "tabs";
28
30
  filterControlIndex = 0;
@@ -58,7 +60,7 @@ export class ActorInspectorOverlay {
58
60
  this.focus = this.menuLevel === "run" ? "runs" : "tabs";
59
61
  }
60
62
  else if (this.focus === "detail")
61
- this.closeDetail();
63
+ this.backDetail();
62
64
  else
63
65
  this.done();
64
66
  this.tui.requestRender();
@@ -84,12 +86,22 @@ export class ActorInspectorOverlay {
84
86
  else if (matchesKey(data, "down"))
85
87
  this.detailScroll += 1;
86
88
  else if (matchesKey(data, "left"))
87
- this.closeDetail();
89
+ this.backDetail();
90
+ else if ((matchesKey(data, "right") || matchesKey(data, "return")) &&
91
+ this.detailView === "evidence" &&
92
+ this.tab === "turns") {
93
+ this.detailView = "transcript";
94
+ this.detailScroll = 0;
95
+ }
88
96
  this.tui.requestRender();
89
97
  return;
90
98
  }
91
99
  if (this.focus === "runs") {
92
- if (matchesKey(data, "down"))
100
+ if (matchesKey(data, "left"))
101
+ this.cycleRun(runs, -1);
102
+ else if (matchesKey(data, "right"))
103
+ this.cycleRun(runs, 1);
104
+ else if (matchesKey(data, "down"))
93
105
  this.focus = "tabs";
94
106
  else if (matchesKey(data, "return") && runs.length > 0)
95
107
  this.openRunMenu();
@@ -131,7 +143,7 @@ export class ActorInspectorOverlay {
131
143
  lines.push(this.row(this.renderRunControl(), innerWidth));
132
144
  let selector = this.focus === "select"
133
145
  ? this.menuLevel === "run"
134
- ? this.renderMenuBox(this.runs().map((run) => `${run.run} · ${run.status}`), this.selectorIndex)
146
+ ? this.renderMenuBox(this.runs().map((run, index, runs) => `#${runs.length - index} ${run.run} ${run.status}`), this.selectorIndex, undefined, undefined, false, false, this.contentViewportRows())
135
147
  : this.renderFilterMenus(innerWidth)
136
148
  : [];
137
149
  const selectorAnchor = this.selectorAnchor();
@@ -160,11 +172,13 @@ export class ActorInspectorOverlay {
160
172
  : this.selectedRun
161
173
  ? this.renderTimeline(innerWidth)
162
174
  : this.renderRunSelector(innerWidth);
163
- const minContentRows = 16;
175
+ const contentViewportRows = this.contentViewportRows();
164
176
  const selectorRows = selector.slice(1);
165
- for (let index = 0; index < Math.max(content.length, minContentRows); index += 1) {
177
+ for (let index = 0; index < Math.max(content.length, contentViewportRows); index += 1) {
166
178
  const base = content[index] ?? "";
167
- const stripeIndex = this.contentStripeIndices[index] ?? index;
179
+ const stripeIndex = index < content.length
180
+ ? this.contentStripeIndices[index] ?? index
181
+ : 0;
168
182
  const popup = selectorRows[index];
169
183
  if (!popup) {
170
184
  lines.push(this.stripedRow(base, innerWidth, stripeIndex));
@@ -195,6 +209,26 @@ export class ActorInspectorOverlay {
195
209
  if (!this.selectedRun)
196
210
  this.selectedRun = runs[0]?.run;
197
211
  }
212
+ contentViewportRows() {
213
+ const overlayRows = Math.floor(this.tui.terminal.rows * 0.94);
214
+ return Math.max(4, Math.min(16, overlayRows - 7));
215
+ }
216
+ selectRun(run, index) {
217
+ this.selectedRun = run;
218
+ this.runIndex = index;
219
+ this.rowIndex = 0;
220
+ this.subagentIndex = 0;
221
+ this.communicationFrom = "all";
222
+ }
223
+ cycleRun(runs, direction) {
224
+ if (runs.length === 0)
225
+ return;
226
+ const currentIndex = Math.max(0, runs.findIndex((run) => run.run === this.selectedRun));
227
+ const nextIndex = (currentIndex + direction + runs.length) % runs.length;
228
+ const next = runs[nextIndex];
229
+ if (next)
230
+ this.selectRun(next.run, nextIndex);
231
+ }
198
232
  listItemCount() {
199
233
  return this.tab === "communications"
200
234
  ? this.communicationPreviews().length
@@ -203,7 +237,7 @@ export class ActorInspectorOverlay {
203
237
  selectorAnchor() {
204
238
  if (this.menuLevel === "run") {
205
239
  const runControl = stripVTControlCharacters(this.renderRunControl());
206
- return Math.max(0, runControl.indexOf("Run:") - 1);
240
+ return Math.max(0, runControl.indexOf("Run:") - 2);
207
241
  }
208
242
  const label = this.tab === "communications" ? "Messages" : "Turns";
209
243
  const tabs = stripVTControlCharacters(this.renderTabs());
@@ -216,17 +250,23 @@ export class ActorInspectorOverlay {
216
250
  ? ` ${hint("↑↓", "option")} ${hint("enter", "apply")} ${hint("←/esc", "back")}`
217
251
  : ` ${hint("↑↓", "option")} ${hint("→/enter", "open")} ${hint("←/esc", "back")}`;
218
252
  if (this.focus === "detail")
219
- return ` ${hint("↑↓", "scroll")} ${hint("←/esc", "back")}`;
253
+ return this.tab === "turns" && this.detailView === "evidence"
254
+ ? ` ${hint("↑↓", "scroll")} ${hint("→/enter", "readable")} ${hint("←/esc", "back")}`
255
+ : ` ${hint("↑↓", "scroll")} ${hint("←/esc", this.detailView === "transcript" ? "evidence" : "back")}`;
220
256
  if (this.focus === "list")
221
257
  return ` ${hint("↑↓", "row")} ${hint("→/enter", "open")} ${hint("esc", "close")}`;
258
+ if (this.focus === "runs")
259
+ return ` ${hint("←→", "run")} ${hint("↓", "tabs")} ${hint("enter", "list")} ${hint("esc", "close")}`;
222
260
  return ` ${hint("←→", "navigate")} ${hint("↑↓", "change row")} ${hint("enter", "select")} ${hint("esc", "close")}`;
223
261
  }
224
262
  renderRunControl() {
225
- const run = this.runs().find((item) => item.run === this.selectedRun);
263
+ const runs = this.runs();
264
+ const runIndex = runs.findIndex((item) => item.run === this.selectedRun);
265
+ const run = runs[runIndex];
226
266
  const focused = this.focus === "runs";
227
267
  const active = focused || (this.focus === "select" && this.menuLevel === "run");
228
- const prefix = active ? this.theme.fg("accent", " [ ") : " ";
229
- const suffix = active ? this.theme.fg("accent", " ] ") : " ";
268
+ const prefix = active ? this.theme.fg("accent", " ← ") : " ";
269
+ const suffix = active ? this.theme.fg("accent", " → ") : " ";
230
270
  if (!run) {
231
271
  const empty = this.theme.fg("muted", `${prefix}Run: none${suffix}`);
232
272
  return focused ? this.theme.bg("selectedBg", empty) : empty;
@@ -238,7 +278,8 @@ export class ActorInspectorOverlay {
238
278
  : run.status === "done"
239
279
  ? "success"
240
280
  : "muted";
241
- const value = `${prefix}${this.theme.fg("muted", "Run: ")}${this.theme.fg("accent", run.run)}${this.theme.fg("muted", " · ")}${this.theme.fg(statusColor, run.status)}${suffix}`;
281
+ const sequence = runs.length - runIndex;
282
+ 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}`;
242
283
  return focused ? this.theme.bg("selectedBg", value) : value;
243
284
  }
244
285
  activeMessageFilters() {
@@ -263,8 +304,8 @@ export class ActorInspectorOverlay {
263
304
  };
264
305
  const messageFilters = this.activeMessageFilters();
265
306
  const subagent = this.subagents()[this.subagentIndex] ?? "all";
266
- const messagesLabel = `Messages${messageFilters.length > 0 ? ` · ${messageFilters.join(" · ")}` : ""}`;
267
- const turnsLabel = `Turns${subagent === "all" ? "" : ` · ${subagent}`}`;
307
+ const messagesLabel = `Messages${messageFilters.length > 0 ? ` (${messageFilters.join(", ")})` : ""}`;
308
+ const turnsLabel = `Turns${subagent === "all" ? "" : ` (${subagent})`}`;
268
309
  return `${tab("communications", messagesLabel)} ${tab("turns", turnsLabel)}`;
269
310
  }
270
311
  renderRunSelector(width) {
@@ -272,10 +313,10 @@ export class ActorInspectorOverlay {
272
313
  if (runs.length === 0)
273
314
  return [this.theme.fg("muted", " No owned actor runs")];
274
315
  this.runIndex = Math.min(this.runIndex, runs.length - 1);
275
- return runs.slice(0, 16).map((run, index) => {
316
+ return runs.slice(0, this.contentViewportRows()).map((run, index) => {
276
317
  const selected = index === this.runIndex;
277
318
  const prefix = selected ? this.theme.fg("accent", " ▶ ") : " ";
278
- const label = `${prefix}run:${run.run}`;
319
+ const label = `${prefix}#${runs.length - index} run:${run.run}`;
279
320
  const status = ` ${run.status}`;
280
321
  return selected
281
322
  ? this.theme.fg("accent", `${label}${status}`)
@@ -300,16 +341,20 @@ export class ActorInspectorOverlay {
300
341
  : (this.subagents()[this.subagentIndex] ?? "all") !== "all";
301
342
  return [
302
343
  this.theme.fg("muted", filtered
303
- ? " No results for the active filters · Enter on the tab to adjust"
344
+ ? " No results for the active filters. Enter on the tab to adjust."
304
345
  : this.tab === "communications"
305
346
  ? " No messages in this run"
306
347
  : " No persisted turns in this run"),
307
348
  ];
308
349
  }
309
350
  this.rowIndex = Math.min(this.rowIndex, rows.length - 1);
310
- const start = Math.max(0, Math.min(this.rowIndex - 7, Math.max(0, rows.length - 15)));
311
- const visibleRows = rows.slice(start, start + 15);
312
- this.contentStripeIndices = visibleRows.map((_, offset) => start + offset);
351
+ const viewportRows = this.contentViewportRows();
352
+ const start = Math.max(0, Math.min(this.rowIndex - Math.floor(viewportRows / 2), Math.max(0, rows.length - viewportRows)));
353
+ const stripeIndices = this.tab === "communications"
354
+ ? this.communicationPreviews().map((preview, index) => Math.max(0, (preview.sequence ?? index + 1) - 1))
355
+ : this.turnItems().map((turn) => Math.max(0, turn.index - 1));
356
+ const visibleRows = rows.slice(start, start + viewportRows);
357
+ this.contentStripeIndices = visibleRows.map((_, offset) => stripeIndices[start + offset] ?? start + offset);
313
358
  return visibleRows.map((row, offset) => {
314
359
  const index = start + offset;
315
360
  const selected = this.focus === "list" && index === this.rowIndex;
@@ -331,9 +376,11 @@ export class ActorInspectorOverlay {
331
376
  readKeys: this.readKeys,
332
377
  run: this.selectedRun,
333
378
  unreadOnly: this.communicationUnread,
334
- }).filter((preview) => this.communicationFrom === "all" ||
379
+ })
380
+ .filter((preview) => this.communicationFrom === "all" ||
335
381
  this.communicationActorLabel(preview.from_display ?? preview.from) ===
336
- this.communicationFrom);
382
+ this.communicationFrom)
383
+ .reverse();
337
384
  }
338
385
  communicationActorLabel(actor) {
339
386
  const value = actor ?? "unknown";
@@ -352,7 +399,7 @@ export class ActorInspectorOverlay {
352
399
  const actor = preview.from_display ?? preview.from ?? "unknown";
353
400
  const target = preview.channel === "room" ? "#all" : preview.to;
354
401
  const text = preview.summary ?? preview.body_preview ?? "(no body)";
355
- return `${preview.needs_response ? "! " : ""}${actor} → ${target} ${preview.type} ${text}`;
402
+ return `#${preview.sequence ?? "?"} ${preview.needs_response ? "! " : ""}${actor} → ${target} ${preview.type} ${text}`;
356
403
  });
357
404
  }
358
405
  renderDetail(width) {
@@ -379,54 +426,238 @@ export class ActorInspectorOverlay {
379
426
  const turn = this.detailTurn;
380
427
  if (!turn)
381
428
  return [this.theme.fg("warning", " Turn row is no longer available")];
382
- const value = (input) => typeof input === "string" ? input : JSON.stringify(input);
383
- const fields = [
384
- ` command ${turn.commandId}${turn.stage ? ` · ${turn.stage}` : ""}`,
385
- ` session ${turn.sessionFile}`,
386
- ...(turn.promptFile
387
- ? [` prompt ${turn.promptFile}${turn.promptBytes ? ` · ${turn.promptBytes} bytes` : ""}`]
429
+ const logicalLines = this.detailView === "transcript"
430
+ ? this.turnTranscriptLines(turn)
431
+ : this.turnEvidenceLines(turn);
432
+ const wrapped = this.wrapDetailLines(logicalLines, width);
433
+ const viewportRows = this.contentViewportRows();
434
+ const maxStart = Math.max(0, wrapped.lines.length - viewportRows);
435
+ this.detailScroll = Math.min(this.detailScroll, maxStart);
436
+ this.contentStripeIndices = wrapped.stripeIndices.slice(this.detailScroll, this.detailScroll + viewportRows);
437
+ return wrapped.lines.slice(this.detailScroll, this.detailScroll + viewportRows);
438
+ }
439
+ wrapDetailLines(lines, width) {
440
+ const wrappedLines = [];
441
+ const stripeIndices = [];
442
+ let sectionIndex = 0;
443
+ for (const line of lines) {
444
+ if (typeof line !== "string") {
445
+ sectionIndex += 1;
446
+ continue;
447
+ }
448
+ const physicalLines = line.replaceAll("\t", " ").split(/\r?\n/);
449
+ for (const physicalLine of physicalLines) {
450
+ if (!physicalLine.trim())
451
+ continue;
452
+ const leading = physicalLine.match(/^ */)?.[0] ?? "";
453
+ const content = physicalLine.slice(leading.length);
454
+ const contentWidth = Math.max(1, width - visibleWidth(leading));
455
+ const wrapped = visibleWidth(physicalLine) <= width
456
+ ? [physicalLine]
457
+ : wrapTextWithAnsi(content, contentWidth).map((visualLine) => `${leading}${visualLine}`);
458
+ for (const visualLine of wrapped) {
459
+ if (!visualLine.trim())
460
+ continue;
461
+ wrappedLines.push(visualLine);
462
+ stripeIndices.push(sectionIndex);
463
+ }
464
+ }
465
+ }
466
+ return { lines: wrappedLines, stripeIndices };
467
+ }
468
+ detailSection(title, content) {
469
+ if (content.length === 0)
470
+ return [];
471
+ return [
472
+ ` ${title}`,
473
+ ...content.flatMap((line) => line
474
+ .split(/\r?\n/)
475
+ .filter((physicalLine) => physicalLine.trim().length > 0)
476
+ .map((physicalLine) => ` ${physicalLine}`)),
477
+ DETAIL_SECTION_BREAK,
478
+ ];
479
+ }
480
+ readableValueLines(value, indent = "", depth = 0) {
481
+ if (depth > 8)
482
+ return [`${indent}[nested value]`];
483
+ if (typeof value === "string") {
484
+ const trimmed = value.trim();
485
+ if (/^[{[]/.test(trimmed)) {
486
+ try {
487
+ return this.readableValueLines(JSON.parse(trimmed), indent, depth + 1);
488
+ }
489
+ catch {
490
+ // Keep non-JSON text as written.
491
+ }
492
+ }
493
+ return value.split(/\r?\n/).map((line) => `${indent}${line}`);
494
+ }
495
+ if (value === null || value === undefined)
496
+ return [`${indent}${String(value)}`];
497
+ if (typeof value !== "object")
498
+ return [`${indent}${String(value)}`];
499
+ if (Array.isArray(value)) {
500
+ if (value.length === 0)
501
+ return [`${indent}(empty)`];
502
+ return value.flatMap((item) => {
503
+ if (item &&
504
+ typeof item === "object" &&
505
+ item.type === "text" &&
506
+ typeof item.text === "string") {
507
+ return this.readableValueLines(item.text, indent, depth + 1);
508
+ }
509
+ if (!item || typeof item !== "object")
510
+ return [`${indent}- ${String(item)}`];
511
+ return [
512
+ `${indent}-`,
513
+ ...this.readableValueLines(item, `${indent} `, depth + 1),
514
+ ];
515
+ });
516
+ }
517
+ const entries = Object.entries(value);
518
+ if (entries.length === 0)
519
+ return [`${indent}(empty)`];
520
+ return entries.flatMap(([key, item]) => {
521
+ if (!item || typeof item !== "object") {
522
+ const scalar = typeof item === "string" ? item : String(item);
523
+ const [first = "", ...rest] = scalar.split(/\r?\n/);
524
+ return [
525
+ `${indent}${key}: ${first}`,
526
+ ...rest.map((line) => `${indent} ${line}`),
527
+ ];
528
+ }
529
+ return [
530
+ `${indent}${key}:`,
531
+ ...this.readableValueLines(item, `${indent} `, depth + 1),
532
+ ];
533
+ });
534
+ }
535
+ turnEvidenceLines(turn) {
536
+ const lines = [];
537
+ const commandNumber = /^command-(\d+)$/.exec(turn.commandId)?.[1];
538
+ const subagent = commandNumber
539
+ ? String(Number.parseInt(commandNumber, 10))
540
+ : turn.commandId;
541
+ const stage = turn.stage && turn.stage !== "subagent" && turn.stage !== "command"
542
+ ? ` (${turn.stage})`
543
+ : "";
544
+ lines.push(` Subagent ${subagent}${stage}`, DETAIL_SECTION_BREAK);
545
+ if (turn.userText)
546
+ lines.push(...this.detailSection("User", [turn.userText]));
547
+ if (turn.thinking)
548
+ lines.push(...this.detailSection("Thinking", [turn.thinking]));
549
+ if (turn.assistantText)
550
+ lines.push(...this.detailSection("Assistant", [turn.assistantText]));
551
+ for (const [index, tool] of turn.toolCalls.entries()) {
552
+ const content = [
553
+ ...(tool.arguments === undefined
554
+ ? []
555
+ : ["Input", ...this.readableValueLines(tool.arguments, " ")]),
556
+ ...(tool.result === undefined
557
+ ? []
558
+ : ["Result", ...this.readableValueLines(tool.result, " ")]),
559
+ `call id: ${tool.id}`,
560
+ ];
561
+ lines.push(...this.detailSection(`Tool ${index + 1} (${tool.name}${tool.resultError ? ", Error" : ""})`, content));
562
+ }
563
+ const execution = [
564
+ ...(!turn.thinking ? ["thinking: not persisted"] : []),
565
+ ...(turn.model ? [`model: ${turn.model}`] : []),
566
+ ...(turn.stopReason ? [`stop reason: ${turn.stopReason}`] : []),
567
+ ...(turn.error ? [`error: ${turn.error}`] : []),
568
+ ...(turn.usage === undefined
569
+ ? []
570
+ : ["Usage", ...this.readableValueLines(turn.usage, " ")]),
571
+ ...(turn.unmatchedToolResults > 0
572
+ ? [`unmatched tool results: ${turn.unmatchedToolResults}`]
388
573
  : []),
389
- ...(turn.recipeContext !== undefined
390
- ? [` recipe context ${value(turn.recipeContext)}`]
574
+ ];
575
+ if (execution.length > 0)
576
+ lines.push(...this.detailSection("Execution", execution));
577
+ if (turn.diagnostics.length > 0)
578
+ lines.push(...this.detailSection("Diagnostics", turn.diagnostics));
579
+ const provenance = [
580
+ "Session",
581
+ ` ${turn.sessionFile}`,
582
+ ...(turn.sessionTruncated ? [" turn list truncated"] : []),
583
+ ...(turn.promptFile
584
+ ? [
585
+ "Prompt",
586
+ ` ${turn.promptFile}${turn.promptBytes ? ` (${turn.promptBytes} bytes)` : ""}`,
587
+ ]
391
588
  : []),
392
- ...(turn.userText ? [` user ${turn.userText}`] : []),
393
- ...(turn.assistantText ? [` assistant ${turn.assistantText}`] : []),
394
- ` reasoning ${turn.thinking ? "persisted thinking block" : "unavailable"}`,
395
- ...(turn.thinking ? [` thinking ${turn.thinking}`] : []),
396
- ...(turn.stopReason ? [` stop reason ${turn.stopReason}`] : []),
397
- ...(turn.usage !== undefined ? [` usage ${value(turn.usage)}`] : []),
398
- ...(turn.error ? [` error ${turn.error}`] : []),
399
- ` tools ${turn.toolCalls.length}`,
589
+ ...(turn.recipeContext === undefined
590
+ ? []
591
+ : [
592
+ "Recipe Context",
593
+ ...this.readableValueLines(turn.recipeContext, " "),
594
+ ]),
400
595
  ];
401
- for (const [index, tool] of turn.toolCalls.entries()) {
402
- fields.push(` tool ${String(index + 1).padEnd(9, " ")} ${tool.name} · ${tool.id}${tool.resultError ? " · error" : ""}`);
403
- if (tool.arguments !== undefined)
404
- fields.push(` arguments ${value(tool.arguments)}`);
405
- if (tool.result !== undefined)
406
- fields.push(` result ${value(tool.result)}`);
596
+ lines.push(...this.detailSection("Provenance", provenance));
597
+ return lines.length > 0 ? lines : [" No persisted turn evidence"];
598
+ }
599
+ readablePromptText(text) {
600
+ const wrapped = /^\s*<file\b[^>]*>\s*([\s\S]*?)\s*<\/file>\s*$/i.exec(text);
601
+ return wrapped?.[1]?.trim() || text;
602
+ }
603
+ turnTranscriptLines(turn) {
604
+ const lines = [];
605
+ if (turn.userText)
606
+ lines.push(...this.detailSection("User", [this.readablePromptText(turn.userText)]));
607
+ if (turn.thinking)
608
+ lines.push(...this.detailSection("Thinking", [turn.thinking]));
609
+ if (turn.assistantText)
610
+ lines.push(...this.detailSection("Assistant", [turn.assistantText]));
611
+ for (const tool of turn.toolCalls) {
612
+ const content = [
613
+ ...(tool.arguments === undefined
614
+ ? []
615
+ : ["Input", ...this.readableValueLines(tool.arguments, " ")]),
616
+ ...(tool.result === undefined
617
+ ? []
618
+ : ["Result", ...this.readableValueLines(tool.result, " ")]),
619
+ ];
620
+ lines.push(...this.detailSection(`Tool (${tool.name}${tool.resultError ? ", Error" : ""})`, content));
407
621
  }
408
- if (turn.unmatchedToolResults > 0)
409
- fields.push(` unmatched ${turn.unmatchedToolResults}`);
410
- if (turn.sessionTruncated)
411
- fields.push(" session turn list truncated");
412
- for (const diagnostic of turn.diagnostics)
413
- fields.push(` diagnostic ${diagnostic}`);
414
- const maxStart = Math.max(0, fields.length - 16);
415
- this.detailScroll = Math.min(this.detailScroll, maxStart);
416
- return fields
417
- .slice(this.detailScroll, this.detailScroll + 16)
418
- .map((field) => truncateToWidth(field, width, "…"));
622
+ if (turn.error)
623
+ lines.push(...this.detailSection("Error", [turn.error]));
624
+ return lines.length > 0 ? lines : [" No readable turn content"];
419
625
  }
420
626
  turnItems() {
421
627
  if (!this.selectedRun)
422
628
  return [];
423
629
  const selectedSubagent = this.subagents()[this.subagentIndex];
424
- return Inspector.readActorInspectorTurns(path.join(this.stateRoot, this.selectedRun)).filter((turn) => !selectedSubagent ||
630
+ return Inspector.readActorInspectorTurns(path.join(this.stateRoot, this.selectedRun))
631
+ .filter((turn) => !selectedSubagent ||
425
632
  selectedSubagent === "all" ||
426
- turn.commandId === selectedSubagent);
633
+ turn.commandId === selectedSubagent)
634
+ .reverse();
427
635
  }
428
636
  turnRows() {
429
- return this.turnItems().map((turn) => `${turn.commandId}${turn.stage ? `/${turn.stage}` : ""} ${turn.provider ?? "unknown"}/${turn.model ?? "unknown"} tools:${turn.toolCalls.length} ${turn.assistantText ?? turn.userText ?? "(no text)"}`);
637
+ return this.turnItems().map((turn) => {
638
+ const commandNumber = /^command-(\d+)$/.exec(turn.commandId)?.[1];
639
+ const subagent = commandNumber
640
+ ? `Subagent ${Number.parseInt(commandNumber, 10)}`
641
+ : `Subagent ${turn.commandId}`;
642
+ const stage = turn.stage && turn.stage !== "subagent" && turn.stage !== "command"
643
+ ? ` (${turn.stage})`
644
+ : "";
645
+ const toolNames = [...new Set(turn.toolCalls.map((tool) => tool.name))];
646
+ const action = turn.toolCalls.length === 0
647
+ ? ""
648
+ : turn.toolCalls.length === 1
649
+ ? ` (${toolNames[0]})`
650
+ : toolNames.length === 1
651
+ ? ` (${toolNames[0]}, ${turn.toolCalls.length} calls)`
652
+ : turn.toolCalls.length <= 2
653
+ ? ` (${toolNames.join(", ")})`
654
+ : ` (${turn.toolCalls.length} tools)`;
655
+ const failed = turn.error || turn.toolCalls.some((tool) => tool.resultError)
656
+ ? " (error)"
657
+ : "";
658
+ const preview = this.readablePromptText(turn.assistantText ?? turn.userText ?? "(no text)").replaceAll(/\s+/g, " ").trim();
659
+ return `#${turn.index} ${subagent}${stage} ${turn.model ?? "unknown model"}${action}${failed} ${preview}`;
660
+ });
430
661
  }
431
662
  subagents() {
432
663
  if (!this.selectedRun || this.tab !== "turns")
@@ -478,11 +709,7 @@ export class ActorInspectorOverlay {
478
709
  if (!value)
479
710
  return;
480
711
  if (this.menuLevel === "run") {
481
- this.selectedRun = value;
482
- this.runIndex = Math.max(0, this.runs().findIndex((run) => run.run === value));
483
- this.rowIndex = 0;
484
- this.subagentIndex = 0;
485
- this.communicationFrom = "all";
712
+ this.selectRun(value, Math.max(0, this.runs().findIndex((run) => run.run === value)));
486
713
  this.focus = "runs";
487
714
  return;
488
715
  }
@@ -518,12 +745,20 @@ export class ActorInspectorOverlay {
518
745
  currentFilterValue() {
519
746
  return this.filterValue(this.filterControlIndex);
520
747
  }
521
- renderMenuBox(options, focusedIndex, current, parentIndex, omitLeftBorder = false, omitRightBorder = false) {
522
- const contentWidth = Math.max(...options.map((option) => option.length + 4));
523
- const border = (left, right) => this.theme.fg("border", `${omitLeftBorder ? "" : left}${"─".repeat(contentWidth)}${omitRightBorder ? "" : right}`);
748
+ renderMenuBox(options, focusedIndex, current, parentIndex, omitLeftBorder = false, omitRightBorder = false, maxVisibleOptions = options.length) {
749
+ const visibleLimit = Math.max(1, Math.min(maxVisibleOptions, options.length));
750
+ const anchorIndex = Math.max(0, Math.min(options.length - 1, focusedIndex >= 0 ? focusedIndex : (parentIndex ?? 0)));
751
+ const maxStart = Math.max(0, options.length - visibleLimit);
752
+ const start = Math.max(0, Math.min(anchorIndex - Math.floor(visibleLimit / 2), maxStart));
753
+ const visibleOptions = options.slice(start, start + visibleLimit);
754
+ const hiddenAbove = start > 0;
755
+ const hiddenBelow = start + visibleOptions.length < options.length;
756
+ const contentWidth = Math.max(4, ...options.map((option) => visibleWidth(option) + 4));
757
+ const border = (left, right, marker = "") => this.theme.fg("border", `${omitLeftBorder ? "" : left}${marker}${"─".repeat(Math.max(0, contentWidth - visibleWidth(marker)))}${omitRightBorder ? "" : right}`);
524
758
  return [
525
- border("╭", "╮"),
526
- ...options.map((option, index) => {
759
+ border("╭", "╮", hiddenAbove ? "↑" : ""),
760
+ ...visibleOptions.map((option, localIndex) => {
761
+ const index = start + localIndex;
527
762
  const focused = index === focusedIndex;
528
763
  const row = this.fit(`${focused ? " ▶ " : " "}${option}`, contentWidth);
529
764
  const colored = focused || option === current || index === parentIndex
@@ -534,17 +769,18 @@ export class ActorInspectorOverlay {
534
769
  : colored;
535
770
  return `${omitLeftBorder ? "" : this.theme.fg("border", "│")}${styled}${omitRightBorder ? "" : this.theme.fg("border", "│")}`;
536
771
  }),
537
- border("╰", "╯"),
772
+ border("╰", "╯", hiddenBelow ? "↓" : ""),
538
773
  ];
539
774
  }
540
775
  renderFilterMenus(_width) {
541
776
  const filters = this.filterOptions().map((filter, index) => `${filter}: ${this.filterValue(index)}`);
777
+ const maxVisibleOptions = Math.max(1, this.contentViewportRows() - 1);
542
778
  const parentFocus = this.menuLevel === "filter" ? this.selectorIndex : -1;
543
- const parent = this.renderMenuBox(filters, parentFocus, undefined, this.menuLevel === "value" ? this.filterControlIndex : undefined);
779
+ const parent = this.renderMenuBox(filters, parentFocus, undefined, this.menuLevel === "value" ? this.filterControlIndex : undefined, false, false, maxVisibleOptions);
544
780
  if (this.menuLevel !== "value")
545
781
  return parent;
546
- const parentShared = this.renderMenuBox(filters, parentFocus, undefined, this.filterControlIndex, false, true);
547
- const child = this.renderMenuBox(this.valueOptions(), this.selectorIndex, this.currentFilterValue());
782
+ const parentShared = this.renderMenuBox(filters, parentFocus, undefined, this.filterControlIndex, false, true, maxVisibleOptions);
783
+ const child = this.renderMenuBox(this.valueOptions(), this.selectorIndex, this.currentFilterValue(), undefined, false, false, maxVisibleOptions);
548
784
  return Array.from({ length: Math.max(parent.length, child.length) }, (_, index) => child[index]
549
785
  ? `${parentShared[index] ?? " ".repeat(visibleWidth(parentShared[0]))}${child[index]}`
550
786
  : (parent[index] ?? ""));
@@ -559,15 +795,25 @@ export class ActorInspectorOverlay {
559
795
  else
560
796
  this.detailTurn = this.turnItems()[this.rowIndex];
561
797
  this.detailScroll = 0;
798
+ this.detailView = "evidence";
562
799
  this.detailOpen = Boolean(this.detailCommunication || this.detailTurn);
563
800
  if (this.detailOpen)
564
801
  this.focus = "detail";
565
802
  }
803
+ backDetail() {
804
+ if (this.detailView === "transcript") {
805
+ this.detailView = "evidence";
806
+ this.detailScroll = 0;
807
+ return;
808
+ }
809
+ this.closeDetail();
810
+ }
566
811
  closeDetail() {
567
812
  this.detailOpen = false;
568
813
  this.detailCommunication = undefined;
569
814
  this.detailTurn = undefined;
570
815
  this.detailScroll = 0;
816
+ this.detailView = "evidence";
571
817
  this.focus = "list";
572
818
  }
573
819
  border(left, title, right, width) {
@@ -31,7 +31,10 @@ export function readActorInspectorRuns(stateRoot, ownerId) {
31
31
  : {}),
32
32
  }];
33
33
  })
34
- .sort((left, right) => String(left.updatedAt ?? "").localeCompare(String(right.updatedAt ?? "")));
34
+ .sort((left, right) => {
35
+ const recency = String(right.updatedAt ?? "").localeCompare(String(left.updatedAt ?? ""));
36
+ return recency || right.run.localeCompare(left.run);
37
+ });
35
38
  }
36
39
  catch (error) {
37
40
  if (error.code === "ENOENT")