@llblab/pi-actors 0.41.1 → 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 (147) hide show
  1. package/AGENTS.md +11 -6
  2. package/BACKLOG.md +2 -62
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +22 -4
  5. package/dist/index.js +25 -121
  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 +20 -3
  25. package/dist/lib/inspector-overlay.js +275 -74
  26. package/dist/lib/inspector.d.ts +9 -0
  27. package/dist/lib/inspector.js +48 -0
  28. package/dist/lib/observability.d.ts +1 -0
  29. package/dist/lib/observability.js +11 -1
  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-spawn.js +4 -1
  86. package/dist/lib/tools.d.ts +1 -0
  87. package/dist/lib/tools.js +1 -0
  88. package/dist/recipes/draft-review.json +24 -0
  89. package/dist/recipes/tool-review.json +24 -0
  90. package/dist/scripts/release-gates.mjs +165 -0
  91. package/dist/skills/actors/SKILL.md +9 -8
  92. package/dist/skills/swarm/SKILL.md +1 -1
  93. package/docs/actor-inspector.md +20 -11
  94. package/docs/async-runs.md +9 -1
  95. package/docs/recipe-library.md +7 -3
  96. package/docs/template-recipes.md +4 -11
  97. package/docs/tool-registry.md +11 -4
  98. package/index.ts +27 -142
  99. package/lib/async-runs.ts +218 -62
  100. package/lib/automatic-review-runtime.ts +135 -0
  101. package/lib/draft-consolidation-transaction.ts +821 -0
  102. package/lib/draft-consolidation.ts +181 -0
  103. package/lib/draft-review.ts +325 -0
  104. package/lib/draft-sleep.ts +576 -0
  105. package/lib/file-state.ts +143 -19
  106. package/lib/inspector-actions.ts +79 -0
  107. package/lib/inspector-command.ts +54 -0
  108. package/lib/inspector-overlay.ts +325 -91
  109. package/lib/inspector.ts +72 -0
  110. package/lib/observability.ts +14 -1
  111. package/lib/paths.ts +43 -0
  112. package/lib/recipes-discovery.ts +34 -26
  113. package/lib/recipes-usage.ts +569 -40
  114. package/lib/review-control.ts +137 -0
  115. package/lib/review-diagnostics.ts +164 -0
  116. package/lib/review-projection.ts +200 -0
  117. package/lib/run-ui-runtime.ts +153 -0
  118. package/lib/runs-artifacts.ts +1 -1
  119. package/lib/runs-control.ts +49 -5
  120. package/lib/runs-identity.ts +1 -1
  121. package/lib/runs-index.ts +57 -21
  122. package/lib/runs-mailbox.ts +1 -1
  123. package/lib/runs-messages.ts +1 -1
  124. package/lib/runs-outbox.ts +1 -1
  125. package/lib/runs-ownership.ts +1 -1
  126. package/lib/runs-parent-teardown.ts +257 -0
  127. package/lib/runs-process.ts +1 -1
  128. package/lib/runs-retention.ts +1 -1
  129. package/lib/runs-start.ts +12 -68
  130. package/lib/runs-status.ts +12 -8
  131. package/lib/runtime.ts +34 -17
  132. package/lib/tool-review-lineage-transaction.ts +881 -0
  133. package/lib/tool-review-lineage.ts +145 -0
  134. package/lib/tool-review-scheduler.ts +635 -0
  135. package/lib/tool-review-transaction.ts +563 -0
  136. package/lib/tool-review.ts +270 -0
  137. package/lib/tools-inspect.ts +33 -3
  138. package/lib/tools-local.ts +8 -2
  139. package/lib/tools-message.ts +45 -30
  140. package/lib/tools-spawn.ts +8 -1
  141. package/lib/tools.ts +5 -0
  142. package/package.json +3 -2
  143. package/recipes/draft-review.json +24 -0
  144. package/recipes/tool-review.json +24 -0
  145. package/scripts/release-gates.mjs +165 -0
  146. package/skills/actors/SKILL.md +9 -8
  147. package/skills/swarm/SKILL.md +1 -1
@@ -12,21 +12,36 @@ import {
12
12
  matchesKey,
13
13
  truncateToWidth,
14
14
  visibleWidth,
15
- wrapTextWithAnsi,
16
15
  type TUI,
17
16
  } from "@earendil-works/pi-tui";
18
17
 
19
18
  import * as Inspector from "./inspector.ts";
20
19
 
21
- 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
+ ];
22
27
 
23
28
  type DetailLogicalLine = string | { sectionBreak: true };
24
29
 
25
30
  const DETAIL_SECTION_BREAK: DetailLogicalLine = { sectionBreak: true };
26
31
 
32
+ export interface ActorInspectorOverlayActionResult {
33
+ ok: boolean;
34
+ message: string;
35
+ }
36
+
27
37
  export interface ActorInspectorOverlayOptions {
28
38
  done: () => void;
39
+ killRun?: (
40
+ run: string,
41
+ runInstanceId: string,
42
+ ) => ActorInspectorOverlayActionResult;
29
43
  ownerId: string;
44
+ readTurns?: (stateDir: string) => Inspector.ActorInspectorTurnItem[];
30
45
  stateRoot: string;
31
46
  theme: Theme;
32
47
  tui: TUI;
@@ -34,7 +49,12 @@ export interface ActorInspectorOverlayOptions {
34
49
 
35
50
  export class ActorInspectorOverlay {
36
51
  private readonly done: () => void;
52
+ private readonly killRun?: (
53
+ run: string,
54
+ runInstanceId: string,
55
+ ) => ActorInspectorOverlayActionResult;
37
56
  private readonly ownerId: string;
57
+ private readonly readTurns: (stateDir: string) => Inspector.ActorInspectorTurnItem[];
38
58
  private readonly stateRoot: string;
39
59
  private readonly theme: Theme;
40
60
  private readonly tui: TUI;
@@ -48,24 +68,32 @@ export class ActorInspectorOverlay {
48
68
  private detailOpen = false;
49
69
  private detailScroll = 0;
50
70
  private detailTurn?: Inspector.ActorInspectorTurnItem;
51
- private detailView: "evidence" | "transcript" = "evidence";
71
+ private feedback?: ActorInspectorOverlayActionResult;
72
+ private killConfirmation?: { run: string; runInstanceId: string };
52
73
  private readonly readKeys = new Set<string>();
53
- private focus: "runs" | "tabs" | "list" | "detail" | "select" = "tabs";
74
+ private focus: "runs" | "tabs" | "recipe" | "list" | "detail" | "select" = "tabs";
54
75
  private filterControlIndex = 0;
55
76
  private menuLevel: "run" | "filter" | "value" = "filter";
56
77
  private selectorIndex = 0;
78
+ private recipeScroll = 0;
57
79
  private rowIndex = 0;
58
80
  private selectedRun?: string;
59
81
  private subagentIndex = 0;
60
- private tab: ActorInspectorOverlayTab = "communications";
82
+ private tab: ActorInspectorOverlayTab = "recipe";
83
+ private turnCache?: { run: string; turns: Inspector.ActorInspectorTurnItem[] };
61
84
 
62
85
  constructor(options: ActorInspectorOverlayOptions) {
63
86
  this.done = options.done;
87
+ this.killRun = options.killRun;
64
88
  this.ownerId = options.ownerId;
89
+ this.readTurns = options.readTurns ?? Inspector.readActorInspectorTurns;
65
90
  this.stateRoot = options.stateRoot;
66
91
  this.theme = options.theme;
67
92
  this.tui = options.tui;
68
- 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);
69
97
  this.refreshTimer.unref?.();
70
98
  }
71
99
 
@@ -76,6 +104,21 @@ export class ActorInspectorOverlay {
76
104
  this.done();
77
105
  return;
78
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
+ }
79
122
  if (matchesKey(data, "escape")) {
80
123
  if (this.focus === "select") {
81
124
  if (this.menuLevel === "value") {
@@ -83,6 +126,7 @@ export class ActorInspectorOverlay {
83
126
  this.selectorIndex = this.filterControlIndex;
84
127
  } else this.focus = this.menuLevel === "run" ? "runs" : "tabs";
85
128
  } else if (this.focus === "detail") this.backDetail();
129
+ else if (this.focus === "recipe") this.focus = "tabs";
86
130
  else this.done();
87
131
  this.tui.requestRender();
88
132
  return;
@@ -102,18 +146,27 @@ export class ActorInspectorOverlay {
102
146
  this.tui.requestRender();
103
147
  return;
104
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
+ }
105
162
  if (this.focus === "detail") {
106
163
  if (matchesKey(data, "up")) this.detailScroll = Math.max(0, this.detailScroll - 1);
107
164
  else if (matchesKey(data, "down")) this.detailScroll += 1;
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();
108
169
  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
- }
117
170
  this.tui.requestRender();
118
171
  return;
119
172
  }
@@ -124,16 +177,30 @@ export class ActorInspectorOverlay {
124
177
  else if (matchesKey(data, "return") && runs.length > 0) this.openRunMenu();
125
178
  } else if (this.focus === "tabs") {
126
179
  if (matchesKey(data, "left") || matchesKey(data, "right")) {
127
- 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
+ ]!;
128
185
  this.filterControlIndex = 0;
186
+ this.recipeScroll = 0;
129
187
  this.rowIndex = 0;
130
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";
131
193
  else if (matchesKey(data, "down") && this.listItemCount() > 0)
132
194
  this.focus = "list";
133
195
  else if (matchesKey(data, "return")) this.openFilterMenu();
134
196
  } else if (this.focus === "list") {
135
197
  const count = this.tab === "communications" ? this.communicationPreviews().length : this.turnItems().length;
136
- 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")) {
137
204
  if (this.rowIndex === 0) this.focus = "tabs";
138
205
  else this.rowIndex -= 1;
139
206
  } else if (matchesKey(data, "down"))
@@ -194,11 +261,26 @@ export class ActorInspectorOverlay {
194
261
  );
195
262
  } else lines.push(this.border("├", "", "┤", innerWidth));
196
263
  this.contentStripeIndices = [];
197
- const content = this.detailOpen
198
- ? this.renderDetail(innerWidth)
199
- : this.selectedRun
200
- ? this.renderTimeline(innerWidth)
201
- : this.renderRunSelector(innerWidth);
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;
202
284
  const contentViewportRows = this.contentViewportRows();
203
285
  const selectorRows = selector.slice(1);
204
286
  for (let index = 0; index < Math.max(content.length, contentViewportRows); index += 1) {
@@ -256,7 +338,9 @@ export class ActorInspectorOverlay {
256
338
 
257
339
  private selectRun(run: string, index: number): void {
258
340
  this.selectedRun = run;
341
+ this.turnCache = undefined;
259
342
  this.runIndex = index;
343
+ this.recipeScroll = 0;
260
344
  this.rowIndex = 0;
261
345
  this.subagentIndex = 0;
262
346
  this.communicationFrom = "all";
@@ -276,7 +360,74 @@ export class ActorInspectorOverlay {
276
360
  if (next) this.selectRun(next.run, nextIndex);
277
361
  }
278
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
+
279
429
  private listItemCount(): number {
430
+ if (this.tab === "recipe") return 0;
280
431
  return this.tab === "communications"
281
432
  ? this.communicationPreviews().length
282
433
  : this.turnItems().length;
@@ -287,7 +438,11 @@ export class ActorInspectorOverlay {
287
438
  const runControl = stripVTControlCharacters(this.renderRunControl());
288
439
  return Math.max(0, runControl.indexOf("Run:") - 2);
289
440
  }
290
- const label = this.tab === "communications" ? "Messages" : "Turns";
441
+ const label = this.tab === "communications"
442
+ ? "Messages"
443
+ : this.tab === "turns"
444
+ ? "Turns"
445
+ : "Recipe";
291
446
  const tabs = stripVTControlCharacters(this.renderTabs());
292
447
  return Math.max(0, tabs.indexOf(label) - 2);
293
448
  }
@@ -295,18 +450,25 @@ export class ActorInspectorOverlay {
295
450
  private renderKeyHints(): string {
296
451
  const hint = (keys: string, description: string) =>
297
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")}`;
298
455
  if (this.focus === "select")
299
456
  return this.menuLevel === "value"
300
457
  ? ` ${hint("↑↓", "option")} ${hint("enter", "apply")} ${hint("←/esc", "back")}`
301
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")}`;
302
461
  if (this.focus === "detail")
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")}`;
462
+ return ` ${hint("↑↓/pgup/pgdn", "scroll")} ${hint("←/esc", "back")}`;
306
463
  if (this.focus === "list")
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")}`;
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
+ }
310
472
  return ` ${hint("←→", "navigate")} ${hint("↑↓", "change row")} ${hint("enter", "select")} ${hint("esc", "close")}`;
311
473
  }
312
474
 
@@ -359,7 +521,7 @@ export class ActorInspectorOverlay {
359
521
  const subagent = this.subagents()[this.subagentIndex] ?? "all";
360
522
  const messagesLabel = `Messages${messageFilters.length > 0 ? ` (${messageFilters.join(", ")})` : ""}`;
361
523
  const turnsLabel = `Turns${subagent === "all" ? "" : ` (${subagent})`}`;
362
- return `${tab("communications", messagesLabel)} ${tab("turns", turnsLabel)}`;
524
+ return `${tab("recipe", "Recipe")} ${tab("communications", messagesLabel)} ${tab("turns", turnsLabel)}`;
363
525
  }
364
526
 
365
527
  private renderRunSelector(width: number): string[] {
@@ -384,6 +546,7 @@ export class ActorInspectorOverlay {
384
546
  this.selectedRun = undefined;
385
547
  return [this.theme.fg("warning", " Selected run is no longer owned by this session")];
386
548
  }
549
+ if (this.tab === "recipe") return this.renderRecipe(width);
387
550
  const rows =
388
551
  this.tab === "communications"
389
552
  ? this.communicationRows()
@@ -431,6 +594,51 @@ export class ActorInspectorOverlay {
431
594
  });
432
595
  }
433
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
+
434
642
  private communicationPreviews(): Inspector.ActorInspectorPreview[] {
435
643
  if (!this.selectedRun) return [];
436
644
  const channels =
@@ -496,7 +704,7 @@ export class ActorInspectorOverlay {
496
704
  ["inbox", preview.inbox_status],
497
705
  ["timestamp", preview.timestamp],
498
706
  ];
499
- return fields
707
+ const lines = fields
500
708
  .filter(([, value]) => value !== undefined)
501
709
  .map(([label, value]) =>
502
710
  truncateToWidth(
@@ -505,12 +713,14 @@ export class ActorInspectorOverlay {
505
713
  "…",
506
714
  ),
507
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);
508
720
  }
509
721
  const turn = this.detailTurn;
510
722
  if (!turn) return [this.theme.fg("warning", " Turn row is no longer available")];
511
- const logicalLines = this.detailView === "transcript"
512
- ? this.turnTranscriptLines(turn)
513
- : this.turnEvidenceLines(turn);
723
+ const logicalLines = this.turnEvidenceLines(turn);
514
724
  const wrapped = this.wrapDetailLines(logicalLines, width);
515
725
  const viewportRows = this.contentViewportRows();
516
726
  const maxStart = Math.max(0, wrapped.lines.length - viewportRows);
@@ -543,11 +753,39 @@ export class ActorInspectorOverlay {
543
753
  const leading = physicalLine.match(/^ */)?.[0] ?? "";
544
754
  const content = physicalLine.slice(leading.length);
545
755
  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
- );
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
+ }
551
789
  for (const visualLine of wrapped) {
552
790
  if (!visualLine.trim()) continue;
553
791
  wrappedLines.push(visualLine);
@@ -558,6 +796,37 @@ export class ActorInspectorOverlay {
558
796
  return { lines: wrappedLines, stripeIndices };
559
797
  }
560
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
+
561
830
  private detailSection(
562
831
  title: string,
563
832
  content: string[],
@@ -612,9 +881,14 @@ export class ActorInspectorOverlay {
612
881
  }
613
882
  if (!item || typeof item !== "object")
614
883
  return [`${indent}- ${String(item)}`];
884
+ const [first = "", ...rest] = this.readableValueLines(
885
+ item,
886
+ `${indent} `,
887
+ depth + 1,
888
+ );
615
889
  return [
616
- `${indent}-`,
617
- ...this.readableValueLines(item, `${indent} `, depth + 1),
890
+ `${indent}- ${first.trimStart()}`,
891
+ ...rest,
618
892
  ];
619
893
  });
620
894
  }
@@ -650,7 +924,9 @@ export class ActorInspectorOverlay {
650
924
  : "";
651
925
  lines.push(` Subagent ${subagent}${stage}`, DETAIL_SECTION_BREAK);
652
926
  if (turn.userText)
653
- lines.push(...this.detailSection("User", [turn.userText]));
927
+ lines.push(
928
+ ...this.detailSection("User", [this.readablePromptText(turn.userText)]),
929
+ );
654
930
  if (turn.thinking)
655
931
  lines.push(...this.detailSection("Thinking", [turn.thinking]));
656
932
  if (turn.assistantText)
@@ -698,12 +974,6 @@ export class ActorInspectorOverlay {
698
974
  ` ${turn.promptFile}${turn.promptBytes ? ` (${turn.promptBytes} bytes)` : ""}`,
699
975
  ]
700
976
  : []),
701
- ...(turn.recipeContext === undefined
702
- ? []
703
- : [
704
- "Recipe Context",
705
- ...this.readableValueLines(turn.recipeContext, " "),
706
- ]),
707
977
  ];
708
978
  lines.push(...this.detailSection("Provenance", provenance));
709
979
  return lines.length > 0 ? lines : [" No persisted turn evidence"];
@@ -714,44 +984,17 @@ export class ActorInspectorOverlay {
714
984
  return wrapped?.[1]?.trim() || text;
715
985
  }
716
986
 
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
- ),
743
- );
744
- }
745
- if (turn.error) lines.push(...this.detailSection("Error", [turn.error]));
746
- return lines.length > 0 ? lines : [" No readable turn content"];
987
+ private allTurnItems(): Inspector.ActorInspectorTurnItem[] {
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;
747
993
  }
748
994
 
749
995
  private turnItems(): Inspector.ActorInspectorTurnItem[] {
750
- if (!this.selectedRun) return [];
751
996
  const selectedSubagent = this.subagents()[this.subagentIndex];
752
- return Inspector.readActorInspectorTurns(
753
- path.join(this.stateRoot, this.selectedRun),
754
- )
997
+ return this.allTurnItems()
755
998
  .filter(
756
999
  (turn) =>
757
1000
  !selectedSubagent ||
@@ -793,9 +1036,7 @@ export class ActorInspectorOverlay {
793
1036
 
794
1037
  private subagents(): string[] {
795
1038
  if (!this.selectedRun || this.tab !== "turns") return ["all"];
796
- const ids = Inspector.readActorInspectorTurns(
797
- path.join(this.stateRoot, this.selectedRun),
798
- ).map((turn) => turn.commandId);
1039
+ const ids = this.allTurnItems().map((turn) => turn.commandId);
799
1040
  return ["all", ...new Set(ids)];
800
1041
  }
801
1042
 
@@ -982,17 +1223,11 @@ export class ActorInspectorOverlay {
982
1223
  if (preview) this.readKeys.add(Inspector.inspectorPreviewReadKey(preview));
983
1224
  } else this.detailTurn = this.turnItems()[this.rowIndex];
984
1225
  this.detailScroll = 0;
985
- this.detailView = "evidence";
986
1226
  this.detailOpen = Boolean(this.detailCommunication || this.detailTurn);
987
1227
  if (this.detailOpen) this.focus = "detail";
988
1228
  }
989
1229
 
990
1230
  private backDetail(): void {
991
- if (this.detailView === "transcript") {
992
- this.detailView = "evidence";
993
- this.detailScroll = 0;
994
- return;
995
- }
996
1231
  this.closeDetail();
997
1232
  }
998
1233
 
@@ -1001,7 +1236,6 @@ export class ActorInspectorOverlay {
1001
1236
  this.detailCommunication = undefined;
1002
1237
  this.detailTurn = undefined;
1003
1238
  this.detailScroll = 0;
1004
- this.detailView = "evidence";
1005
1239
  this.focus = "list";
1006
1240
  }
1007
1241