@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
@@ -5,12 +5,19 @@
5
5
  */
6
6
  import * as path from "node:path";
7
7
  import { stripVTControlCharacters } from "node:util";
8
- import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
8
+ import { matchesKey, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
9
9
  import * as Inspector from "./inspector.js";
10
+ const INSPECTOR_TABS = [
11
+ "recipe",
12
+ "communications",
13
+ "turns",
14
+ ];
10
15
  const DETAIL_SECTION_BREAK = { sectionBreak: true };
11
16
  export class ActorInspectorOverlay {
12
17
  done;
18
+ killRun;
13
19
  ownerId;
20
+ readTurns;
14
21
  stateRoot;
15
22
  theme;
16
23
  tui;
@@ -24,23 +31,33 @@ export class ActorInspectorOverlay {
24
31
  detailOpen = false;
25
32
  detailScroll = 0;
26
33
  detailTurn;
27
- detailView = "evidence";
34
+ feedback;
35
+ killConfirmation;
36
+ killDialogChoice = "cancel";
28
37
  readKeys = new Set();
29
38
  focus = "tabs";
30
39
  filterControlIndex = 0;
31
40
  menuLevel = "filter";
32
41
  selectorIndex = 0;
42
+ recipeScroll = 0;
33
43
  rowIndex = 0;
34
44
  selectedRun;
35
45
  subagentIndex = 0;
36
- tab = "communications";
46
+ tab = "recipe";
47
+ turnCache;
37
48
  constructor(options) {
38
49
  this.done = options.done;
50
+ this.killRun = options.killRun;
39
51
  this.ownerId = options.ownerId;
52
+ this.readTurns = options.readTurns ?? Inspector.readActorInspectorTurns;
40
53
  this.stateRoot = options.stateRoot;
41
54
  this.theme = options.theme;
42
55
  this.tui = options.tui;
43
- this.refreshTimer = setInterval(() => this.tui.requestRender(), 1000);
56
+ this.refreshTimer = setInterval(() => {
57
+ if (this.focus !== "list" && this.focus !== "detail")
58
+ this.turnCache = undefined;
59
+ this.tui.requestRender();
60
+ }, 1000);
44
61
  this.refreshTimer.unref?.();
45
62
  }
46
63
  handleInput(data) {
@@ -50,6 +67,34 @@ export class ActorInspectorOverlay {
50
67
  this.done();
51
68
  return;
52
69
  }
70
+ if (this.killConfirmation) {
71
+ const key = data.toLowerCase();
72
+ if (matchesKey(data, "escape") || key === "n") {
73
+ this.cancelKill();
74
+ }
75
+ else if (matchesKey(data, "left") ||
76
+ matchesKey(data, "right") ||
77
+ matchesKey(data, "tab")) {
78
+ this.killDialogChoice =
79
+ this.killDialogChoice === "cancel" ? "kill" : "cancel";
80
+ }
81
+ else if (key === "y") {
82
+ this.confirmKill();
83
+ }
84
+ else if (matchesKey(data, "return")) {
85
+ if (this.killDialogChoice === "kill")
86
+ this.confirmKill();
87
+ else
88
+ this.cancelKill();
89
+ }
90
+ this.tui.requestRender();
91
+ return;
92
+ }
93
+ if (data.toLowerCase() === "k") {
94
+ this.requestKill(runs);
95
+ this.tui.requestRender();
96
+ return;
97
+ }
53
98
  if (matchesKey(data, "escape")) {
54
99
  if (this.focus === "select") {
55
100
  if (this.menuLevel === "value") {
@@ -61,6 +106,8 @@ export class ActorInspectorOverlay {
61
106
  }
62
107
  else if (this.focus === "detail")
63
108
  this.backDetail();
109
+ else if (this.focus === "recipe")
110
+ this.focus = "tabs";
64
111
  else
65
112
  this.done();
66
113
  this.tui.requestRender();
@@ -80,19 +127,35 @@ export class ActorInspectorOverlay {
80
127
  this.tui.requestRender();
81
128
  return;
82
129
  }
130
+ if (this.focus === "recipe") {
131
+ if (matchesKey(data, "up")) {
132
+ if (this.recipeScroll === 0)
133
+ this.focus = "tabs";
134
+ else
135
+ this.recipeScroll -= 1;
136
+ }
137
+ else if (matchesKey(data, "down"))
138
+ this.recipeScroll += 1;
139
+ else if (matchesKey(data, "pageUp"))
140
+ this.recipeScroll = Math.max(0, this.recipeScroll - this.contentViewportRows());
141
+ else if (matchesKey(data, "pageDown"))
142
+ this.recipeScroll += this.contentViewportRows();
143
+ else if (matchesKey(data, "left"))
144
+ this.focus = "tabs";
145
+ this.tui.requestRender();
146
+ return;
147
+ }
83
148
  if (this.focus === "detail") {
84
149
  if (matchesKey(data, "up"))
85
150
  this.detailScroll = Math.max(0, this.detailScroll - 1);
86
151
  else if (matchesKey(data, "down"))
87
152
  this.detailScroll += 1;
153
+ else if (matchesKey(data, "pageUp"))
154
+ this.detailScroll = Math.max(0, this.detailScroll - this.contentViewportRows());
155
+ else if (matchesKey(data, "pageDown"))
156
+ this.detailScroll += this.contentViewportRows();
88
157
  else if (matchesKey(data, "left"))
89
158
  this.backDetail();
90
- else if ((matchesKey(data, "right") || matchesKey(data, "return")) &&
91
- this.detailView === "evidence" &&
92
- this.tab === "turns") {
93
- this.detailView = "transcript";
94
- this.detailScroll = 0;
95
- }
96
159
  this.tui.requestRender();
97
160
  return;
98
161
  }
@@ -108,12 +171,18 @@ export class ActorInspectorOverlay {
108
171
  }
109
172
  else if (this.focus === "tabs") {
110
173
  if (matchesKey(data, "left") || matchesKey(data, "right")) {
111
- this.tab = this.tab === "communications" ? "turns" : "communications";
174
+ const current = INSPECTOR_TABS.indexOf(this.tab);
175
+ const direction = matchesKey(data, "right") ? 1 : -1;
176
+ this.tab = INSPECTOR_TABS[(current + direction + INSPECTOR_TABS.length) % INSPECTOR_TABS.length];
112
177
  this.filterControlIndex = 0;
178
+ this.recipeScroll = 0;
113
179
  this.rowIndex = 0;
114
180
  }
115
181
  else if (matchesKey(data, "up"))
116
182
  this.focus = "runs";
183
+ else if ((matchesKey(data, "down") || matchesKey(data, "return")) &&
184
+ this.tab === "recipe")
185
+ this.focus = "recipe";
117
186
  else if (matchesKey(data, "down") && this.listItemCount() > 0)
118
187
  this.focus = "list";
119
188
  else if (matchesKey(data, "return"))
@@ -121,7 +190,13 @@ export class ActorInspectorOverlay {
121
190
  }
122
191
  else if (this.focus === "list") {
123
192
  const count = this.tab === "communications" ? this.communicationPreviews().length : this.turnItems().length;
124
- if (matchesKey(data, "up")) {
193
+ if (matchesKey(data, "left"))
194
+ this.focus = "tabs";
195
+ else if (matchesKey(data, "pageUp"))
196
+ this.rowIndex = Math.max(0, this.rowIndex - this.contentViewportRows());
197
+ else if (matchesKey(data, "pageDown"))
198
+ this.rowIndex = Math.min(Math.max(0, count - 1), this.rowIndex + this.contentViewportRows());
199
+ else if (matchesKey(data, "up")) {
125
200
  if (this.rowIndex === 0)
126
201
  this.focus = "tabs";
127
202
  else
@@ -138,6 +213,8 @@ export class ActorInspectorOverlay {
138
213
  this.ensureSelectedRun(this.runs());
139
214
  const safeWidth = Math.max(24, width);
140
215
  const innerWidth = Math.max(1, safeWidth - 2);
216
+ if (this.killConfirmation)
217
+ return this.renderKillDialog(safeWidth, innerWidth);
141
218
  const lines = [];
142
219
  lines.push(this.border("╭", " Actor Inspector ", "╮", innerWidth));
143
220
  lines.push(this.row(this.renderRunControl(), innerWidth));
@@ -167,11 +244,21 @@ export class ActorInspectorOverlay {
167
244
  else
168
245
  lines.push(this.border("├", "", "┤", innerWidth));
169
246
  this.contentStripeIndices = [];
170
- const content = this.detailOpen
247
+ const baseContent = this.detailOpen
171
248
  ? this.renderDetail(innerWidth)
172
249
  : this.selectedRun
173
250
  ? this.renderTimeline(innerWidth)
174
251
  : this.renderRunSelector(innerWidth);
252
+ const content = this.feedback && !this.killConfirmation
253
+ ? [
254
+ this.theme.fg(this.feedback.ok
255
+ ? "success"
256
+ : /^Kill (failed|rejected):/.test(this.feedback.message)
257
+ ? "error"
258
+ : "warning", ` ${this.feedback.message}`),
259
+ ...baseContent.slice(0, Math.max(0, this.contentViewportRows() - 1)),
260
+ ]
261
+ : baseContent;
175
262
  const contentViewportRows = this.contentViewportRows();
176
263
  const selectorRows = selector.slice(1);
177
264
  for (let index = 0; index < Math.max(content.length, contentViewportRows); index += 1) {
@@ -215,7 +302,9 @@ export class ActorInspectorOverlay {
215
302
  }
216
303
  selectRun(run, index) {
217
304
  this.selectedRun = run;
305
+ this.turnCache = undefined;
218
306
  this.runIndex = index;
307
+ this.recipeScroll = 0;
219
308
  this.rowIndex = 0;
220
309
  this.subagentIndex = 0;
221
310
  this.communicationFrom = "all";
@@ -229,7 +318,105 @@ export class ActorInspectorOverlay {
229
318
  if (next)
230
319
  this.selectRun(next.run, nextIndex);
231
320
  }
321
+ requestKill(runs) {
322
+ this.feedback = undefined;
323
+ if (this.focus !== "runs") {
324
+ this.feedback = { ok: false, message: "Focus Run before using Kill." };
325
+ return;
326
+ }
327
+ const run = runs.find((item) => item.run === this.selectedRun);
328
+ if (!run) {
329
+ this.feedback = { ok: false, message: "Kill unavailable: no owned run." };
330
+ return;
331
+ }
332
+ if (!this.killRun) {
333
+ this.feedback = { ok: false, message: "Kill unavailable in this Inspector." };
334
+ return;
335
+ }
336
+ if (run.status !== "running") {
337
+ this.feedback = {
338
+ ok: false,
339
+ message: `Kill unavailable: run is ${run.status}.`,
340
+ };
341
+ return;
342
+ }
343
+ if (!run.runInstanceId) {
344
+ this.feedback = {
345
+ ok: false,
346
+ message: "Kill unavailable: run generation unavailable.",
347
+ };
348
+ return;
349
+ }
350
+ this.killConfirmation = {
351
+ run: run.run,
352
+ runInstanceId: run.runInstanceId,
353
+ status: run.status,
354
+ };
355
+ this.killDialogChoice = "cancel";
356
+ }
357
+ cancelKill() {
358
+ this.feedback = { ok: false, message: "Kill cancelled." };
359
+ this.killConfirmation = undefined;
360
+ this.killDialogChoice = "cancel";
361
+ }
362
+ confirmKill() {
363
+ const confirmation = this.killConfirmation;
364
+ this.killConfirmation = undefined;
365
+ this.killDialogChoice = "cancel";
366
+ if (!confirmation || !this.killRun) {
367
+ this.feedback = { ok: false, message: "Kill unavailable in this Inspector." };
368
+ return;
369
+ }
370
+ try {
371
+ this.feedback = this.killRun(confirmation.run, confirmation.runInstanceId);
372
+ }
373
+ catch (error) {
374
+ const message = error instanceof Error ? error.message : String(error);
375
+ this.feedback = {
376
+ ok: false,
377
+ message: `Kill failed: ${message.replaceAll(/\s+/g, " ").slice(0, 160)}.`,
378
+ };
379
+ }
380
+ }
381
+ renderKillDialog(_width, innerWidth) {
382
+ const confirmation = this.killConfirmation;
383
+ const totalRows = this.contentViewportRows() + 7;
384
+ const cancel = this.killDialogChoice === "cancel"
385
+ ? this.theme.bg("selectedBg", this.theme.fg("accent", " Cancel "))
386
+ : this.theme.fg("muted", " Cancel ");
387
+ const kill = this.killDialogChoice === "kill"
388
+ ? this.theme.bg("selectedBg", this.theme.fg("error", " Kill actor "))
389
+ : this.theme.fg("error", " Kill actor ");
390
+ const body = [
391
+ "",
392
+ this.theme.fg("error", "Kill this running actor?"),
393
+ "",
394
+ `${this.theme.fg("muted", "Run:")} ${this.theme.fg("accent", `run:${confirmation.run}`)}`,
395
+ `${this.theme.fg("muted", "Current status:")} ${this.theme.fg("warning", confirmation.status)}`,
396
+ "",
397
+ this.theme.fg("text", "This sends canonical control.kill."),
398
+ this.theme.fg("error", "The action is destructive and cannot be undone."),
399
+ "",
400
+ `${cancel} ${kill}`,
401
+ "",
402
+ ];
403
+ const lines = [this.border("╭", " Confirm Actor Kill ", "╮", innerWidth)];
404
+ const availableRows = Math.max(1, totalRows - 2);
405
+ const topPadding = Math.max(0, Math.floor((availableRows - body.length) / 2));
406
+ const dialogRows = [
407
+ ...Array.from({ length: topPadding }, () => ""),
408
+ ...body,
409
+ ].slice(0, availableRows);
410
+ while (dialogRows.length < availableRows)
411
+ dialogRows.push("");
412
+ for (const line of dialogRows)
413
+ lines.push(this.row(this.center(line, innerWidth), innerWidth));
414
+ lines.push(this.border("╰", " ←→/tab choose · enter activate · esc cancel ", "╯", innerWidth));
415
+ return lines;
416
+ }
232
417
  listItemCount() {
418
+ if (this.tab === "recipe")
419
+ return 0;
233
420
  return this.tab === "communications"
234
421
  ? this.communicationPreviews().length
235
422
  : this.turnItems().length;
@@ -239,24 +426,35 @@ export class ActorInspectorOverlay {
239
426
  const runControl = stripVTControlCharacters(this.renderRunControl());
240
427
  return Math.max(0, runControl.indexOf("Run:") - 2);
241
428
  }
242
- const label = this.tab === "communications" ? "Messages" : "Turns";
429
+ const label = this.tab === "communications"
430
+ ? "Messages"
431
+ : this.tab === "turns"
432
+ ? "Turns"
433
+ : "Recipe";
243
434
  const tabs = stripVTControlCharacters(this.renderTabs());
244
435
  return Math.max(0, tabs.indexOf(label) - 2);
245
436
  }
246
437
  renderKeyHints() {
247
438
  const hint = (keys, description) => `${this.theme.fg("accent", keys)}${this.theme.fg("dim", ` ${description}`)}`;
439
+ if (this.killConfirmation)
440
+ return ` ${hint("y/enter", "confirm kill")} ${hint("n/esc", "cancel")}`;
248
441
  if (this.focus === "select")
249
442
  return this.menuLevel === "value"
250
443
  ? ` ${hint("↑↓", "option")} ${hint("enter", "apply")} ${hint("←/esc", "back")}`
251
444
  : ` ${hint("↑↓", "option")} ${hint("→/enter", "open")} ${hint("←/esc", "back")}`;
445
+ if (this.focus === "recipe")
446
+ return ` ${hint("↑↓/pgup/pgdn", "scroll")} ${hint("↑ at top", "tabs")} ${hint("←/esc", "tabs")}`;
252
447
  if (this.focus === "detail")
253
- return this.tab === "turns" && this.detailView === "evidence"
254
- ? ` ${hint("↑↓", "scroll")} ${hint("→/enter", "readable")} ${hint("←/esc", "back")}`
255
- : ` ${hint("↑↓", "scroll")} ${hint("←/esc", this.detailView === "transcript" ? "evidence" : "back")}`;
448
+ return ` ${hint("↑↓/pgup/pgdn", "scroll")} ${hint("←/esc", "back")}`;
256
449
  if (this.focus === "list")
257
- return ` ${hint("↑↓", "row")} ${hint("→/enter", "open")} ${hint("esc", "close")}`;
258
- if (this.focus === "runs")
259
- return ` ${hint("←→", "run")} ${hint("↓", "tabs")} ${hint("enter", "list")} ${hint("esc", "close")}`;
450
+ return ` ${hint("↑↓/pgup/pgdn", "row")} ${hint("→/enter", "open")} ${hint("←", "tabs")} ${hint("esc", "close")}`;
451
+ if (this.focus === "runs") {
452
+ const run = this.runs().find((item) => item.run === this.selectedRun);
453
+ const killHint = this.killRun && run?.status === "running" && run.runInstanceId
454
+ ? ` ${hint("k", "kill")}`
455
+ : "";
456
+ return ` ${hint("←→", "run")} ${hint("↓", "tabs")} ${hint("enter", "list")}${killHint} ${hint("esc", "close")}`;
457
+ }
260
458
  return ` ${hint("←→", "navigate")} ${hint("↑↓", "change row")} ${hint("enter", "select")} ${hint("esc", "close")}`;
261
459
  }
262
460
  renderRunControl() {
@@ -306,7 +504,7 @@ export class ActorInspectorOverlay {
306
504
  const subagent = this.subagents()[this.subagentIndex] ?? "all";
307
505
  const messagesLabel = `Messages${messageFilters.length > 0 ? ` (${messageFilters.join(", ")})` : ""}`;
308
506
  const turnsLabel = `Turns${subagent === "all" ? "" : ` (${subagent})`}`;
309
- return `${tab("communications", messagesLabel)} ${tab("turns", turnsLabel)}`;
507
+ return `${tab("recipe", "Recipe")} ${tab("communications", messagesLabel)} ${tab("turns", turnsLabel)}`;
310
508
  }
311
509
  renderRunSelector(width) {
312
510
  const runs = this.runs();
@@ -331,6 +529,8 @@ export class ActorInspectorOverlay {
331
529
  this.selectedRun = undefined;
332
530
  return [this.theme.fg("warning", " Selected run is no longer owned by this session")];
333
531
  }
532
+ if (this.tab === "recipe")
533
+ return this.renderRecipe(width);
334
534
  const rows = this.tab === "communications"
335
535
  ? this.communicationRows()
336
536
  : this.turnRows();
@@ -363,6 +563,28 @@ export class ActorInspectorOverlay {
363
563
  : ` ${row}`;
364
564
  });
365
565
  }
566
+ renderRecipe(width) {
567
+ if (!this.selectedRun)
568
+ return [];
569
+ const recipe = Inspector.readActorInspectorRecipe(path.join(this.stateRoot, this.selectedRun));
570
+ const logicalLines = [
571
+ ...this.detailSection("Identity", this.readableValueLines(recipe.identity, " ")),
572
+ ...(recipe.definition
573
+ ? this.detailSection("Recipe", this.readableValueLines(recipe.definition, " "))
574
+ : []),
575
+ ...this.detailSection("Launch", this.readableValueLines(recipe.launch, " ")),
576
+ ...(recipe.composition.length > 0
577
+ ? this.detailSection("Composition", this.readableValueLines(recipe.composition, " "))
578
+ : []),
579
+ ...this.detailSection("Diagnostics", recipe.diagnostics),
580
+ ];
581
+ const wrapped = this.wrapDetailLines(logicalLines.length > 0 ? logicalLines : [" No persisted recipe evidence"], width);
582
+ const viewportRows = this.contentViewportRows();
583
+ const maxStart = Math.max(0, wrapped.lines.length - viewportRows);
584
+ this.recipeScroll = Math.min(this.recipeScroll, maxStart);
585
+ this.contentStripeIndices = wrapped.stripeIndices.slice(this.recipeScroll, this.recipeScroll + viewportRows);
586
+ return wrapped.lines.slice(this.recipeScroll, this.recipeScroll + viewportRows);
587
+ }
366
588
  communicationPreviews() {
367
589
  if (!this.selectedRun)
368
590
  return [];
@@ -419,16 +641,18 @@ export class ActorInspectorOverlay {
419
641
  ["inbox", preview.inbox_status],
420
642
  ["timestamp", preview.timestamp],
421
643
  ];
422
- return fields
644
+ const lines = fields
423
645
  .filter(([, value]) => value !== undefined)
424
646
  .map(([label, value]) => truncateToWidth(` ${label.padEnd(12, " ")} ${typeof value === "string" ? value : JSON.stringify(value)}`, width, "…"));
647
+ const viewportRows = this.contentViewportRows();
648
+ const maxStart = Math.max(0, lines.length - viewportRows);
649
+ this.detailScroll = Math.min(this.detailScroll, maxStart);
650
+ return lines.slice(this.detailScroll, this.detailScroll + viewportRows);
425
651
  }
426
652
  const turn = this.detailTurn;
427
653
  if (!turn)
428
654
  return [this.theme.fg("warning", " Turn row is no longer available")];
429
- const logicalLines = this.detailView === "transcript"
430
- ? this.turnTranscriptLines(turn)
431
- : this.turnEvidenceLines(turn);
655
+ const logicalLines = this.turnEvidenceLines(turn);
432
656
  const wrapped = this.wrapDetailLines(logicalLines, width);
433
657
  const viewportRows = this.contentViewportRows();
434
658
  const maxStart = Math.max(0, wrapped.lines.length - viewportRows);
@@ -452,9 +676,31 @@ export class ActorInspectorOverlay {
452
676
  const leading = physicalLine.match(/^ */)?.[0] ?? "";
453
677
  const content = physicalLine.slice(leading.length);
454
678
  const contentWidth = Math.max(1, width - visibleWidth(leading));
455
- const wrapped = visibleWidth(physicalLine) <= width
456
- ? [physicalLine]
457
- : wrapTextWithAnsi(content, contentWidth).map((visualLine) => `${leading}${visualLine}`);
679
+ const labeled = /^(\s*(?:-\s+)?[^:]+:\s+)(\S.*)$/u.exec(physicalLine);
680
+ let wrapped;
681
+ if (visibleWidth(physicalLine) > width &&
682
+ labeled &&
683
+ visibleWidth(labeled[1]) < width) {
684
+ const prefix = labeled[1];
685
+ const value = labeled[2];
686
+ const firstWidth = Math.max(1, width - visibleWidth(prefix));
687
+ const first = this.wrapPreferred(value, firstWidth)[0] ??
688
+ this.takeVisiblePrefix(value, firstWidth);
689
+ const remainder = value.slice(first.length).trimStart();
690
+ const continuation = `${leading} `;
691
+ const continuationWidth = Math.max(1, width - visibleWidth(continuation));
692
+ wrapped = [
693
+ `${prefix}${first}`,
694
+ ...(remainder
695
+ ? this.wrapPreferred(remainder, continuationWidth).map((visualLine) => `${continuation}${visualLine}`)
696
+ : []),
697
+ ];
698
+ }
699
+ else {
700
+ wrapped = visibleWidth(physicalLine) <= width
701
+ ? [physicalLine]
702
+ : this.wrapPreferred(content, contentWidth).map((visualLine) => `${leading}${visualLine}`);
703
+ }
458
704
  for (const visualLine of wrapped) {
459
705
  if (!visualLine.trim())
460
706
  continue;
@@ -465,6 +711,40 @@ export class ActorInspectorOverlay {
465
711
  }
466
712
  return { lines: wrappedLines, stripeIndices };
467
713
  }
714
+ wrapPreferred(content, width) {
715
+ const lines = [];
716
+ let remaining = stripVTControlCharacters(content).trimEnd();
717
+ while (remaining && visibleWidth(remaining) > width) {
718
+ const candidate = this.takeVisiblePrefix(remaining, width);
719
+ let splitAt = -1;
720
+ let trimBoundary = false;
721
+ for (let index = 1; index < candidate.length; index += 1) {
722
+ if (/\s/u.test(candidate[index])) {
723
+ splitAt = index + 1;
724
+ trimBoundary = true;
725
+ }
726
+ else if (/[-+=/_,.:]/u.test(candidate[index])) {
727
+ splitAt = index;
728
+ trimBoundary = false;
729
+ }
730
+ }
731
+ if (splitAt <= 0)
732
+ splitAt = candidate.length;
733
+ const head = remaining.slice(0, splitAt).trimEnd();
734
+ if (!head) {
735
+ lines.push(candidate);
736
+ remaining = remaining.slice(candidate.length);
737
+ continue;
738
+ }
739
+ lines.push(head);
740
+ remaining = remaining.slice(splitAt);
741
+ if (trimBoundary)
742
+ remaining = remaining.trimStart();
743
+ }
744
+ if (remaining)
745
+ lines.push(remaining);
746
+ return lines;
747
+ }
468
748
  detailSection(title, content) {
469
749
  if (content.length === 0)
470
750
  return [];
@@ -508,9 +788,10 @@ export class ActorInspectorOverlay {
508
788
  }
509
789
  if (!item || typeof item !== "object")
510
790
  return [`${indent}- ${String(item)}`];
791
+ const [first = "", ...rest] = this.readableValueLines(item, `${indent} `, depth + 1);
511
792
  return [
512
- `${indent}-`,
513
- ...this.readableValueLines(item, `${indent} `, depth + 1),
793
+ `${indent}- ${first.trimStart()}`,
794
+ ...rest,
514
795
  ];
515
796
  });
516
797
  }
@@ -543,7 +824,7 @@ export class ActorInspectorOverlay {
543
824
  : "";
544
825
  lines.push(` Subagent ${subagent}${stage}`, DETAIL_SECTION_BREAK);
545
826
  if (turn.userText)
546
- lines.push(...this.detailSection("User", [turn.userText]));
827
+ lines.push(...this.detailSection("User", [this.readablePromptText(turn.userText)]));
547
828
  if (turn.thinking)
548
829
  lines.push(...this.detailSection("Thinking", [turn.thinking]));
549
830
  if (turn.assistantText)
@@ -586,12 +867,6 @@ export class ActorInspectorOverlay {
586
867
  ` ${turn.promptFile}${turn.promptBytes ? ` (${turn.promptBytes} bytes)` : ""}`,
587
868
  ]
588
869
  : []),
589
- ...(turn.recipeContext === undefined
590
- ? []
591
- : [
592
- "Recipe Context",
593
- ...this.readableValueLines(turn.recipeContext, " "),
594
- ]),
595
870
  ];
596
871
  lines.push(...this.detailSection("Provenance", provenance));
597
872
  return lines.length > 0 ? lines : [" No persisted turn evidence"];
@@ -600,34 +875,18 @@ export class ActorInspectorOverlay {
600
875
  const wrapped = /^\s*<file\b[^>]*>\s*([\s\S]*?)\s*<\/file>\s*$/i.exec(text);
601
876
  return wrapped?.[1]?.trim() || text;
602
877
  }
603
- turnTranscriptLines(turn) {
604
- const lines = [];
605
- if (turn.userText)
606
- lines.push(...this.detailSection("User", [this.readablePromptText(turn.userText)]));
607
- if (turn.thinking)
608
- lines.push(...this.detailSection("Thinking", [turn.thinking]));
609
- if (turn.assistantText)
610
- lines.push(...this.detailSection("Assistant", [turn.assistantText]));
611
- for (const tool of turn.toolCalls) {
612
- const content = [
613
- ...(tool.arguments === undefined
614
- ? []
615
- : ["Input", ...this.readableValueLines(tool.arguments, " ")]),
616
- ...(tool.result === undefined
617
- ? []
618
- : ["Result", ...this.readableValueLines(tool.result, " ")]),
619
- ];
620
- lines.push(...this.detailSection(`Tool (${tool.name}${tool.resultError ? ", Error" : ""})`, content));
621
- }
622
- if (turn.error)
623
- lines.push(...this.detailSection("Error", [turn.error]));
624
- return lines.length > 0 ? lines : [" No readable turn content"];
625
- }
626
- turnItems() {
878
+ allTurnItems() {
627
879
  if (!this.selectedRun)
628
880
  return [];
881
+ if (this.turnCache?.run === this.selectedRun)
882
+ return this.turnCache.turns;
883
+ const turns = this.readTurns(path.join(this.stateRoot, this.selectedRun));
884
+ this.turnCache = { run: this.selectedRun, turns };
885
+ return turns;
886
+ }
887
+ turnItems() {
629
888
  const selectedSubagent = this.subagents()[this.subagentIndex];
630
- return Inspector.readActorInspectorTurns(path.join(this.stateRoot, this.selectedRun))
889
+ return this.allTurnItems()
631
890
  .filter((turn) => !selectedSubagent ||
632
891
  selectedSubagent === "all" ||
633
892
  turn.commandId === selectedSubagent)
@@ -662,7 +921,7 @@ export class ActorInspectorOverlay {
662
921
  subagents() {
663
922
  if (!this.selectedRun || this.tab !== "turns")
664
923
  return ["all"];
665
- const ids = Inspector.readActorInspectorTurns(path.join(this.stateRoot, this.selectedRun)).map((turn) => turn.commandId);
924
+ const ids = this.allTurnItems().map((turn) => turn.commandId);
666
925
  return ["all", ...new Set(ids)];
667
926
  }
668
927
  valueOptions() {
@@ -795,17 +1054,11 @@ export class ActorInspectorOverlay {
795
1054
  else
796
1055
  this.detailTurn = this.turnItems()[this.rowIndex];
797
1056
  this.detailScroll = 0;
798
- this.detailView = "evidence";
799
1057
  this.detailOpen = Boolean(this.detailCommunication || this.detailTurn);
800
1058
  if (this.detailOpen)
801
1059
  this.focus = "detail";
802
1060
  }
803
1061
  backDetail() {
804
- if (this.detailView === "transcript") {
805
- this.detailView = "evidence";
806
- this.detailScroll = 0;
807
- return;
808
- }
809
1062
  this.closeDetail();
810
1063
  }
811
1064
  closeDetail() {
@@ -813,7 +1066,6 @@ export class ActorInspectorOverlay {
813
1066
  this.detailCommunication = undefined;
814
1067
  this.detailTurn = undefined;
815
1068
  this.detailScroll = 0;
816
- this.detailView = "evidence";
817
1069
  this.focus = "list";
818
1070
  }
819
1071
  border(left, title, right, width) {
@@ -861,6 +1113,11 @@ export class ActorInspectorOverlay {
861
1113
  }
862
1114
  return plain.slice(index);
863
1115
  }
1116
+ center(content, width) {
1117
+ const contentWidth = visibleWidth(content);
1118
+ const leading = Math.max(0, Math.floor((width - contentWidth) / 2));
1119
+ return `${" ".repeat(leading)}${content}`;
1120
+ }
864
1121
  fit(content, width) {
865
1122
  // A Component render entry must remain exactly one terminal row. Persisted
866
1123
  // prompts, messages, and tool results may contain line breaks or tabs;
@@ -55,10 +55,19 @@ export interface ActorInspectorPreviewReadOptions {
55
55
  }
56
56
  export interface ActorInspectorRunItem {
57
57
  run: string;
58
+ runInstanceId?: string;
58
59
  status: string;
59
60
  updatedAt?: string;
60
61
  }
61
62
  export declare function readActorInspectorRuns(stateRoot: string, ownerId: string): ActorInspectorRunItem[];
63
+ export interface ActorInspectorRecipeView {
64
+ composition: Array<Record<string, unknown>>;
65
+ definition?: Record<string, unknown>;
66
+ diagnostics: string[];
67
+ identity: Record<string, unknown>;
68
+ launch: Record<string, unknown>;
69
+ }
70
+ export declare function readActorInspectorRecipe(stateDir: string): ActorInspectorRecipeView;
62
71
  export interface ActorInspectorTurnItem extends SessionEvidence.SessionEvidenceTurn {
63
72
  commandId: string;
64
73
  diagnostics: string[];