@llblab/pi-actors 0.41.1 → 0.42.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.
Files changed (157) hide show
  1. package/AGENTS.md +11 -6
  2. package/BACKLOG.md +1 -65
  3. package/CHANGELOG.md +25 -0
  4. package/README.md +24 -4
  5. package/dist/index.js +25 -121
  6. package/dist/lib/async-runs.d.ts +37 -5
  7. package/dist/lib/async-runs.js +181 -51
  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/command-templates.js +1 -1
  11. package/dist/lib/draft-consolidation-transaction.d.ts +65 -0
  12. package/dist/lib/draft-consolidation-transaction.js +610 -0
  13. package/dist/lib/draft-consolidation.d.ts +35 -0
  14. package/dist/lib/draft-consolidation.js +126 -0
  15. package/dist/lib/draft-review.d.ts +56 -0
  16. package/dist/lib/draft-review.js +254 -0
  17. package/dist/lib/draft-sleep.d.ts +65 -0
  18. package/dist/lib/draft-sleep.js +468 -0
  19. package/dist/lib/file-state.d.ts +8 -1
  20. package/dist/lib/file-state.js +115 -18
  21. package/dist/lib/inspector-actions.d.ts +16 -0
  22. package/dist/lib/inspector-actions.js +57 -0
  23. package/dist/lib/inspector-command.d.ts +7 -0
  24. package/dist/lib/inspector-command.js +37 -0
  25. package/dist/lib/inspector-overlay.d.ts +23 -3
  26. package/dist/lib/inspector-overlay.js +327 -70
  27. package/dist/lib/inspector.d.ts +9 -0
  28. package/dist/lib/inspector.js +49 -1
  29. package/dist/lib/observability.d.ts +13 -0
  30. package/dist/lib/observability.js +160 -6
  31. package/dist/lib/paths.d.ts +7 -0
  32. package/dist/lib/paths.js +28 -0
  33. package/dist/lib/recipes-discovery.js +32 -24
  34. package/dist/lib/recipes-usage.d.ts +18 -6
  35. package/dist/lib/recipes-usage.js +445 -34
  36. package/dist/lib/review-control.d.ts +14 -0
  37. package/dist/lib/review-control.js +111 -0
  38. package/dist/lib/review-diagnostics.d.ts +11 -0
  39. package/dist/lib/review-diagnostics.js +148 -0
  40. package/dist/lib/review-projection.d.ts +14 -0
  41. package/dist/lib/review-projection.js +170 -0
  42. package/dist/lib/run-ui-runtime.d.ts +18 -0
  43. package/dist/lib/run-ui-runtime.js +123 -0
  44. package/dist/lib/runs-artifacts.d.ts +1 -1
  45. package/dist/lib/runs-artifacts.js +1 -1
  46. package/dist/lib/runs-control.d.ts +10 -2
  47. package/dist/lib/runs-control.js +37 -7
  48. package/dist/lib/runs-identity.d.ts +1 -1
  49. package/dist/lib/runs-identity.js +1 -1
  50. package/dist/lib/runs-index.d.ts +11 -2
  51. package/dist/lib/runs-index.js +46 -23
  52. package/dist/lib/runs-mailbox.d.ts +1 -1
  53. package/dist/lib/runs-mailbox.js +1 -1
  54. package/dist/lib/runs-messages.d.ts +1 -1
  55. package/dist/lib/runs-messages.js +1 -1
  56. package/dist/lib/runs-outbox.d.ts +1 -1
  57. package/dist/lib/runs-outbox.js +1 -1
  58. package/dist/lib/runs-ownership.d.ts +1 -1
  59. package/dist/lib/runs-ownership.js +18 -4
  60. package/dist/lib/runs-parent-teardown.d.ts +51 -0
  61. package/dist/lib/runs-parent-teardown.js +172 -0
  62. package/dist/lib/runs-process.d.ts +1 -1
  63. package/dist/lib/runs-process.js +5 -4
  64. package/dist/lib/runs-retention.d.ts +1 -1
  65. package/dist/lib/runs-retention.js +1 -1
  66. package/dist/lib/runs-start.d.ts +5 -3
  67. package/dist/lib/runs-start.js +7 -48
  68. package/dist/lib/runs-status.d.ts +5 -3
  69. package/dist/lib/runs-status.js +7 -6
  70. package/dist/lib/runtime.d.ts +7 -1
  71. package/dist/lib/runtime.js +32 -18
  72. package/dist/lib/tool-review-lineage-transaction.d.ts +27 -0
  73. package/dist/lib/tool-review-lineage-transaction.js +597 -0
  74. package/dist/lib/tool-review-lineage.d.ts +24 -0
  75. package/dist/lib/tool-review-lineage.js +98 -0
  76. package/dist/lib/tool-review-scheduler.d.ts +80 -0
  77. package/dist/lib/tool-review-scheduler.js +494 -0
  78. package/dist/lib/tool-review-transaction.d.ts +50 -0
  79. package/dist/lib/tool-review-transaction.js +362 -0
  80. package/dist/lib/tool-review.d.ts +56 -0
  81. package/dist/lib/tool-review.js +197 -0
  82. package/dist/lib/tools-inspect.js +28 -4
  83. package/dist/lib/tools-local.js +21 -4
  84. package/dist/lib/tools-message.d.ts +1 -0
  85. package/dist/lib/tools-message.js +29 -17
  86. package/dist/lib/tools-spawn.js +14 -2
  87. package/dist/lib/tools.d.ts +1 -0
  88. package/dist/lib/tools.js +1 -0
  89. package/dist/recipes/draft-review.json +24 -0
  90. package/dist/recipes/tool-review.json +24 -0
  91. package/dist/scripts/async-runner.mjs +9 -9
  92. package/dist/scripts/build-dist.mjs +6 -1
  93. package/dist/scripts/conformance.mjs +6 -1
  94. package/dist/scripts/recipe-utils.mjs +3 -3
  95. package/dist/scripts/release-gates.mjs +165 -0
  96. package/dist/skills/actors/SKILL.md +9 -8
  97. package/dist/skills/swarm/SKILL.md +1 -1
  98. package/docs/actor-inspector.md +20 -11
  99. package/docs/async-runs.md +16 -2
  100. package/docs/recipe-library.md +7 -3
  101. package/docs/template-recipes.md +4 -11
  102. package/docs/tool-registry.md +13 -4
  103. package/index.ts +27 -142
  104. package/lib/async-runs.ts +283 -67
  105. package/lib/automatic-review-runtime.ts +135 -0
  106. package/lib/command-templates.ts +1 -1
  107. package/lib/draft-consolidation-transaction.ts +821 -0
  108. package/lib/draft-consolidation.ts +181 -0
  109. package/lib/draft-review.ts +325 -0
  110. package/lib/draft-sleep.ts +576 -0
  111. package/lib/file-state.ts +143 -19
  112. package/lib/inspector-actions.ts +79 -0
  113. package/lib/inspector-command.ts +54 -0
  114. package/lib/inspector-overlay.ts +377 -91
  115. package/lib/inspector.ts +73 -1
  116. package/lib/observability.ts +194 -5
  117. package/lib/paths.ts +43 -0
  118. package/lib/recipes-discovery.ts +34 -26
  119. package/lib/recipes-usage.ts +569 -40
  120. package/lib/review-control.ts +137 -0
  121. package/lib/review-diagnostics.ts +164 -0
  122. package/lib/review-projection.ts +200 -0
  123. package/lib/run-ui-runtime.ts +153 -0
  124. package/lib/runs-artifacts.ts +1 -1
  125. package/lib/runs-control.ts +69 -6
  126. package/lib/runs-identity.ts +1 -1
  127. package/lib/runs-index.ts +57 -21
  128. package/lib/runs-mailbox.ts +1 -1
  129. package/lib/runs-messages.ts +1 -1
  130. package/lib/runs-outbox.ts +1 -1
  131. package/lib/runs-ownership.ts +23 -4
  132. package/lib/runs-parent-teardown.ts +257 -0
  133. package/lib/runs-process.ts +5 -4
  134. package/lib/runs-retention.ts +1 -1
  135. package/lib/runs-start.ts +13 -68
  136. package/lib/runs-status.ts +17 -8
  137. package/lib/runtime.ts +34 -17
  138. package/lib/tool-review-lineage-transaction.ts +881 -0
  139. package/lib/tool-review-lineage.ts +145 -0
  140. package/lib/tool-review-scheduler.ts +635 -0
  141. package/lib/tool-review-transaction.ts +563 -0
  142. package/lib/tool-review.ts +270 -0
  143. package/lib/tools-inspect.ts +35 -4
  144. package/lib/tools-local.ts +29 -4
  145. package/lib/tools-message.ts +45 -30
  146. package/lib/tools-spawn.ts +22 -2
  147. package/lib/tools.ts +5 -0
  148. package/package.json +5 -3
  149. package/recipes/draft-review.json +24 -0
  150. package/recipes/tool-review.json +24 -0
  151. package/scripts/async-runner.mjs +9 -9
  152. package/scripts/build-dist.mjs +6 -1
  153. package/scripts/conformance.mjs +6 -1
  154. package/scripts/recipe-utils.mjs +3 -3
  155. package/scripts/release-gates.mjs +165 -0
  156. package/skills/actors/SKILL.md +9 -8
  157. 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,33 @@ 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; status: string };
73
+ private killDialogChoice: "cancel" | "kill" = "cancel";
52
74
  private readonly readKeys = new Set<string>();
53
- private focus: "runs" | "tabs" | "list" | "detail" | "select" = "tabs";
75
+ private focus: "runs" | "tabs" | "recipe" | "list" | "detail" | "select" = "tabs";
54
76
  private filterControlIndex = 0;
55
77
  private menuLevel: "run" | "filter" | "value" = "filter";
56
78
  private selectorIndex = 0;
79
+ private recipeScroll = 0;
57
80
  private rowIndex = 0;
58
81
  private selectedRun?: string;
59
82
  private subagentIndex = 0;
60
- private tab: ActorInspectorOverlayTab = "communications";
83
+ private tab: ActorInspectorOverlayTab = "recipe";
84
+ private turnCache?: { run: string; turns: Inspector.ActorInspectorTurnItem[] };
61
85
 
62
86
  constructor(options: ActorInspectorOverlayOptions) {
63
87
  this.done = options.done;
88
+ this.killRun = options.killRun;
64
89
  this.ownerId = options.ownerId;
90
+ this.readTurns = options.readTurns ?? Inspector.readActorInspectorTurns;
65
91
  this.stateRoot = options.stateRoot;
66
92
  this.theme = options.theme;
67
93
  this.tui = options.tui;
68
- this.refreshTimer = setInterval(() => this.tui.requestRender(), 1000);
94
+ this.refreshTimer = setInterval(() => {
95
+ if (this.focus !== "list" && this.focus !== "detail") this.turnCache = undefined;
96
+ this.tui.requestRender();
97
+ }, 1000);
69
98
  this.refreshTimer.unref?.();
70
99
  }
71
100
 
@@ -76,6 +105,31 @@ export class ActorInspectorOverlay {
76
105
  this.done();
77
106
  return;
78
107
  }
108
+ if (this.killConfirmation) {
109
+ const key = data.toLowerCase();
110
+ if (matchesKey(data, "escape") || key === "n") {
111
+ this.cancelKill();
112
+ } else if (
113
+ matchesKey(data, "left") ||
114
+ matchesKey(data, "right") ||
115
+ matchesKey(data, "tab")
116
+ ) {
117
+ this.killDialogChoice =
118
+ this.killDialogChoice === "cancel" ? "kill" : "cancel";
119
+ } else if (key === "y") {
120
+ this.confirmKill();
121
+ } else if (matchesKey(data, "return")) {
122
+ if (this.killDialogChoice === "kill") this.confirmKill();
123
+ else this.cancelKill();
124
+ }
125
+ this.tui.requestRender();
126
+ return;
127
+ }
128
+ if (data.toLowerCase() === "k") {
129
+ this.requestKill(runs);
130
+ this.tui.requestRender();
131
+ return;
132
+ }
79
133
  if (matchesKey(data, "escape")) {
80
134
  if (this.focus === "select") {
81
135
  if (this.menuLevel === "value") {
@@ -83,6 +137,7 @@ export class ActorInspectorOverlay {
83
137
  this.selectorIndex = this.filterControlIndex;
84
138
  } else this.focus = this.menuLevel === "run" ? "runs" : "tabs";
85
139
  } else if (this.focus === "detail") this.backDetail();
140
+ else if (this.focus === "recipe") this.focus = "tabs";
86
141
  else this.done();
87
142
  this.tui.requestRender();
88
143
  return;
@@ -102,18 +157,27 @@ export class ActorInspectorOverlay {
102
157
  this.tui.requestRender();
103
158
  return;
104
159
  }
160
+ if (this.focus === "recipe") {
161
+ if (matchesKey(data, "up")) {
162
+ if (this.recipeScroll === 0) this.focus = "tabs";
163
+ else this.recipeScroll -= 1;
164
+ } else if (matchesKey(data, "down")) this.recipeScroll += 1;
165
+ else if (matchesKey(data, "pageUp"))
166
+ this.recipeScroll = Math.max(0, this.recipeScroll - this.contentViewportRows());
167
+ else if (matchesKey(data, "pageDown"))
168
+ this.recipeScroll += this.contentViewportRows();
169
+ else if (matchesKey(data, "left")) this.focus = "tabs";
170
+ this.tui.requestRender();
171
+ return;
172
+ }
105
173
  if (this.focus === "detail") {
106
174
  if (matchesKey(data, "up")) this.detailScroll = Math.max(0, this.detailScroll - 1);
107
175
  else if (matchesKey(data, "down")) this.detailScroll += 1;
176
+ else if (matchesKey(data, "pageUp"))
177
+ this.detailScroll = Math.max(0, this.detailScroll - this.contentViewportRows());
178
+ else if (matchesKey(data, "pageDown"))
179
+ this.detailScroll += this.contentViewportRows();
108
180
  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
181
  this.tui.requestRender();
118
182
  return;
119
183
  }
@@ -124,16 +188,30 @@ export class ActorInspectorOverlay {
124
188
  else if (matchesKey(data, "return") && runs.length > 0) this.openRunMenu();
125
189
  } else if (this.focus === "tabs") {
126
190
  if (matchesKey(data, "left") || matchesKey(data, "right")) {
127
- this.tab = this.tab === "communications" ? "turns" : "communications";
191
+ const current = INSPECTOR_TABS.indexOf(this.tab);
192
+ const direction = matchesKey(data, "right") ? 1 : -1;
193
+ this.tab = INSPECTOR_TABS[
194
+ (current + direction + INSPECTOR_TABS.length) % INSPECTOR_TABS.length
195
+ ]!;
128
196
  this.filterControlIndex = 0;
197
+ this.recipeScroll = 0;
129
198
  this.rowIndex = 0;
130
199
  } else if (matchesKey(data, "up")) this.focus = "runs";
200
+ else if (
201
+ (matchesKey(data, "down") || matchesKey(data, "return")) &&
202
+ this.tab === "recipe"
203
+ ) this.focus = "recipe";
131
204
  else if (matchesKey(data, "down") && this.listItemCount() > 0)
132
205
  this.focus = "list";
133
206
  else if (matchesKey(data, "return")) this.openFilterMenu();
134
207
  } else if (this.focus === "list") {
135
208
  const count = this.tab === "communications" ? this.communicationPreviews().length : this.turnItems().length;
136
- if (matchesKey(data, "up")) {
209
+ if (matchesKey(data, "left")) this.focus = "tabs";
210
+ else if (matchesKey(data, "pageUp"))
211
+ this.rowIndex = Math.max(0, this.rowIndex - this.contentViewportRows());
212
+ else if (matchesKey(data, "pageDown"))
213
+ this.rowIndex = Math.min(Math.max(0, count - 1), this.rowIndex + this.contentViewportRows());
214
+ else if (matchesKey(data, "up")) {
137
215
  if (this.rowIndex === 0) this.focus = "tabs";
138
216
  else this.rowIndex -= 1;
139
217
  } else if (matchesKey(data, "down"))
@@ -148,6 +226,8 @@ export class ActorInspectorOverlay {
148
226
  this.ensureSelectedRun(this.runs());
149
227
  const safeWidth = Math.max(24, width);
150
228
  const innerWidth = Math.max(1, safeWidth - 2);
229
+ if (this.killConfirmation)
230
+ return this.renderKillDialog(safeWidth, innerWidth);
151
231
  const lines: string[] = [];
152
232
  lines.push(this.border("╭", " Actor Inspector ", "╮", innerWidth));
153
233
  lines.push(this.row(this.renderRunControl(), innerWidth));
@@ -194,11 +274,24 @@ export class ActorInspectorOverlay {
194
274
  );
195
275
  } else lines.push(this.border("├", "", "┤", innerWidth));
196
276
  this.contentStripeIndices = [];
197
- const content = this.detailOpen
198
- ? this.renderDetail(innerWidth)
199
- : this.selectedRun
200
- ? this.renderTimeline(innerWidth)
201
- : this.renderRunSelector(innerWidth);
277
+ const baseContent = this.detailOpen
278
+ ? this.renderDetail(innerWidth)
279
+ : this.selectedRun
280
+ ? this.renderTimeline(innerWidth)
281
+ : this.renderRunSelector(innerWidth);
282
+ const content = this.feedback && !this.killConfirmation
283
+ ? [
284
+ this.theme.fg(
285
+ this.feedback.ok
286
+ ? "success"
287
+ : /^Kill (failed|rejected):/.test(this.feedback.message)
288
+ ? "error"
289
+ : "warning",
290
+ ` ${this.feedback.message}`,
291
+ ),
292
+ ...baseContent.slice(0, Math.max(0, this.contentViewportRows() - 1)),
293
+ ]
294
+ : baseContent;
202
295
  const contentViewportRows = this.contentViewportRows();
203
296
  const selectorRows = selector.slice(1);
204
297
  for (let index = 0; index < Math.max(content.length, contentViewportRows); index += 1) {
@@ -256,7 +349,9 @@ export class ActorInspectorOverlay {
256
349
 
257
350
  private selectRun(run: string, index: number): void {
258
351
  this.selectedRun = run;
352
+ this.turnCache = undefined;
259
353
  this.runIndex = index;
354
+ this.recipeScroll = 0;
260
355
  this.rowIndex = 0;
261
356
  this.subagentIndex = 0;
262
357
  this.communicationFrom = "all";
@@ -276,7 +371,109 @@ export class ActorInspectorOverlay {
276
371
  if (next) this.selectRun(next.run, nextIndex);
277
372
  }
278
373
 
374
+ private requestKill(runs: Inspector.ActorInspectorRunItem[]): void {
375
+ this.feedback = undefined;
376
+ if (this.focus !== "runs") {
377
+ this.feedback = { ok: false, message: "Focus Run before using Kill." };
378
+ return;
379
+ }
380
+ const run = runs.find((item) => item.run === this.selectedRun);
381
+ if (!run) {
382
+ this.feedback = { ok: false, message: "Kill unavailable: no owned run." };
383
+ return;
384
+ }
385
+ if (!this.killRun) {
386
+ this.feedback = { ok: false, message: "Kill unavailable in this Inspector." };
387
+ return;
388
+ }
389
+ if (run.status !== "running") {
390
+ this.feedback = {
391
+ ok: false,
392
+ message: `Kill unavailable: run is ${run.status}.`,
393
+ };
394
+ return;
395
+ }
396
+ if (!run.runInstanceId) {
397
+ this.feedback = {
398
+ ok: false,
399
+ message: "Kill unavailable: run generation unavailable.",
400
+ };
401
+ return;
402
+ }
403
+ this.killConfirmation = {
404
+ run: run.run,
405
+ runInstanceId: run.runInstanceId,
406
+ status: run.status,
407
+ };
408
+ this.killDialogChoice = "cancel";
409
+ }
410
+
411
+ private cancelKill(): void {
412
+ this.feedback = { ok: false, message: "Kill cancelled." };
413
+ this.killConfirmation = undefined;
414
+ this.killDialogChoice = "cancel";
415
+ }
416
+
417
+ private confirmKill(): void {
418
+ const confirmation = this.killConfirmation;
419
+ this.killConfirmation = undefined;
420
+ this.killDialogChoice = "cancel";
421
+ if (!confirmation || !this.killRun) {
422
+ this.feedback = { ok: false, message: "Kill unavailable in this Inspector." };
423
+ return;
424
+ }
425
+ try {
426
+ this.feedback = this.killRun(
427
+ confirmation.run,
428
+ confirmation.runInstanceId,
429
+ );
430
+ } catch (error) {
431
+ const message = error instanceof Error ? error.message : String(error);
432
+ this.feedback = {
433
+ ok: false,
434
+ message: `Kill failed: ${message.replaceAll(/\s+/g, " ").slice(0, 160)}.`,
435
+ };
436
+ }
437
+ }
438
+
439
+ private renderKillDialog(_width: number, innerWidth: number): string[] {
440
+ const confirmation = this.killConfirmation!;
441
+ const totalRows = this.contentViewportRows() + 7;
442
+ const cancel = this.killDialogChoice === "cancel"
443
+ ? this.theme.bg("selectedBg", this.theme.fg("accent", " Cancel "))
444
+ : this.theme.fg("muted", " Cancel ");
445
+ const kill = this.killDialogChoice === "kill"
446
+ ? this.theme.bg("selectedBg", this.theme.fg("error", " Kill actor "))
447
+ : this.theme.fg("error", " Kill actor ");
448
+ const body = [
449
+ "",
450
+ this.theme.fg("error", "Kill this running actor?"),
451
+ "",
452
+ `${this.theme.fg("muted", "Run:")} ${this.theme.fg("accent", `run:${confirmation.run}`)}`,
453
+ `${this.theme.fg("muted", "Current status:")} ${this.theme.fg("warning", confirmation.status)}`,
454
+ "",
455
+ this.theme.fg("text", "This sends canonical control.kill."),
456
+ this.theme.fg("error", "The action is destructive and cannot be undone."),
457
+ "",
458
+ `${cancel} ${kill}`,
459
+ "",
460
+ ];
461
+ const lines = [this.border("╭", " Confirm Actor Kill ", "╮", innerWidth)];
462
+ const availableRows = Math.max(1, totalRows - 2);
463
+ const topPadding = Math.max(0, Math.floor((availableRows - body.length) / 2));
464
+ const dialogRows = [
465
+ ...Array.from({ length: topPadding }, () => ""),
466
+ ...body,
467
+ ].slice(0, availableRows);
468
+ while (dialogRows.length < availableRows) dialogRows.push("");
469
+ for (const line of dialogRows)
470
+ lines.push(this.row(this.center(line, innerWidth), innerWidth));
471
+ lines.push(this.border("╰", " ←→/tab choose · enter activate · esc cancel ", "╯", innerWidth));
472
+ return lines;
473
+ }
474
+
279
475
  private listItemCount(): number {
476
+ if (this.tab === "recipe") return 0;
280
477
  return this.tab === "communications"
281
478
  ? this.communicationPreviews().length
282
479
  : this.turnItems().length;
@@ -287,7 +484,11 @@ export class ActorInspectorOverlay {
287
484
  const runControl = stripVTControlCharacters(this.renderRunControl());
288
485
  return Math.max(0, runControl.indexOf("Run:") - 2);
289
486
  }
290
- const label = this.tab === "communications" ? "Messages" : "Turns";
487
+ const label = this.tab === "communications"
488
+ ? "Messages"
489
+ : this.tab === "turns"
490
+ ? "Turns"
491
+ : "Recipe";
291
492
  const tabs = stripVTControlCharacters(this.renderTabs());
292
493
  return Math.max(0, tabs.indexOf(label) - 2);
293
494
  }
@@ -295,18 +496,25 @@ export class ActorInspectorOverlay {
295
496
  private renderKeyHints(): string {
296
497
  const hint = (keys: string, description: string) =>
297
498
  `${this.theme.fg("accent", keys)}${this.theme.fg("dim", ` ${description}`)}`;
499
+ if (this.killConfirmation)
500
+ return ` ${hint("y/enter", "confirm kill")} ${hint("n/esc", "cancel")}`;
298
501
  if (this.focus === "select")
299
502
  return this.menuLevel === "value"
300
503
  ? ` ${hint("↑↓", "option")} ${hint("enter", "apply")} ${hint("←/esc", "back")}`
301
504
  : ` ${hint("↑↓", "option")} ${hint("→/enter", "open")} ${hint("←/esc", "back")}`;
505
+ if (this.focus === "recipe")
506
+ return ` ${hint("↑↓/pgup/pgdn", "scroll")} ${hint("↑ at top", "tabs")} ${hint("←/esc", "tabs")}`;
302
507
  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")}`;
508
+ return ` ${hint("↑↓/pgup/pgdn", "scroll")} ${hint("←/esc", "back")}`;
306
509
  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")}`;
510
+ return ` ${hint("↑↓/pgup/pgdn", "row")} ${hint("→/enter", "open")} ${hint("←", "tabs")} ${hint("esc", "close")}`;
511
+ if (this.focus === "runs") {
512
+ const run = this.runs().find((item) => item.run === this.selectedRun);
513
+ const killHint = this.killRun && run?.status === "running" && run.runInstanceId
514
+ ? ` ${hint("k", "kill")}`
515
+ : "";
516
+ return ` ${hint("←→", "run")} ${hint("↓", "tabs")} ${hint("enter", "list")}${killHint} ${hint("esc", "close")}`;
517
+ }
310
518
  return ` ${hint("←→", "navigate")} ${hint("↑↓", "change row")} ${hint("enter", "select")} ${hint("esc", "close")}`;
311
519
  }
312
520
 
@@ -359,7 +567,7 @@ export class ActorInspectorOverlay {
359
567
  const subagent = this.subagents()[this.subagentIndex] ?? "all";
360
568
  const messagesLabel = `Messages${messageFilters.length > 0 ? ` (${messageFilters.join(", ")})` : ""}`;
361
569
  const turnsLabel = `Turns${subagent === "all" ? "" : ` (${subagent})`}`;
362
- return `${tab("communications", messagesLabel)} ${tab("turns", turnsLabel)}`;
570
+ return `${tab("recipe", "Recipe")} ${tab("communications", messagesLabel)} ${tab("turns", turnsLabel)}`;
363
571
  }
364
572
 
365
573
  private renderRunSelector(width: number): string[] {
@@ -384,6 +592,7 @@ export class ActorInspectorOverlay {
384
592
  this.selectedRun = undefined;
385
593
  return [this.theme.fg("warning", " Selected run is no longer owned by this session")];
386
594
  }
595
+ if (this.tab === "recipe") return this.renderRecipe(width);
387
596
  const rows =
388
597
  this.tab === "communications"
389
598
  ? this.communicationRows()
@@ -431,6 +640,51 @@ export class ActorInspectorOverlay {
431
640
  });
432
641
  }
433
642
 
643
+ private renderRecipe(width: number): string[] {
644
+ if (!this.selectedRun) return [];
645
+ const recipe = Inspector.readActorInspectorRecipe(
646
+ path.join(this.stateRoot, this.selectedRun),
647
+ );
648
+ const logicalLines: DetailLogicalLine[] = [
649
+ ...this.detailSection(
650
+ "Identity",
651
+ this.readableValueLines(recipe.identity, " "),
652
+ ),
653
+ ...(recipe.definition
654
+ ? this.detailSection(
655
+ "Recipe",
656
+ this.readableValueLines(recipe.definition, " "),
657
+ )
658
+ : []),
659
+ ...this.detailSection(
660
+ "Launch",
661
+ this.readableValueLines(recipe.launch, " "),
662
+ ),
663
+ ...(recipe.composition.length > 0
664
+ ? this.detailSection(
665
+ "Composition",
666
+ this.readableValueLines(recipe.composition, " "),
667
+ )
668
+ : []),
669
+ ...this.detailSection("Diagnostics", recipe.diagnostics),
670
+ ];
671
+ const wrapped = this.wrapDetailLines(
672
+ logicalLines.length > 0 ? logicalLines : [" No persisted recipe evidence"],
673
+ width,
674
+ );
675
+ const viewportRows = this.contentViewportRows();
676
+ const maxStart = Math.max(0, wrapped.lines.length - viewportRows);
677
+ this.recipeScroll = Math.min(this.recipeScroll, maxStart);
678
+ this.contentStripeIndices = wrapped.stripeIndices.slice(
679
+ this.recipeScroll,
680
+ this.recipeScroll + viewportRows,
681
+ );
682
+ return wrapped.lines.slice(
683
+ this.recipeScroll,
684
+ this.recipeScroll + viewportRows,
685
+ );
686
+ }
687
+
434
688
  private communicationPreviews(): Inspector.ActorInspectorPreview[] {
435
689
  if (!this.selectedRun) return [];
436
690
  const channels =
@@ -496,7 +750,7 @@ export class ActorInspectorOverlay {
496
750
  ["inbox", preview.inbox_status],
497
751
  ["timestamp", preview.timestamp],
498
752
  ];
499
- return fields
753
+ const lines = fields
500
754
  .filter(([, value]) => value !== undefined)
501
755
  .map(([label, value]) =>
502
756
  truncateToWidth(
@@ -505,12 +759,14 @@ export class ActorInspectorOverlay {
505
759
  "…",
506
760
  ),
507
761
  );
762
+ const viewportRows = this.contentViewportRows();
763
+ const maxStart = Math.max(0, lines.length - viewportRows);
764
+ this.detailScroll = Math.min(this.detailScroll, maxStart);
765
+ return lines.slice(this.detailScroll, this.detailScroll + viewportRows);
508
766
  }
509
767
  const turn = this.detailTurn;
510
768
  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);
769
+ const logicalLines = this.turnEvidenceLines(turn);
514
770
  const wrapped = this.wrapDetailLines(logicalLines, width);
515
771
  const viewportRows = this.contentViewportRows();
516
772
  const maxStart = Math.max(0, wrapped.lines.length - viewportRows);
@@ -543,11 +799,39 @@ export class ActorInspectorOverlay {
543
799
  const leading = physicalLine.match(/^ */)?.[0] ?? "";
544
800
  const content = physicalLine.slice(leading.length);
545
801
  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
- );
802
+ const labeled = /^(\s*(?:-\s+)?[^:]+:\s+)(\S.*)$/u.exec(physicalLine);
803
+ let wrapped: string[];
804
+ if (
805
+ visibleWidth(physicalLine) > width &&
806
+ labeled &&
807
+ visibleWidth(labeled[1]) < width
808
+ ) {
809
+ const prefix = labeled[1];
810
+ const value = labeled[2];
811
+ const firstWidth = Math.max(1, width - visibleWidth(prefix));
812
+ const first = this.wrapPreferred(value, firstWidth)[0] ??
813
+ this.takeVisiblePrefix(value, firstWidth);
814
+ const remainder = value.slice(first.length).trimStart();
815
+ const continuation = `${leading} `;
816
+ const continuationWidth = Math.max(
817
+ 1,
818
+ width - visibleWidth(continuation),
819
+ );
820
+ wrapped = [
821
+ `${prefix}${first}`,
822
+ ...(remainder
823
+ ? this.wrapPreferred(remainder, continuationWidth).map(
824
+ (visualLine) => `${continuation}${visualLine}`,
825
+ )
826
+ : []),
827
+ ];
828
+ } else {
829
+ wrapped = visibleWidth(physicalLine) <= width
830
+ ? [physicalLine]
831
+ : this.wrapPreferred(content, contentWidth).map(
832
+ (visualLine) => `${leading}${visualLine}`,
833
+ );
834
+ }
551
835
  for (const visualLine of wrapped) {
552
836
  if (!visualLine.trim()) continue;
553
837
  wrappedLines.push(visualLine);
@@ -558,6 +842,37 @@ export class ActorInspectorOverlay {
558
842
  return { lines: wrappedLines, stripeIndices };
559
843
  }
560
844
 
845
+ private wrapPreferred(content: string, width: number): string[] {
846
+ const lines: string[] = [];
847
+ let remaining = stripVTControlCharacters(content).trimEnd();
848
+ while (remaining && visibleWidth(remaining) > width) {
849
+ const candidate = this.takeVisiblePrefix(remaining, width);
850
+ let splitAt = -1;
851
+ let trimBoundary = false;
852
+ for (let index = 1; index < candidate.length; index += 1) {
853
+ if (/\s/u.test(candidate[index]!)) {
854
+ splitAt = index + 1;
855
+ trimBoundary = true;
856
+ } else if (/[-+=/_,.:]/u.test(candidate[index]!)) {
857
+ splitAt = index;
858
+ trimBoundary = false;
859
+ }
860
+ }
861
+ if (splitAt <= 0) splitAt = candidate.length;
862
+ const head = remaining.slice(0, splitAt).trimEnd();
863
+ if (!head) {
864
+ lines.push(candidate);
865
+ remaining = remaining.slice(candidate.length);
866
+ continue;
867
+ }
868
+ lines.push(head);
869
+ remaining = remaining.slice(splitAt);
870
+ if (trimBoundary) remaining = remaining.trimStart();
871
+ }
872
+ if (remaining) lines.push(remaining);
873
+ return lines;
874
+ }
875
+
561
876
  private detailSection(
562
877
  title: string,
563
878
  content: string[],
@@ -612,9 +927,14 @@ export class ActorInspectorOverlay {
612
927
  }
613
928
  if (!item || typeof item !== "object")
614
929
  return [`${indent}- ${String(item)}`];
930
+ const [first = "", ...rest] = this.readableValueLines(
931
+ item,
932
+ `${indent} `,
933
+ depth + 1,
934
+ );
615
935
  return [
616
- `${indent}-`,
617
- ...this.readableValueLines(item, `${indent} `, depth + 1),
936
+ `${indent}- ${first.trimStart()}`,
937
+ ...rest,
618
938
  ];
619
939
  });
620
940
  }
@@ -650,7 +970,9 @@ export class ActorInspectorOverlay {
650
970
  : "";
651
971
  lines.push(` Subagent ${subagent}${stage}`, DETAIL_SECTION_BREAK);
652
972
  if (turn.userText)
653
- lines.push(...this.detailSection("User", [turn.userText]));
973
+ lines.push(
974
+ ...this.detailSection("User", [this.readablePromptText(turn.userText)]),
975
+ );
654
976
  if (turn.thinking)
655
977
  lines.push(...this.detailSection("Thinking", [turn.thinking]));
656
978
  if (turn.assistantText)
@@ -698,12 +1020,6 @@ export class ActorInspectorOverlay {
698
1020
  ` ${turn.promptFile}${turn.promptBytes ? ` (${turn.promptBytes} bytes)` : ""}`,
699
1021
  ]
700
1022
  : []),
701
- ...(turn.recipeContext === undefined
702
- ? []
703
- : [
704
- "Recipe Context",
705
- ...this.readableValueLines(turn.recipeContext, " "),
706
- ]),
707
1023
  ];
708
1024
  lines.push(...this.detailSection("Provenance", provenance));
709
1025
  return lines.length > 0 ? lines : [" No persisted turn evidence"];
@@ -714,44 +1030,17 @@ export class ActorInspectorOverlay {
714
1030
  return wrapped?.[1]?.trim() || text;
715
1031
  }
716
1032
 
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"];
1033
+ private allTurnItems(): Inspector.ActorInspectorTurnItem[] {
1034
+ if (!this.selectedRun) return [];
1035
+ if (this.turnCache?.run === this.selectedRun) return this.turnCache.turns;
1036
+ const turns = this.readTurns(path.join(this.stateRoot, this.selectedRun));
1037
+ this.turnCache = { run: this.selectedRun, turns };
1038
+ return turns;
747
1039
  }
748
1040
 
749
1041
  private turnItems(): Inspector.ActorInspectorTurnItem[] {
750
- if (!this.selectedRun) return [];
751
1042
  const selectedSubagent = this.subagents()[this.subagentIndex];
752
- return Inspector.readActorInspectorTurns(
753
- path.join(this.stateRoot, this.selectedRun),
754
- )
1043
+ return this.allTurnItems()
755
1044
  .filter(
756
1045
  (turn) =>
757
1046
  !selectedSubagent ||
@@ -793,9 +1082,7 @@ export class ActorInspectorOverlay {
793
1082
 
794
1083
  private subagents(): string[] {
795
1084
  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);
1085
+ const ids = this.allTurnItems().map((turn) => turn.commandId);
799
1086
  return ["all", ...new Set(ids)];
800
1087
  }
801
1088
 
@@ -982,17 +1269,11 @@ export class ActorInspectorOverlay {
982
1269
  if (preview) this.readKeys.add(Inspector.inspectorPreviewReadKey(preview));
983
1270
  } else this.detailTurn = this.turnItems()[this.rowIndex];
984
1271
  this.detailScroll = 0;
985
- this.detailView = "evidence";
986
1272
  this.detailOpen = Boolean(this.detailCommunication || this.detailTurn);
987
1273
  if (this.detailOpen) this.focus = "detail";
988
1274
  }
989
1275
 
990
1276
  private backDetail(): void {
991
- if (this.detailView === "transcript") {
992
- this.detailView = "evidence";
993
- this.detailScroll = 0;
994
- return;
995
- }
996
1277
  this.closeDetail();
997
1278
  }
998
1279
 
@@ -1001,7 +1282,6 @@ export class ActorInspectorOverlay {
1001
1282
  this.detailCommunication = undefined;
1002
1283
  this.detailTurn = undefined;
1003
1284
  this.detailScroll = 0;
1004
- this.detailView = "evidence";
1005
1285
  this.focus = "list";
1006
1286
  }
1007
1287
 
@@ -1059,6 +1339,12 @@ export class ActorInspectorOverlay {
1059
1339
  return plain.slice(index);
1060
1340
  }
1061
1341
 
1342
+ private center(content: string, width: number): string {
1343
+ const contentWidth = visibleWidth(content);
1344
+ const leading = Math.max(0, Math.floor((width - contentWidth) / 2));
1345
+ return `${" ".repeat(leading)}${content}`;
1346
+ }
1347
+
1062
1348
  private fit(content: string, width: number): string {
1063
1349
  // A Component render entry must remain exactly one terminal row. Persisted
1064
1350
  // prompts, messages, and tool results may contain line breaks or tabs;