@llblab/pi-actors 0.41.0 → 0.42.0

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.
Files changed (150) hide show
  1. package/AGENTS.md +13 -8
  2. package/BACKLOG.md +2 -81
  3. package/CHANGELOG.md +22 -0
  4. package/README.md +22 -4
  5. package/dist/index.js +29 -82
  6. package/dist/lib/async-runs.d.ts +25 -5
  7. package/dist/lib/async-runs.js +136 -47
  8. package/dist/lib/automatic-review-runtime.d.ts +18 -0
  9. package/dist/lib/automatic-review-runtime.js +96 -0
  10. package/dist/lib/draft-consolidation-transaction.d.ts +65 -0
  11. package/dist/lib/draft-consolidation-transaction.js +610 -0
  12. package/dist/lib/draft-consolidation.d.ts +35 -0
  13. package/dist/lib/draft-consolidation.js +126 -0
  14. package/dist/lib/draft-review.d.ts +56 -0
  15. package/dist/lib/draft-review.js +254 -0
  16. package/dist/lib/draft-sleep.d.ts +65 -0
  17. package/dist/lib/draft-sleep.js +468 -0
  18. package/dist/lib/file-state.d.ts +8 -1
  19. package/dist/lib/file-state.js +115 -18
  20. package/dist/lib/inspector-actions.d.ts +16 -0
  21. package/dist/lib/inspector-actions.js +57 -0
  22. package/dist/lib/inspector-command.d.ts +7 -0
  23. package/dist/lib/inspector-command.js +37 -0
  24. package/dist/lib/inspector-overlay.d.ts +29 -1
  25. package/dist/lib/inspector-overlay.js +537 -90
  26. package/dist/lib/inspector.d.ts +9 -0
  27. package/dist/lib/inspector.js +52 -1
  28. package/dist/lib/observability.d.ts +39 -3
  29. package/dist/lib/observability.js +162 -34
  30. package/dist/lib/paths.d.ts +7 -0
  31. package/dist/lib/paths.js +28 -0
  32. package/dist/lib/recipes-discovery.js +32 -24
  33. package/dist/lib/recipes-usage.d.ts +18 -6
  34. package/dist/lib/recipes-usage.js +445 -34
  35. package/dist/lib/review-control.d.ts +14 -0
  36. package/dist/lib/review-control.js +111 -0
  37. package/dist/lib/review-diagnostics.d.ts +11 -0
  38. package/dist/lib/review-diagnostics.js +148 -0
  39. package/dist/lib/review-projection.d.ts +14 -0
  40. package/dist/lib/review-projection.js +170 -0
  41. package/dist/lib/run-ui-runtime.d.ts +18 -0
  42. package/dist/lib/run-ui-runtime.js +123 -0
  43. package/dist/lib/runs-artifacts.d.ts +1 -1
  44. package/dist/lib/runs-artifacts.js +1 -1
  45. package/dist/lib/runs-control.d.ts +8 -2
  46. package/dist/lib/runs-control.js +23 -6
  47. package/dist/lib/runs-identity.d.ts +1 -1
  48. package/dist/lib/runs-identity.js +1 -1
  49. package/dist/lib/runs-index.d.ts +11 -2
  50. package/dist/lib/runs-index.js +46 -23
  51. package/dist/lib/runs-mailbox.d.ts +1 -1
  52. package/dist/lib/runs-mailbox.js +1 -1
  53. package/dist/lib/runs-messages.d.ts +1 -1
  54. package/dist/lib/runs-messages.js +1 -1
  55. package/dist/lib/runs-outbox.d.ts +1 -1
  56. package/dist/lib/runs-outbox.js +1 -1
  57. package/dist/lib/runs-ownership.d.ts +1 -1
  58. package/dist/lib/runs-ownership.js +1 -1
  59. package/dist/lib/runs-parent-teardown.d.ts +51 -0
  60. package/dist/lib/runs-parent-teardown.js +172 -0
  61. package/dist/lib/runs-process.d.ts +1 -1
  62. package/dist/lib/runs-process.js +1 -1
  63. package/dist/lib/runs-retention.d.ts +1 -1
  64. package/dist/lib/runs-retention.js +1 -1
  65. package/dist/lib/runs-start.d.ts +5 -3
  66. package/dist/lib/runs-start.js +6 -48
  67. package/dist/lib/runs-status.d.ts +5 -3
  68. package/dist/lib/runs-status.js +4 -6
  69. package/dist/lib/runtime.d.ts +7 -1
  70. package/dist/lib/runtime.js +32 -18
  71. package/dist/lib/tool-review-lineage-transaction.d.ts +27 -0
  72. package/dist/lib/tool-review-lineage-transaction.js +597 -0
  73. package/dist/lib/tool-review-lineage.d.ts +24 -0
  74. package/dist/lib/tool-review-lineage.js +98 -0
  75. package/dist/lib/tool-review-scheduler.d.ts +80 -0
  76. package/dist/lib/tool-review-scheduler.js +494 -0
  77. package/dist/lib/tool-review-transaction.d.ts +50 -0
  78. package/dist/lib/tool-review-transaction.js +362 -0
  79. package/dist/lib/tool-review.d.ts +56 -0
  80. package/dist/lib/tool-review.js +197 -0
  81. package/dist/lib/tools-inspect.js +26 -3
  82. package/dist/lib/tools-local.js +4 -2
  83. package/dist/lib/tools-message.d.ts +1 -0
  84. package/dist/lib/tools-message.js +29 -17
  85. package/dist/lib/tools-response.d.ts +1 -1
  86. package/dist/lib/tools-response.js +5 -7
  87. package/dist/lib/tools-spawn.js +4 -1
  88. package/dist/lib/tools.d.ts +1 -0
  89. package/dist/lib/tools.js +1 -0
  90. package/dist/recipes/draft-review.json +24 -0
  91. package/dist/recipes/tool-review.json +24 -0
  92. package/dist/scripts/release-gates.mjs +165 -0
  93. package/dist/skills/actors/SKILL.md +11 -14
  94. package/dist/skills/swarm/SKILL.md +1 -1
  95. package/docs/actor-inspector.md +32 -18
  96. package/docs/async-runs.md +10 -2
  97. package/docs/recipe-library.md +7 -3
  98. package/docs/template-recipes.md +4 -11
  99. package/docs/tool-registry.md +11 -4
  100. package/index.ts +29 -103
  101. package/lib/async-runs.ts +218 -62
  102. package/lib/automatic-review-runtime.ts +135 -0
  103. package/lib/draft-consolidation-transaction.ts +821 -0
  104. package/lib/draft-consolidation.ts +181 -0
  105. package/lib/draft-review.ts +325 -0
  106. package/lib/draft-sleep.ts +576 -0
  107. package/lib/file-state.ts +143 -19
  108. package/lib/inspector-actions.ts +79 -0
  109. package/lib/inspector-command.ts +54 -0
  110. package/lib/inspector-overlay.ts +675 -105
  111. package/lib/inspector.ts +78 -3
  112. package/lib/observability.ts +219 -40
  113. package/lib/paths.ts +43 -0
  114. package/lib/recipes-discovery.ts +34 -26
  115. package/lib/recipes-usage.ts +569 -40
  116. package/lib/review-control.ts +137 -0
  117. package/lib/review-diagnostics.ts +164 -0
  118. package/lib/review-projection.ts +200 -0
  119. package/lib/run-ui-runtime.ts +153 -0
  120. package/lib/runs-artifacts.ts +1 -1
  121. package/lib/runs-control.ts +49 -5
  122. package/lib/runs-identity.ts +1 -1
  123. package/lib/runs-index.ts +57 -21
  124. package/lib/runs-mailbox.ts +1 -1
  125. package/lib/runs-messages.ts +1 -1
  126. package/lib/runs-outbox.ts +1 -1
  127. package/lib/runs-ownership.ts +1 -1
  128. package/lib/runs-parent-teardown.ts +257 -0
  129. package/lib/runs-process.ts +1 -1
  130. package/lib/runs-retention.ts +1 -1
  131. package/lib/runs-start.ts +12 -68
  132. package/lib/runs-status.ts +12 -8
  133. package/lib/runtime.ts +34 -17
  134. package/lib/tool-review-lineage-transaction.ts +881 -0
  135. package/lib/tool-review-lineage.ts +145 -0
  136. package/lib/tool-review-scheduler.ts +635 -0
  137. package/lib/tool-review-transaction.ts +563 -0
  138. package/lib/tool-review.ts +270 -0
  139. package/lib/tools-inspect.ts +33 -3
  140. package/lib/tools-local.ts +8 -2
  141. package/lib/tools-message.ts +45 -30
  142. package/lib/tools-response.ts +5 -6
  143. package/lib/tools-spawn.ts +8 -1
  144. package/lib/tools.ts +5 -0
  145. package/package.json +3 -2
  146. package/recipes/draft-review.json +24 -0
  147. package/recipes/tool-review.json +24 -0
  148. package/scripts/release-gates.mjs +165 -0
  149. package/skills/actors/SKILL.md +11 -14
  150. package/skills/swarm/SKILL.md +1 -1
@@ -17,11 +17,31 @@ import {
17
17
 
18
18
  import * as Inspector from "./inspector.ts";
19
19
 
20
- export type ActorInspectorOverlayTab = "communications" | "turns";
20
+ export type ActorInspectorOverlayTab = "recipe" | "communications" | "turns";
21
+
22
+ const INSPECTOR_TABS: ActorInspectorOverlayTab[] = [
23
+ "recipe",
24
+ "communications",
25
+ "turns",
26
+ ];
27
+
28
+ type DetailLogicalLine = string | { sectionBreak: true };
29
+
30
+ const DETAIL_SECTION_BREAK: DetailLogicalLine = { sectionBreak: true };
31
+
32
+ export interface ActorInspectorOverlayActionResult {
33
+ ok: boolean;
34
+ message: string;
35
+ }
21
36
 
22
37
  export interface ActorInspectorOverlayOptions {
23
38
  done: () => void;
39
+ killRun?: (
40
+ run: string,
41
+ runInstanceId: string,
42
+ ) => ActorInspectorOverlayActionResult;
24
43
  ownerId: string;
44
+ readTurns?: (stateDir: string) => Inspector.ActorInspectorTurnItem[];
25
45
  stateRoot: string;
26
46
  theme: Theme;
27
47
  tui: TUI;
@@ -29,7 +49,12 @@ export interface ActorInspectorOverlayOptions {
29
49
 
30
50
  export class ActorInspectorOverlay {
31
51
  private readonly done: () => void;
52
+ private readonly killRun?: (
53
+ run: string,
54
+ runInstanceId: string,
55
+ ) => ActorInspectorOverlayActionResult;
32
56
  private readonly ownerId: string;
57
+ private readonly readTurns: (stateDir: string) => Inspector.ActorInspectorTurnItem[];
33
58
  private readonly stateRoot: string;
34
59
  private readonly theme: Theme;
35
60
  private readonly tui: TUI;
@@ -43,23 +68,32 @@ export class ActorInspectorOverlay {
43
68
  private detailOpen = false;
44
69
  private detailScroll = 0;
45
70
  private detailTurn?: Inspector.ActorInspectorTurnItem;
71
+ private feedback?: ActorInspectorOverlayActionResult;
72
+ private killConfirmation?: { run: string; runInstanceId: string };
46
73
  private readonly readKeys = new Set<string>();
47
- private focus: "runs" | "tabs" | "list" | "detail" | "select" = "tabs";
74
+ private focus: "runs" | "tabs" | "recipe" | "list" | "detail" | "select" = "tabs";
48
75
  private filterControlIndex = 0;
49
76
  private menuLevel: "run" | "filter" | "value" = "filter";
50
77
  private selectorIndex = 0;
78
+ private recipeScroll = 0;
51
79
  private rowIndex = 0;
52
80
  private selectedRun?: string;
53
81
  private subagentIndex = 0;
54
- private tab: ActorInspectorOverlayTab = "communications";
82
+ private tab: ActorInspectorOverlayTab = "recipe";
83
+ private turnCache?: { run: string; turns: Inspector.ActorInspectorTurnItem[] };
55
84
 
56
85
  constructor(options: ActorInspectorOverlayOptions) {
57
86
  this.done = options.done;
87
+ this.killRun = options.killRun;
58
88
  this.ownerId = options.ownerId;
89
+ this.readTurns = options.readTurns ?? Inspector.readActorInspectorTurns;
59
90
  this.stateRoot = options.stateRoot;
60
91
  this.theme = options.theme;
61
92
  this.tui = options.tui;
62
- this.refreshTimer = setInterval(() => this.tui.requestRender(), 1000);
93
+ this.refreshTimer = setInterval(() => {
94
+ if (this.focus !== "list" && this.focus !== "detail") this.turnCache = undefined;
95
+ this.tui.requestRender();
96
+ }, 1000);
63
97
  this.refreshTimer.unref?.();
64
98
  }
65
99
 
@@ -70,13 +104,29 @@ export class ActorInspectorOverlay {
70
104
  this.done();
71
105
  return;
72
106
  }
107
+ if (this.killConfirmation) {
108
+ if (matchesKey(data, "escape") || data.toLowerCase() === "n") {
109
+ this.feedback = { ok: false, message: "Kill cancelled." };
110
+ this.killConfirmation = undefined;
111
+ } else if (matchesKey(data, "return") || data.toLowerCase() === "y") {
112
+ this.confirmKill();
113
+ }
114
+ this.tui.requestRender();
115
+ return;
116
+ }
117
+ if (data.toLowerCase() === "k") {
118
+ this.requestKill(runs);
119
+ this.tui.requestRender();
120
+ return;
121
+ }
73
122
  if (matchesKey(data, "escape")) {
74
123
  if (this.focus === "select") {
75
124
  if (this.menuLevel === "value") {
76
125
  this.menuLevel = "filter";
77
126
  this.selectorIndex = this.filterControlIndex;
78
127
  } else this.focus = this.menuLevel === "run" ? "runs" : "tabs";
79
- } else if (this.focus === "detail") this.closeDetail();
128
+ } else if (this.focus === "detail") this.backDetail();
129
+ else if (this.focus === "recipe") this.focus = "tabs";
80
130
  else this.done();
81
131
  this.tui.requestRender();
82
132
  return;
@@ -96,28 +146,61 @@ export class ActorInspectorOverlay {
96
146
  this.tui.requestRender();
97
147
  return;
98
148
  }
149
+ if (this.focus === "recipe") {
150
+ if (matchesKey(data, "up")) {
151
+ if (this.recipeScroll === 0) this.focus = "tabs";
152
+ else this.recipeScroll -= 1;
153
+ } else if (matchesKey(data, "down")) this.recipeScroll += 1;
154
+ else if (matchesKey(data, "pageUp"))
155
+ this.recipeScroll = Math.max(0, this.recipeScroll - this.contentViewportRows());
156
+ else if (matchesKey(data, "pageDown"))
157
+ this.recipeScroll += this.contentViewportRows();
158
+ else if (matchesKey(data, "left")) this.focus = "tabs";
159
+ this.tui.requestRender();
160
+ return;
161
+ }
99
162
  if (this.focus === "detail") {
100
163
  if (matchesKey(data, "up")) this.detailScroll = Math.max(0, this.detailScroll - 1);
101
164
  else if (matchesKey(data, "down")) this.detailScroll += 1;
102
- else if (matchesKey(data, "left")) this.closeDetail();
165
+ else if (matchesKey(data, "pageUp"))
166
+ this.detailScroll = Math.max(0, this.detailScroll - this.contentViewportRows());
167
+ else if (matchesKey(data, "pageDown"))
168
+ this.detailScroll += this.contentViewportRows();
169
+ else if (matchesKey(data, "left")) this.backDetail();
103
170
  this.tui.requestRender();
104
171
  return;
105
172
  }
106
173
  if (this.focus === "runs") {
107
- if (matchesKey(data, "down")) this.focus = "tabs";
174
+ if (matchesKey(data, "left")) this.cycleRun(runs, -1);
175
+ else if (matchesKey(data, "right")) this.cycleRun(runs, 1);
176
+ else if (matchesKey(data, "down")) this.focus = "tabs";
108
177
  else if (matchesKey(data, "return") && runs.length > 0) this.openRunMenu();
109
178
  } else if (this.focus === "tabs") {
110
179
  if (matchesKey(data, "left") || matchesKey(data, "right")) {
111
- this.tab = this.tab === "communications" ? "turns" : "communications";
180
+ const current = INSPECTOR_TABS.indexOf(this.tab);
181
+ const direction = matchesKey(data, "right") ? 1 : -1;
182
+ this.tab = INSPECTOR_TABS[
183
+ (current + direction + INSPECTOR_TABS.length) % INSPECTOR_TABS.length
184
+ ]!;
112
185
  this.filterControlIndex = 0;
186
+ this.recipeScroll = 0;
113
187
  this.rowIndex = 0;
114
188
  } else if (matchesKey(data, "up")) this.focus = "runs";
189
+ else if (
190
+ (matchesKey(data, "down") || matchesKey(data, "return")) &&
191
+ this.tab === "recipe"
192
+ ) this.focus = "recipe";
115
193
  else if (matchesKey(data, "down") && this.listItemCount() > 0)
116
194
  this.focus = "list";
117
195
  else if (matchesKey(data, "return")) this.openFilterMenu();
118
196
  } else if (this.focus === "list") {
119
197
  const count = this.tab === "communications" ? this.communicationPreviews().length : this.turnItems().length;
120
- if (matchesKey(data, "up")) {
198
+ if (matchesKey(data, "left")) this.focus = "tabs";
199
+ else if (matchesKey(data, "pageUp"))
200
+ this.rowIndex = Math.max(0, this.rowIndex - this.contentViewportRows());
201
+ else if (matchesKey(data, "pageDown"))
202
+ this.rowIndex = Math.min(Math.max(0, count - 1), this.rowIndex + this.contentViewportRows());
203
+ else if (matchesKey(data, "up")) {
121
204
  if (this.rowIndex === 0) this.focus = "tabs";
122
205
  else this.rowIndex -= 1;
123
206
  } else if (matchesKey(data, "down"))
@@ -138,8 +221,16 @@ export class ActorInspectorOverlay {
138
221
  let selector = this.focus === "select"
139
222
  ? this.menuLevel === "run"
140
223
  ? this.renderMenuBox(
141
- this.runs().map((run) => `${run.run} · ${run.status}`),
224
+ this.runs().map(
225
+ (run, index, runs) =>
226
+ `#${runs.length - index} ${run.run} ${run.status}`,
227
+ ),
142
228
  this.selectorIndex,
229
+ undefined,
230
+ undefined,
231
+ false,
232
+ false,
233
+ this.contentViewportRows(),
143
234
  )
144
235
  : this.renderFilterMenus(innerWidth)
145
236
  : [];
@@ -170,16 +261,33 @@ export class ActorInspectorOverlay {
170
261
  );
171
262
  } else lines.push(this.border("├", "", "┤", innerWidth));
172
263
  this.contentStripeIndices = [];
173
- const content = this.detailOpen
174
- ? this.renderDetail(innerWidth)
175
- : this.selectedRun
176
- ? this.renderTimeline(innerWidth)
177
- : this.renderRunSelector(innerWidth);
178
- const minContentRows = 16;
264
+ const baseContent = this.killConfirmation
265
+ ? this.renderKillConfirmation()
266
+ : this.detailOpen
267
+ ? this.renderDetail(innerWidth)
268
+ : this.selectedRun
269
+ ? this.renderTimeline(innerWidth)
270
+ : this.renderRunSelector(innerWidth);
271
+ const content = this.feedback && !this.killConfirmation
272
+ ? [
273
+ this.theme.fg(
274
+ this.feedback.ok
275
+ ? "success"
276
+ : /^Kill (failed|rejected):/.test(this.feedback.message)
277
+ ? "error"
278
+ : "warning",
279
+ ` ${this.feedback.message}`,
280
+ ),
281
+ ...baseContent.slice(0, Math.max(0, this.contentViewportRows() - 1)),
282
+ ]
283
+ : baseContent;
284
+ const contentViewportRows = this.contentViewportRows();
179
285
  const selectorRows = selector.slice(1);
180
- for (let index = 0; index < Math.max(content.length, minContentRows); index += 1) {
286
+ for (let index = 0; index < Math.max(content.length, contentViewportRows); index += 1) {
181
287
  const base = content[index] ?? "";
182
- const stripeIndex = this.contentStripeIndices[index] ?? index;
288
+ const stripeIndex = index < content.length
289
+ ? this.contentStripeIndices[index] ?? index
290
+ : 0;
183
291
  const popup = selectorRows[index];
184
292
  if (!popup) {
185
293
  lines.push(this.stripedRow(base, innerWidth, stripeIndex));
@@ -223,7 +331,103 @@ export class ActorInspectorOverlay {
223
331
  if (!this.selectedRun) this.selectedRun = runs[0]?.run;
224
332
  }
225
333
 
334
+ private contentViewportRows(): number {
335
+ const overlayRows = Math.floor(this.tui.terminal.rows * 0.94);
336
+ return Math.max(4, Math.min(16, overlayRows - 7));
337
+ }
338
+
339
+ private selectRun(run: string, index: number): void {
340
+ this.selectedRun = run;
341
+ this.turnCache = undefined;
342
+ this.runIndex = index;
343
+ this.recipeScroll = 0;
344
+ this.rowIndex = 0;
345
+ this.subagentIndex = 0;
346
+ this.communicationFrom = "all";
347
+ }
348
+
349
+ private cycleRun(
350
+ runs: Inspector.ActorInspectorRunItem[],
351
+ direction: -1 | 1,
352
+ ): void {
353
+ if (runs.length === 0) return;
354
+ const currentIndex = Math.max(
355
+ 0,
356
+ runs.findIndex((run) => run.run === this.selectedRun),
357
+ );
358
+ const nextIndex = (currentIndex + direction + runs.length) % runs.length;
359
+ const next = runs[nextIndex];
360
+ if (next) this.selectRun(next.run, nextIndex);
361
+ }
362
+
363
+ private requestKill(runs: Inspector.ActorInspectorRunItem[]): void {
364
+ this.feedback = undefined;
365
+ if (this.focus !== "runs") {
366
+ this.feedback = { ok: false, message: "Focus Run before using Kill." };
367
+ return;
368
+ }
369
+ const run = runs.find((item) => item.run === this.selectedRun);
370
+ if (!run) {
371
+ this.feedback = { ok: false, message: "Kill unavailable: no owned run." };
372
+ return;
373
+ }
374
+ if (!this.killRun) {
375
+ this.feedback = { ok: false, message: "Kill unavailable in this Inspector." };
376
+ return;
377
+ }
378
+ if (run.status !== "running") {
379
+ this.feedback = {
380
+ ok: false,
381
+ message: `Kill unavailable: run is ${run.status}.`,
382
+ };
383
+ return;
384
+ }
385
+ if (!run.runInstanceId) {
386
+ this.feedback = {
387
+ ok: false,
388
+ message: "Kill unavailable: run generation unavailable.",
389
+ };
390
+ return;
391
+ }
392
+ this.killConfirmation = {
393
+ run: run.run,
394
+ runInstanceId: run.runInstanceId,
395
+ };
396
+ }
397
+
398
+ private confirmKill(): void {
399
+ const confirmation = this.killConfirmation;
400
+ this.killConfirmation = undefined;
401
+ if (!confirmation || !this.killRun) {
402
+ this.feedback = { ok: false, message: "Kill unavailable in this Inspector." };
403
+ return;
404
+ }
405
+ try {
406
+ this.feedback = this.killRun(
407
+ confirmation.run,
408
+ confirmation.runInstanceId,
409
+ );
410
+ } catch (error) {
411
+ const message = error instanceof Error ? error.message : String(error);
412
+ this.feedback = {
413
+ ok: false,
414
+ message: `Kill failed: ${message.replaceAll(/\s+/g, " ").slice(0, 160)}.`,
415
+ };
416
+ }
417
+ }
418
+
419
+ private renderKillConfirmation(): string[] {
420
+ return [
421
+ this.theme.fg(
422
+ "error",
423
+ ` Kill running actor run:${this.killConfirmation?.run}?`,
424
+ ),
425
+ this.theme.fg("muted", " This sends canonical control.kill and cannot be undone."),
426
+ ];
427
+ }
428
+
226
429
  private listItemCount(): number {
430
+ if (this.tab === "recipe") return 0;
227
431
  return this.tab === "communications"
228
432
  ? this.communicationPreviews().length
229
433
  : this.turnItems().length;
@@ -232,9 +436,13 @@ export class ActorInspectorOverlay {
232
436
  private selectorAnchor(): number {
233
437
  if (this.menuLevel === "run") {
234
438
  const runControl = stripVTControlCharacters(this.renderRunControl());
235
- return Math.max(0, runControl.indexOf("Run:") - 1);
439
+ return Math.max(0, runControl.indexOf("Run:") - 2);
236
440
  }
237
- const label = this.tab === "communications" ? "Messages" : "Turns";
441
+ const label = this.tab === "communications"
442
+ ? "Messages"
443
+ : this.tab === "turns"
444
+ ? "Turns"
445
+ : "Recipe";
238
446
  const tabs = stripVTControlCharacters(this.renderTabs());
239
447
  return Math.max(0, tabs.indexOf(label) - 2);
240
448
  }
@@ -242,23 +450,36 @@ export class ActorInspectorOverlay {
242
450
  private renderKeyHints(): string {
243
451
  const hint = (keys: string, description: string) =>
244
452
  `${this.theme.fg("accent", keys)}${this.theme.fg("dim", ` ${description}`)}`;
453
+ if (this.killConfirmation)
454
+ return ` ${hint("y/enter", "confirm kill")} ${hint("n/esc", "cancel")}`;
245
455
  if (this.focus === "select")
246
456
  return this.menuLevel === "value"
247
457
  ? ` ${hint("↑↓", "option")} ${hint("enter", "apply")} ${hint("←/esc", "back")}`
248
458
  : ` ${hint("↑↓", "option")} ${hint("→/enter", "open")} ${hint("←/esc", "back")}`;
459
+ if (this.focus === "recipe")
460
+ return ` ${hint("↑↓/pgup/pgdn", "scroll")} ${hint("↑ at top", "tabs")} ${hint("←/esc", "tabs")}`;
249
461
  if (this.focus === "detail")
250
- return ` ${hint("↑↓", "scroll")} ${hint("←/esc", "back")}`;
462
+ return ` ${hint("↑↓/pgup/pgdn", "scroll")} ${hint("←/esc", "back")}`;
251
463
  if (this.focus === "list")
252
- return ` ${hint("↑↓", "row")} ${hint("→/enter", "open")} ${hint("esc", "close")}`;
464
+ return ` ${hint("↑↓/pgup/pgdn", "row")} ${hint("→/enter", "open")} ${hint("←", "tabs")} ${hint("esc", "close")}`;
465
+ if (this.focus === "runs") {
466
+ const run = this.runs().find((item) => item.run === this.selectedRun);
467
+ const killHint = this.killRun && run?.status === "running" && run.runInstanceId
468
+ ? ` ${hint("k", "kill")}`
469
+ : "";
470
+ return ` ${hint("←→", "run")} ${hint("↓", "tabs")} ${hint("enter", "list")}${killHint} ${hint("esc", "close")}`;
471
+ }
253
472
  return ` ${hint("←→", "navigate")} ${hint("↑↓", "change row")} ${hint("enter", "select")} ${hint("esc", "close")}`;
254
473
  }
255
474
 
256
475
  private renderRunControl(): string {
257
- const run = this.runs().find((item) => item.run === this.selectedRun);
476
+ const runs = this.runs();
477
+ const runIndex = runs.findIndex((item) => item.run === this.selectedRun);
478
+ const run = runs[runIndex];
258
479
  const focused = this.focus === "runs";
259
480
  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", " ] ") : " ";
481
+ const prefix = active ? this.theme.fg("accent", " ← ") : " ";
482
+ const suffix = active ? this.theme.fg("accent", " → ") : " ";
262
483
  if (!run) {
263
484
  const empty = this.theme.fg("muted", `${prefix}Run: none${suffix}`);
264
485
  return focused ? this.theme.bg("selectedBg", empty) : empty;
@@ -270,7 +491,8 @@ export class ActorInspectorOverlay {
270
491
  : run.status === "done"
271
492
  ? "success"
272
493
  : "muted";
273
- 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}`;
494
+ const sequence = runs.length - runIndex;
495
+ 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
496
  return focused ? this.theme.bg("selectedBg", value) : value;
275
497
  }
276
498
 
@@ -297,19 +519,19 @@ export class ActorInspectorOverlay {
297
519
  };
298
520
  const messageFilters = this.activeMessageFilters();
299
521
  const subagent = this.subagents()[this.subagentIndex] ?? "all";
300
- const messagesLabel = `Messages${messageFilters.length > 0 ? ` · ${messageFilters.join(" · ")}` : ""}`;
301
- const turnsLabel = `Turns${subagent === "all" ? "" : ` · ${subagent}`}`;
302
- return `${tab("communications", messagesLabel)} ${tab("turns", turnsLabel)}`;
522
+ const messagesLabel = `Messages${messageFilters.length > 0 ? ` (${messageFilters.join(", ")})` : ""}`;
523
+ const turnsLabel = `Turns${subagent === "all" ? "" : ` (${subagent})`}`;
524
+ return `${tab("recipe", "Recipe")} ${tab("communications", messagesLabel)} ${tab("turns", turnsLabel)}`;
303
525
  }
304
526
 
305
527
  private renderRunSelector(width: number): string[] {
306
528
  const runs = this.runs();
307
529
  if (runs.length === 0) return [this.theme.fg("muted", " No owned actor runs")];
308
530
  this.runIndex = Math.min(this.runIndex, runs.length - 1);
309
- return runs.slice(0, 16).map((run, index) => {
531
+ return runs.slice(0, this.contentViewportRows()).map((run, index) => {
310
532
  const selected = index === this.runIndex;
311
533
  const prefix = selected ? this.theme.fg("accent", " ▶ ") : " ";
312
- const label = `${prefix}run:${run.run}`;
534
+ const label = `${prefix}#${runs.length - index} run:${run.run}`;
313
535
  const status = ` ${run.status}`;
314
536
  return selected
315
537
  ? this.theme.fg("accent", `${label}${status}`)
@@ -324,6 +546,7 @@ export class ActorInspectorOverlay {
324
546
  this.selectedRun = undefined;
325
547
  return [this.theme.fg("warning", " Selected run is no longer owned by this session")];
326
548
  }
549
+ if (this.tab === "recipe") return this.renderRecipe(width);
327
550
  const rows =
328
551
  this.tab === "communications"
329
552
  ? this.communicationRows()
@@ -337,7 +560,7 @@ export class ActorInspectorOverlay {
337
560
  this.theme.fg(
338
561
  "muted",
339
562
  filtered
340
- ? " No results for the active filters · Enter on the tab to adjust"
563
+ ? " No results for the active filters. Enter on the tab to adjust."
341
564
  : this.tab === "communications"
342
565
  ? " No messages in this run"
343
566
  : " No persisted turns in this run",
@@ -345,12 +568,23 @@ export class ActorInspectorOverlay {
345
568
  ];
346
569
  }
347
570
  this.rowIndex = Math.min(this.rowIndex, rows.length - 1);
571
+ const viewportRows = this.contentViewportRows();
348
572
  const start = Math.max(
349
573
  0,
350
- Math.min(this.rowIndex - 7, Math.max(0, rows.length - 15)),
574
+ Math.min(
575
+ this.rowIndex - Math.floor(viewportRows / 2),
576
+ Math.max(0, rows.length - viewportRows),
577
+ ),
578
+ );
579
+ const stripeIndices = this.tab === "communications"
580
+ ? this.communicationPreviews().map((preview, index) =>
581
+ Math.max(0, (preview.sequence ?? index + 1) - 1),
582
+ )
583
+ : this.turnItems().map((turn) => Math.max(0, turn.index - 1));
584
+ const visibleRows = rows.slice(start, start + viewportRows);
585
+ this.contentStripeIndices = visibleRows.map(
586
+ (_, offset) => stripeIndices[start + offset] ?? start + offset,
351
587
  );
352
- const visibleRows = rows.slice(start, start + 15);
353
- this.contentStripeIndices = visibleRows.map((_, offset) => start + offset);
354
588
  return visibleRows.map((row, offset) => {
355
589
  const index = start + offset;
356
590
  const selected = this.focus === "list" && index === this.rowIndex;
@@ -360,6 +594,51 @@ export class ActorInspectorOverlay {
360
594
  });
361
595
  }
362
596
 
597
+ private renderRecipe(width: number): string[] {
598
+ if (!this.selectedRun) return [];
599
+ const recipe = Inspector.readActorInspectorRecipe(
600
+ path.join(this.stateRoot, this.selectedRun),
601
+ );
602
+ const logicalLines: DetailLogicalLine[] = [
603
+ ...this.detailSection(
604
+ "Identity",
605
+ this.readableValueLines(recipe.identity, " "),
606
+ ),
607
+ ...(recipe.definition
608
+ ? this.detailSection(
609
+ "Recipe",
610
+ this.readableValueLines(recipe.definition, " "),
611
+ )
612
+ : []),
613
+ ...this.detailSection(
614
+ "Launch",
615
+ this.readableValueLines(recipe.launch, " "),
616
+ ),
617
+ ...(recipe.composition.length > 0
618
+ ? this.detailSection(
619
+ "Composition",
620
+ this.readableValueLines(recipe.composition, " "),
621
+ )
622
+ : []),
623
+ ...this.detailSection("Diagnostics", recipe.diagnostics),
624
+ ];
625
+ const wrapped = this.wrapDetailLines(
626
+ logicalLines.length > 0 ? logicalLines : [" No persisted recipe evidence"],
627
+ width,
628
+ );
629
+ const viewportRows = this.contentViewportRows();
630
+ const maxStart = Math.max(0, wrapped.lines.length - viewportRows);
631
+ this.recipeScroll = Math.min(this.recipeScroll, maxStart);
632
+ this.contentStripeIndices = wrapped.stripeIndices.slice(
633
+ this.recipeScroll,
634
+ this.recipeScroll + viewportRows,
635
+ );
636
+ return wrapped.lines.slice(
637
+ this.recipeScroll,
638
+ this.recipeScroll + viewportRows,
639
+ );
640
+ }
641
+
363
642
  private communicationPreviews(): Inspector.ActorInspectorPreview[] {
364
643
  if (!this.selectedRun) return [];
365
644
  const channels =
@@ -373,12 +652,14 @@ export class ActorInspectorOverlay {
373
652
  readKeys: this.readKeys,
374
653
  run: this.selectedRun,
375
654
  unreadOnly: this.communicationUnread,
376
- }).filter(
377
- (preview) =>
378
- this.communicationFrom === "all" ||
379
- this.communicationActorLabel(preview.from_display ?? preview.from) ===
380
- this.communicationFrom,
381
- );
655
+ })
656
+ .filter(
657
+ (preview) =>
658
+ this.communicationFrom === "all" ||
659
+ this.communicationActorLabel(preview.from_display ?? preview.from) ===
660
+ this.communicationFrom,
661
+ )
662
+ .reverse();
382
663
  }
383
664
 
384
665
  private communicationActorLabel(actor?: string): string {
@@ -403,7 +684,7 @@ export class ActorInspectorOverlay {
403
684
  const actor = preview.from_display ?? preview.from ?? "unknown";
404
685
  const target = preview.channel === "room" ? "#all" : preview.to;
405
686
  const text = preview.summary ?? preview.body_preview ?? "(no body)";
406
- return `${preview.needs_response ? "! " : ""}${actor} → ${target} ${preview.type} ${text}`;
687
+ return `#${preview.sequence ?? "?"} ${preview.needs_response ? "! " : ""}${actor} → ${target} ${preview.type} ${text}`;
407
688
  });
408
689
  }
409
690
 
@@ -423,7 +704,7 @@ export class ActorInspectorOverlay {
423
704
  ["inbox", preview.inbox_status],
424
705
  ["timestamp", preview.timestamp],
425
706
  ];
426
- return fields
707
+ const lines = fields
427
708
  .filter(([, value]) => value !== undefined)
428
709
  .map(([label, value]) =>
429
710
  truncateToWidth(
@@ -432,74 +713,330 @@ export class ActorInspectorOverlay {
432
713
  "…",
433
714
  ),
434
715
  );
716
+ const viewportRows = this.contentViewportRows();
717
+ const maxStart = Math.max(0, lines.length - viewportRows);
718
+ this.detailScroll = Math.min(this.detailScroll, maxStart);
719
+ return lines.slice(this.detailScroll, this.detailScroll + viewportRows);
435
720
  }
436
721
  const turn = this.detailTurn;
437
722
  if (!turn) return [this.theme.fg("warning", " Turn row is no longer available")];
438
- const value = (input: unknown) =>
439
- typeof input === "string" ? input : JSON.stringify(input);
440
- const fields: string[] = [
441
- ` command ${turn.commandId}${turn.stage ? ` · ${turn.stage}` : ""}`,
442
- ` session ${turn.sessionFile}`,
443
- ...(turn.promptFile
444
- ? [` prompt ${turn.promptFile}${turn.promptBytes ? ` · ${turn.promptBytes} bytes` : ""}`]
445
- : []),
446
- ...(turn.recipeContext !== undefined
447
- ? [` recipe context ${value(turn.recipeContext)}`]
448
- : []),
449
- ...(turn.userText ? [` user ${turn.userText}`] : []),
450
- ...(turn.assistantText ? [` assistant ${turn.assistantText}`] : []),
451
- ` reasoning ${turn.thinking ? "persisted thinking block" : "unavailable"}`,
452
- ...(turn.thinking ? [` thinking ${turn.thinking}`] : []),
453
- ...(turn.stopReason ? [` stop reason ${turn.stopReason}`] : []),
454
- ...(turn.usage !== undefined ? [` usage ${value(turn.usage)}`] : []),
455
- ...(turn.error ? [` error ${turn.error}`] : []),
456
- ` tools ${turn.toolCalls.length}`,
723
+ const logicalLines = this.turnEvidenceLines(turn);
724
+ const wrapped = this.wrapDetailLines(logicalLines, width);
725
+ const viewportRows = this.contentViewportRows();
726
+ const maxStart = Math.max(0, wrapped.lines.length - viewportRows);
727
+ this.detailScroll = Math.min(this.detailScroll, maxStart);
728
+ this.contentStripeIndices = wrapped.stripeIndices.slice(
729
+ this.detailScroll,
730
+ this.detailScroll + viewportRows,
731
+ );
732
+ return wrapped.lines.slice(
733
+ this.detailScroll,
734
+ this.detailScroll + viewportRows,
735
+ );
736
+ }
737
+
738
+ private wrapDetailLines(
739
+ lines: DetailLogicalLine[],
740
+ width: number,
741
+ ): { lines: string[]; stripeIndices: number[] } {
742
+ const wrappedLines: string[] = [];
743
+ const stripeIndices: number[] = [];
744
+ let sectionIndex = 0;
745
+ for (const line of lines) {
746
+ if (typeof line !== "string") {
747
+ sectionIndex += 1;
748
+ continue;
749
+ }
750
+ const physicalLines = line.replaceAll("\t", " ").split(/\r?\n/);
751
+ for (const physicalLine of physicalLines) {
752
+ if (!physicalLine.trim()) continue;
753
+ const leading = physicalLine.match(/^ */)?.[0] ?? "";
754
+ const content = physicalLine.slice(leading.length);
755
+ const contentWidth = Math.max(1, width - visibleWidth(leading));
756
+ const labeled = /^(\s*(?:-\s+)?[^:]+:\s+)(\S.*)$/u.exec(physicalLine);
757
+ let wrapped: string[];
758
+ if (
759
+ visibleWidth(physicalLine) > width &&
760
+ labeled &&
761
+ visibleWidth(labeled[1]) < width
762
+ ) {
763
+ const prefix = labeled[1];
764
+ const value = labeled[2];
765
+ const firstWidth = Math.max(1, width - visibleWidth(prefix));
766
+ const first = this.wrapPreferred(value, firstWidth)[0] ??
767
+ this.takeVisiblePrefix(value, firstWidth);
768
+ const remainder = value.slice(first.length).trimStart();
769
+ const continuation = `${leading} `;
770
+ const continuationWidth = Math.max(
771
+ 1,
772
+ width - visibleWidth(continuation),
773
+ );
774
+ wrapped = [
775
+ `${prefix}${first}`,
776
+ ...(remainder
777
+ ? this.wrapPreferred(remainder, continuationWidth).map(
778
+ (visualLine) => `${continuation}${visualLine}`,
779
+ )
780
+ : []),
781
+ ];
782
+ } else {
783
+ wrapped = visibleWidth(physicalLine) <= width
784
+ ? [physicalLine]
785
+ : this.wrapPreferred(content, contentWidth).map(
786
+ (visualLine) => `${leading}${visualLine}`,
787
+ );
788
+ }
789
+ for (const visualLine of wrapped) {
790
+ if (!visualLine.trim()) continue;
791
+ wrappedLines.push(visualLine);
792
+ stripeIndices.push(sectionIndex);
793
+ }
794
+ }
795
+ }
796
+ return { lines: wrappedLines, stripeIndices };
797
+ }
798
+
799
+ private wrapPreferred(content: string, width: number): string[] {
800
+ const lines: string[] = [];
801
+ let remaining = stripVTControlCharacters(content).trimEnd();
802
+ while (remaining && visibleWidth(remaining) > width) {
803
+ const candidate = this.takeVisiblePrefix(remaining, width);
804
+ let splitAt = -1;
805
+ let trimBoundary = false;
806
+ for (let index = 1; index < candidate.length; index += 1) {
807
+ if (/\s/u.test(candidate[index]!)) {
808
+ splitAt = index + 1;
809
+ trimBoundary = true;
810
+ } else if (/[-+=/_,.:]/u.test(candidate[index]!)) {
811
+ splitAt = index;
812
+ trimBoundary = false;
813
+ }
814
+ }
815
+ if (splitAt <= 0) splitAt = candidate.length;
816
+ const head = remaining.slice(0, splitAt).trimEnd();
817
+ if (!head) {
818
+ lines.push(candidate);
819
+ remaining = remaining.slice(candidate.length);
820
+ continue;
821
+ }
822
+ lines.push(head);
823
+ remaining = remaining.slice(splitAt);
824
+ if (trimBoundary) remaining = remaining.trimStart();
825
+ }
826
+ if (remaining) lines.push(remaining);
827
+ return lines;
828
+ }
829
+
830
+ private detailSection(
831
+ title: string,
832
+ content: string[],
833
+ ): DetailLogicalLine[] {
834
+ if (content.length === 0) return [];
835
+ return [
836
+ ` ${title}`,
837
+ ...content.flatMap((line) =>
838
+ line
839
+ .split(/\r?\n/)
840
+ .filter((physicalLine) => physicalLine.trim().length > 0)
841
+ .map((physicalLine) => ` ${physicalLine}`),
842
+ ),
843
+ DETAIL_SECTION_BREAK,
457
844
  ];
845
+ }
846
+
847
+ private readableValueLines(
848
+ value: unknown,
849
+ indent = "",
850
+ depth = 0,
851
+ ): string[] {
852
+ if (depth > 8) return [`${indent}[nested value]`];
853
+ if (typeof value === "string") {
854
+ const trimmed = value.trim();
855
+ if (/^[{[]/.test(trimmed)) {
856
+ try {
857
+ return this.readableValueLines(JSON.parse(trimmed), indent, depth + 1);
858
+ } catch {
859
+ // Keep non-JSON text as written.
860
+ }
861
+ }
862
+ return value.split(/\r?\n/).map((line) => `${indent}${line}`);
863
+ }
864
+ if (value === null || value === undefined)
865
+ return [`${indent}${String(value)}`];
866
+ if (typeof value !== "object") return [`${indent}${String(value)}`];
867
+ if (Array.isArray(value)) {
868
+ if (value.length === 0) return [`${indent}(empty)`];
869
+ return value.flatMap((item) => {
870
+ if (
871
+ item &&
872
+ typeof item === "object" &&
873
+ (item as { type?: unknown }).type === "text" &&
874
+ typeof (item as { text?: unknown }).text === "string"
875
+ ) {
876
+ return this.readableValueLines(
877
+ (item as { text: string }).text,
878
+ indent,
879
+ depth + 1,
880
+ );
881
+ }
882
+ if (!item || typeof item !== "object")
883
+ return [`${indent}- ${String(item)}`];
884
+ const [first = "", ...rest] = this.readableValueLines(
885
+ item,
886
+ `${indent} `,
887
+ depth + 1,
888
+ );
889
+ return [
890
+ `${indent}- ${first.trimStart()}`,
891
+ ...rest,
892
+ ];
893
+ });
894
+ }
895
+ const entries = Object.entries(value as Record<string, unknown>);
896
+ if (entries.length === 0) return [`${indent}(empty)`];
897
+ return entries.flatMap(([key, item]) => {
898
+ if (!item || typeof item !== "object") {
899
+ const scalar = typeof item === "string" ? item : String(item);
900
+ const [first = "", ...rest] = scalar.split(/\r?\n/);
901
+ return [
902
+ `${indent}${key}: ${first}`,
903
+ ...rest.map((line) => `${indent} ${line}`),
904
+ ];
905
+ }
906
+ return [
907
+ `${indent}${key}:`,
908
+ ...this.readableValueLines(item, `${indent} `, depth + 1),
909
+ ];
910
+ });
911
+ }
912
+
913
+ private turnEvidenceLines(
914
+ turn: Inspector.ActorInspectorTurnItem,
915
+ ): DetailLogicalLine[] {
916
+ const lines: DetailLogicalLine[] = [];
917
+ const commandNumber = /^command-(\d+)$/.exec(turn.commandId)?.[1];
918
+ const subagent = commandNumber
919
+ ? String(Number.parseInt(commandNumber, 10))
920
+ : turn.commandId;
921
+ const stage =
922
+ turn.stage && turn.stage !== "subagent" && turn.stage !== "command"
923
+ ? ` (${turn.stage})`
924
+ : "";
925
+ lines.push(` Subagent ${subagent}${stage}`, DETAIL_SECTION_BREAK);
926
+ if (turn.userText)
927
+ lines.push(
928
+ ...this.detailSection("User", [this.readablePromptText(turn.userText)]),
929
+ );
930
+ if (turn.thinking)
931
+ lines.push(...this.detailSection("Thinking", [turn.thinking]));
932
+ if (turn.assistantText)
933
+ lines.push(...this.detailSection("Assistant", [turn.assistantText]));
458
934
  for (const [index, tool] of turn.toolCalls.entries()) {
459
- fields.push(
460
- ` tool ${String(index + 1).padEnd(9, " ")} ${tool.name} · ${tool.id}${tool.resultError ? " · error" : ""}`,
935
+ const content = [
936
+ ...(tool.arguments === undefined
937
+ ? []
938
+ : ["Input", ...this.readableValueLines(tool.arguments, " ")]),
939
+ ...(tool.result === undefined
940
+ ? []
941
+ : ["Result", ...this.readableValueLines(tool.result, " ")]),
942
+ `call id: ${tool.id}`,
943
+ ];
944
+ lines.push(
945
+ ...this.detailSection(
946
+ `Tool ${index + 1} (${tool.name}${tool.resultError ? ", Error" : ""})`,
947
+ content,
948
+ ),
461
949
  );
462
- if (tool.arguments !== undefined)
463
- fields.push(` arguments ${value(tool.arguments)}`);
464
- if (tool.result !== undefined) fields.push(` result ${value(tool.result)}`);
465
- }
466
- if (turn.unmatchedToolResults > 0)
467
- fields.push(` unmatched ${turn.unmatchedToolResults}`);
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, "…"));
950
+ }
951
+ const execution = [
952
+ ...(!turn.thinking ? ["thinking: not persisted"] : []),
953
+ ...(turn.model ? [`model: ${turn.model}`] : []),
954
+ ...(turn.stopReason ? [`stop reason: ${turn.stopReason}`] : []),
955
+ ...(turn.error ? [`error: ${turn.error}`] : []),
956
+ ...(turn.usage === undefined
957
+ ? []
958
+ : ["Usage", ...this.readableValueLines(turn.usage, " ")]),
959
+ ...(turn.unmatchedToolResults > 0
960
+ ? [`unmatched tool results: ${turn.unmatchedToolResults}`]
961
+ : []),
962
+ ];
963
+ if (execution.length > 0)
964
+ lines.push(...this.detailSection("Execution", execution));
965
+ if (turn.diagnostics.length > 0)
966
+ lines.push(...this.detailSection("Diagnostics", turn.diagnostics));
967
+ const provenance = [
968
+ "Session",
969
+ ` ${turn.sessionFile}`,
970
+ ...(turn.sessionTruncated ? [" turn list truncated"] : []),
971
+ ...(turn.promptFile
972
+ ? [
973
+ "Prompt",
974
+ ` ${turn.promptFile}${turn.promptBytes ? ` (${turn.promptBytes} bytes)` : ""}`,
975
+ ]
976
+ : []),
977
+ ];
978
+ lines.push(...this.detailSection("Provenance", provenance));
979
+ return lines.length > 0 ? lines : [" No persisted turn evidence"];
476
980
  }
477
981
 
478
- private turnItems(): Inspector.ActorInspectorTurnItem[] {
982
+ private readablePromptText(text: string): string {
983
+ const wrapped = /^\s*<file\b[^>]*>\s*([\s\S]*?)\s*<\/file>\s*$/i.exec(text);
984
+ return wrapped?.[1]?.trim() || text;
985
+ }
986
+
987
+ private allTurnItems(): Inspector.ActorInspectorTurnItem[] {
479
988
  if (!this.selectedRun) return [];
989
+ if (this.turnCache?.run === this.selectedRun) return this.turnCache.turns;
990
+ const turns = this.readTurns(path.join(this.stateRoot, this.selectedRun));
991
+ this.turnCache = { run: this.selectedRun, turns };
992
+ return turns;
993
+ }
994
+
995
+ private turnItems(): Inspector.ActorInspectorTurnItem[] {
480
996
  const selectedSubagent = this.subagents()[this.subagentIndex];
481
- return Inspector.readActorInspectorTurns(
482
- path.join(this.stateRoot, this.selectedRun),
483
- ).filter(
484
- (turn) =>
485
- !selectedSubagent ||
486
- selectedSubagent === "all" ||
487
- turn.commandId === selectedSubagent,
488
- );
997
+ return this.allTurnItems()
998
+ .filter(
999
+ (turn) =>
1000
+ !selectedSubagent ||
1001
+ selectedSubagent === "all" ||
1002
+ turn.commandId === selectedSubagent,
1003
+ )
1004
+ .reverse();
489
1005
  }
490
1006
 
491
1007
  private turnRows(): string[] {
492
- return this.turnItems().map(
493
- (turn) =>
494
- `${turn.commandId}${turn.stage ? `/${turn.stage}` : ""} ${turn.provider ?? "unknown"}/${turn.model ?? "unknown"} tools:${turn.toolCalls.length} ${turn.assistantText ?? turn.userText ?? "(no text)"}`,
495
- );
1008
+ return this.turnItems().map((turn) => {
1009
+ const commandNumber = /^command-(\d+)$/.exec(turn.commandId)?.[1];
1010
+ const subagent = commandNumber
1011
+ ? `Subagent ${Number.parseInt(commandNumber, 10)}`
1012
+ : `Subagent ${turn.commandId}`;
1013
+ const stage =
1014
+ turn.stage && turn.stage !== "subagent" && turn.stage !== "command"
1015
+ ? ` (${turn.stage})`
1016
+ : "";
1017
+ const toolNames = [...new Set(turn.toolCalls.map((tool) => tool.name))];
1018
+ const action = turn.toolCalls.length === 0
1019
+ ? ""
1020
+ : turn.toolCalls.length === 1
1021
+ ? ` (${toolNames[0]})`
1022
+ : toolNames.length === 1
1023
+ ? ` (${toolNames[0]}, ${turn.toolCalls.length} calls)`
1024
+ : turn.toolCalls.length <= 2
1025
+ ? ` (${toolNames.join(", ")})`
1026
+ : ` (${turn.toolCalls.length} tools)`;
1027
+ const failed = turn.error || turn.toolCalls.some((tool) => tool.resultError)
1028
+ ? " (error)"
1029
+ : "";
1030
+ const preview = this.readablePromptText(
1031
+ turn.assistantText ?? turn.userText ?? "(no text)",
1032
+ ).replaceAll(/\s+/g, " ").trim();
1033
+ return `#${turn.index} ${subagent}${stage} ${turn.model ?? "unknown model"}${action}${failed} ${preview}`;
1034
+ });
496
1035
  }
497
1036
 
498
1037
  private subagents(): string[] {
499
1038
  if (!this.selectedRun || this.tab !== "turns") return ["all"];
500
- const ids = Inspector.readActorInspectorTurns(
501
- path.join(this.stateRoot, this.selectedRun),
502
- ).map((turn) => turn.commandId);
1039
+ const ids = this.allTurnItems().map((turn) => turn.commandId);
503
1040
  return ["all", ...new Set(ids)];
504
1041
  }
505
1042
 
@@ -548,11 +1085,10 @@ export class ActorInspectorOverlay {
548
1085
  private applyMenuOption(value?: string): void {
549
1086
  if (!value) return;
550
1087
  if (this.menuLevel === "run") {
551
- this.selectedRun = value;
552
- this.runIndex = Math.max(0, this.runs().findIndex((run) => run.run === value));
553
- this.rowIndex = 0;
554
- this.subagentIndex = 0;
555
- this.communicationFrom = "all";
1088
+ this.selectRun(
1089
+ value,
1090
+ Math.max(0, this.runs().findIndex((run) => run.run === value)),
1091
+ );
556
1092
  this.focus = "runs";
557
1093
  return;
558
1094
  }
@@ -594,16 +1130,37 @@ export class ActorInspectorOverlay {
594
1130
  parentIndex?: number,
595
1131
  omitLeftBorder = false,
596
1132
  omitRightBorder = false,
1133
+ maxVisibleOptions = options.length,
597
1134
  ): string[] {
598
- const contentWidth = Math.max(...options.map((option) => option.length + 4));
599
- const border = (left: string, right: string) =>
1135
+ const visibleLimit = Math.max(1, Math.min(maxVisibleOptions, options.length));
1136
+ const anchorIndex = Math.max(
1137
+ 0,
1138
+ Math.min(
1139
+ options.length - 1,
1140
+ focusedIndex >= 0 ? focusedIndex : (parentIndex ?? 0),
1141
+ ),
1142
+ );
1143
+ const maxStart = Math.max(0, options.length - visibleLimit);
1144
+ const start = Math.max(
1145
+ 0,
1146
+ Math.min(anchorIndex - Math.floor(visibleLimit / 2), maxStart),
1147
+ );
1148
+ const visibleOptions = options.slice(start, start + visibleLimit);
1149
+ const hiddenAbove = start > 0;
1150
+ const hiddenBelow = start + visibleOptions.length < options.length;
1151
+ const contentWidth = Math.max(
1152
+ 4,
1153
+ ...options.map((option) => visibleWidth(option) + 4),
1154
+ );
1155
+ const border = (left: string, right: string, marker = "") =>
600
1156
  this.theme.fg(
601
1157
  "border",
602
- `${omitLeftBorder ? "" : left}${"─".repeat(contentWidth)}${omitRightBorder ? "" : right}`,
1158
+ `${omitLeftBorder ? "" : left}${marker}${"─".repeat(Math.max(0, contentWidth - visibleWidth(marker)))}${omitRightBorder ? "" : right}`,
603
1159
  );
604
1160
  return [
605
- border("╭", "╮"),
606
- ...options.map((option, index) => {
1161
+ border("╭", "╮", hiddenAbove ? "↑" : ""),
1162
+ ...visibleOptions.map((option, localIndex) => {
1163
+ const index = start + localIndex;
607
1164
  const focused = index === focusedIndex;
608
1165
  const row = this.fit(`${focused ? " ▶ " : " "}${option}`, contentWidth);
609
1166
  const colored = focused || option === current || index === parentIndex
@@ -614,7 +1171,7 @@ export class ActorInspectorOverlay {
614
1171
  : colored;
615
1172
  return `${omitLeftBorder ? "" : this.theme.fg("border", "│")}${styled}${omitRightBorder ? "" : this.theme.fg("border", "│")}`;
616
1173
  }),
617
- border("╰", "╯"),
1174
+ border("╰", "╯", hiddenBelow ? "↓" : ""),
618
1175
  ];
619
1176
  }
620
1177
 
@@ -622,12 +1179,16 @@ export class ActorInspectorOverlay {
622
1179
  const filters = this.filterOptions().map(
623
1180
  (filter, index) => `${filter}: ${this.filterValue(index)}`,
624
1181
  );
1182
+ const maxVisibleOptions = Math.max(1, this.contentViewportRows() - 1);
625
1183
  const parentFocus = this.menuLevel === "filter" ? this.selectorIndex : -1;
626
1184
  const parent = this.renderMenuBox(
627
1185
  filters,
628
1186
  parentFocus,
629
1187
  undefined,
630
1188
  this.menuLevel === "value" ? this.filterControlIndex : undefined,
1189
+ false,
1190
+ false,
1191
+ maxVisibleOptions,
631
1192
  );
632
1193
  if (this.menuLevel !== "value") return parent;
633
1194
  const parentShared = this.renderMenuBox(
@@ -637,11 +1198,16 @@ export class ActorInspectorOverlay {
637
1198
  this.filterControlIndex,
638
1199
  false,
639
1200
  true,
1201
+ maxVisibleOptions,
640
1202
  );
641
1203
  const child = this.renderMenuBox(
642
1204
  this.valueOptions(),
643
1205
  this.selectorIndex,
644
1206
  this.currentFilterValue(),
1207
+ undefined,
1208
+ false,
1209
+ false,
1210
+ maxVisibleOptions,
645
1211
  );
646
1212
  return Array.from({ length: Math.max(parent.length, child.length) }, (_, index) =>
647
1213
  child[index]
@@ -661,6 +1227,10 @@ export class ActorInspectorOverlay {
661
1227
  if (this.detailOpen) this.focus = "detail";
662
1228
  }
663
1229
 
1230
+ private backDetail(): void {
1231
+ this.closeDetail();
1232
+ }
1233
+
664
1234
  private closeDetail(): void {
665
1235
  this.detailOpen = false;
666
1236
  this.detailCommunication = undefined;