@llblab/pi-actors 0.41.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/AGENTS.md +13 -8
  2. package/BACKLOG.md +2 -81
  3. package/CHANGELOG.md +22 -0
  4. package/README.md +22 -4
  5. package/dist/index.js +29 -82
  6. package/dist/lib/async-runs.d.ts +25 -5
  7. package/dist/lib/async-runs.js +136 -47
  8. package/dist/lib/automatic-review-runtime.d.ts +18 -0
  9. package/dist/lib/automatic-review-runtime.js +96 -0
  10. package/dist/lib/draft-consolidation-transaction.d.ts +65 -0
  11. package/dist/lib/draft-consolidation-transaction.js +610 -0
  12. package/dist/lib/draft-consolidation.d.ts +35 -0
  13. package/dist/lib/draft-consolidation.js +126 -0
  14. package/dist/lib/draft-review.d.ts +56 -0
  15. package/dist/lib/draft-review.js +254 -0
  16. package/dist/lib/draft-sleep.d.ts +65 -0
  17. package/dist/lib/draft-sleep.js +468 -0
  18. package/dist/lib/file-state.d.ts +8 -1
  19. package/dist/lib/file-state.js +115 -18
  20. package/dist/lib/inspector-actions.d.ts +16 -0
  21. package/dist/lib/inspector-actions.js +57 -0
  22. package/dist/lib/inspector-command.d.ts +7 -0
  23. package/dist/lib/inspector-command.js +37 -0
  24. package/dist/lib/inspector-overlay.d.ts +29 -1
  25. package/dist/lib/inspector-overlay.js +537 -90
  26. package/dist/lib/inspector.d.ts +9 -0
  27. package/dist/lib/inspector.js +52 -1
  28. package/dist/lib/observability.d.ts +39 -3
  29. package/dist/lib/observability.js +162 -34
  30. package/dist/lib/paths.d.ts +7 -0
  31. package/dist/lib/paths.js +28 -0
  32. package/dist/lib/recipes-discovery.js +32 -24
  33. package/dist/lib/recipes-usage.d.ts +18 -6
  34. package/dist/lib/recipes-usage.js +445 -34
  35. package/dist/lib/review-control.d.ts +14 -0
  36. package/dist/lib/review-control.js +111 -0
  37. package/dist/lib/review-diagnostics.d.ts +11 -0
  38. package/dist/lib/review-diagnostics.js +148 -0
  39. package/dist/lib/review-projection.d.ts +14 -0
  40. package/dist/lib/review-projection.js +170 -0
  41. package/dist/lib/run-ui-runtime.d.ts +18 -0
  42. package/dist/lib/run-ui-runtime.js +123 -0
  43. package/dist/lib/runs-artifacts.d.ts +1 -1
  44. package/dist/lib/runs-artifacts.js +1 -1
  45. package/dist/lib/runs-control.d.ts +8 -2
  46. package/dist/lib/runs-control.js +23 -6
  47. package/dist/lib/runs-identity.d.ts +1 -1
  48. package/dist/lib/runs-identity.js +1 -1
  49. package/dist/lib/runs-index.d.ts +11 -2
  50. package/dist/lib/runs-index.js +46 -23
  51. package/dist/lib/runs-mailbox.d.ts +1 -1
  52. package/dist/lib/runs-mailbox.js +1 -1
  53. package/dist/lib/runs-messages.d.ts +1 -1
  54. package/dist/lib/runs-messages.js +1 -1
  55. package/dist/lib/runs-outbox.d.ts +1 -1
  56. package/dist/lib/runs-outbox.js +1 -1
  57. package/dist/lib/runs-ownership.d.ts +1 -1
  58. package/dist/lib/runs-ownership.js +1 -1
  59. package/dist/lib/runs-parent-teardown.d.ts +51 -0
  60. package/dist/lib/runs-parent-teardown.js +172 -0
  61. package/dist/lib/runs-process.d.ts +1 -1
  62. package/dist/lib/runs-process.js +1 -1
  63. package/dist/lib/runs-retention.d.ts +1 -1
  64. package/dist/lib/runs-retention.js +1 -1
  65. package/dist/lib/runs-start.d.ts +5 -3
  66. package/dist/lib/runs-start.js +6 -48
  67. package/dist/lib/runs-status.d.ts +5 -3
  68. package/dist/lib/runs-status.js +4 -6
  69. package/dist/lib/runtime.d.ts +7 -1
  70. package/dist/lib/runtime.js +32 -18
  71. package/dist/lib/tool-review-lineage-transaction.d.ts +27 -0
  72. package/dist/lib/tool-review-lineage-transaction.js +597 -0
  73. package/dist/lib/tool-review-lineage.d.ts +24 -0
  74. package/dist/lib/tool-review-lineage.js +98 -0
  75. package/dist/lib/tool-review-scheduler.d.ts +80 -0
  76. package/dist/lib/tool-review-scheduler.js +494 -0
  77. package/dist/lib/tool-review-transaction.d.ts +50 -0
  78. package/dist/lib/tool-review-transaction.js +362 -0
  79. package/dist/lib/tool-review.d.ts +56 -0
  80. package/dist/lib/tool-review.js +197 -0
  81. package/dist/lib/tools-inspect.js +26 -3
  82. package/dist/lib/tools-local.js +4 -2
  83. package/dist/lib/tools-message.d.ts +1 -0
  84. package/dist/lib/tools-message.js +29 -17
  85. package/dist/lib/tools-response.d.ts +1 -1
  86. package/dist/lib/tools-response.js +5 -7
  87. package/dist/lib/tools-spawn.js +4 -1
  88. package/dist/lib/tools.d.ts +1 -0
  89. package/dist/lib/tools.js +1 -0
  90. package/dist/recipes/draft-review.json +24 -0
  91. package/dist/recipes/tool-review.json +24 -0
  92. package/dist/scripts/release-gates.mjs +165 -0
  93. package/dist/skills/actors/SKILL.md +11 -14
  94. package/dist/skills/swarm/SKILL.md +1 -1
  95. package/docs/actor-inspector.md +32 -18
  96. package/docs/async-runs.md +10 -2
  97. package/docs/recipe-library.md +7 -3
  98. package/docs/template-recipes.md +4 -11
  99. package/docs/tool-registry.md +11 -4
  100. package/index.ts +29 -103
  101. package/lib/async-runs.ts +218 -62
  102. package/lib/automatic-review-runtime.ts +135 -0
  103. package/lib/draft-consolidation-transaction.ts +821 -0
  104. package/lib/draft-consolidation.ts +181 -0
  105. package/lib/draft-review.ts +325 -0
  106. package/lib/draft-sleep.ts +576 -0
  107. package/lib/file-state.ts +143 -19
  108. package/lib/inspector-actions.ts +79 -0
  109. package/lib/inspector-command.ts +54 -0
  110. package/lib/inspector-overlay.ts +675 -105
  111. package/lib/inspector.ts +78 -3
  112. package/lib/observability.ts +219 -40
  113. package/lib/paths.ts +43 -0
  114. package/lib/recipes-discovery.ts +34 -26
  115. package/lib/recipes-usage.ts +569 -40
  116. package/lib/review-control.ts +137 -0
  117. package/lib/review-diagnostics.ts +164 -0
  118. package/lib/review-projection.ts +200 -0
  119. package/lib/run-ui-runtime.ts +153 -0
  120. package/lib/runs-artifacts.ts +1 -1
  121. package/lib/runs-control.ts +49 -5
  122. package/lib/runs-identity.ts +1 -1
  123. package/lib/runs-index.ts +57 -21
  124. package/lib/runs-mailbox.ts +1 -1
  125. package/lib/runs-messages.ts +1 -1
  126. package/lib/runs-outbox.ts +1 -1
  127. package/lib/runs-ownership.ts +1 -1
  128. package/lib/runs-parent-teardown.ts +257 -0
  129. package/lib/runs-process.ts +1 -1
  130. package/lib/runs-retention.ts +1 -1
  131. package/lib/runs-start.ts +12 -68
  132. package/lib/runs-status.ts +12 -8
  133. package/lib/runtime.ts +34 -17
  134. package/lib/tool-review-lineage-transaction.ts +881 -0
  135. package/lib/tool-review-lineage.ts +145 -0
  136. package/lib/tool-review-scheduler.ts +635 -0
  137. package/lib/tool-review-transaction.ts +563 -0
  138. package/lib/tool-review.ts +270 -0
  139. package/lib/tools-inspect.ts +33 -3
  140. package/lib/tools-local.ts +8 -2
  141. package/lib/tools-message.ts +45 -30
  142. package/lib/tools-response.ts +5 -6
  143. package/lib/tools-spawn.ts +8 -1
  144. package/lib/tools.ts +5 -0
  145. package/package.json +3 -2
  146. package/recipes/draft-review.json +24 -0
  147. package/recipes/tool-review.json +24 -0
  148. package/scripts/release-gates.mjs +165 -0
  149. package/skills/actors/SKILL.md +11 -14
  150. package/skills/swarm/SKILL.md +1 -1
@@ -7,9 +7,17 @@ import * as path from "node:path";
7
7
  import { stripVTControlCharacters } from "node:util";
8
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
+ ];
15
+ const DETAIL_SECTION_BREAK = { sectionBreak: true };
10
16
  export class ActorInspectorOverlay {
11
17
  done;
18
+ killRun;
12
19
  ownerId;
20
+ readTurns;
13
21
  stateRoot;
14
22
  theme;
15
23
  tui;
@@ -23,22 +31,32 @@ export class ActorInspectorOverlay {
23
31
  detailOpen = false;
24
32
  detailScroll = 0;
25
33
  detailTurn;
34
+ feedback;
35
+ killConfirmation;
26
36
  readKeys = new Set();
27
37
  focus = "tabs";
28
38
  filterControlIndex = 0;
29
39
  menuLevel = "filter";
30
40
  selectorIndex = 0;
41
+ recipeScroll = 0;
31
42
  rowIndex = 0;
32
43
  selectedRun;
33
44
  subagentIndex = 0;
34
- tab = "communications";
45
+ tab = "recipe";
46
+ turnCache;
35
47
  constructor(options) {
36
48
  this.done = options.done;
49
+ this.killRun = options.killRun;
37
50
  this.ownerId = options.ownerId;
51
+ this.readTurns = options.readTurns ?? Inspector.readActorInspectorTurns;
38
52
  this.stateRoot = options.stateRoot;
39
53
  this.theme = options.theme;
40
54
  this.tui = options.tui;
41
- this.refreshTimer = setInterval(() => this.tui.requestRender(), 1000);
55
+ this.refreshTimer = setInterval(() => {
56
+ if (this.focus !== "list" && this.focus !== "detail")
57
+ this.turnCache = undefined;
58
+ this.tui.requestRender();
59
+ }, 1000);
42
60
  this.refreshTimer.unref?.();
43
61
  }
44
62
  handleInput(data) {
@@ -48,6 +66,22 @@ export class ActorInspectorOverlay {
48
66
  this.done();
49
67
  return;
50
68
  }
69
+ if (this.killConfirmation) {
70
+ if (matchesKey(data, "escape") || data.toLowerCase() === "n") {
71
+ this.feedback = { ok: false, message: "Kill cancelled." };
72
+ this.killConfirmation = undefined;
73
+ }
74
+ else if (matchesKey(data, "return") || data.toLowerCase() === "y") {
75
+ this.confirmKill();
76
+ }
77
+ this.tui.requestRender();
78
+ return;
79
+ }
80
+ if (data.toLowerCase() === "k") {
81
+ this.requestKill(runs);
82
+ this.tui.requestRender();
83
+ return;
84
+ }
51
85
  if (matchesKey(data, "escape")) {
52
86
  if (this.focus === "select") {
53
87
  if (this.menuLevel === "value") {
@@ -58,7 +92,9 @@ export class ActorInspectorOverlay {
58
92
  this.focus = this.menuLevel === "run" ? "runs" : "tabs";
59
93
  }
60
94
  else if (this.focus === "detail")
61
- this.closeDetail();
95
+ this.backDetail();
96
+ else if (this.focus === "recipe")
97
+ this.focus = "tabs";
62
98
  else
63
99
  this.done();
64
100
  this.tui.requestRender();
@@ -78,30 +114,62 @@ export class ActorInspectorOverlay {
78
114
  this.tui.requestRender();
79
115
  return;
80
116
  }
117
+ if (this.focus === "recipe") {
118
+ if (matchesKey(data, "up")) {
119
+ if (this.recipeScroll === 0)
120
+ this.focus = "tabs";
121
+ else
122
+ this.recipeScroll -= 1;
123
+ }
124
+ else if (matchesKey(data, "down"))
125
+ this.recipeScroll += 1;
126
+ else if (matchesKey(data, "pageUp"))
127
+ this.recipeScroll = Math.max(0, this.recipeScroll - this.contentViewportRows());
128
+ else if (matchesKey(data, "pageDown"))
129
+ this.recipeScroll += this.contentViewportRows();
130
+ else if (matchesKey(data, "left"))
131
+ this.focus = "tabs";
132
+ this.tui.requestRender();
133
+ return;
134
+ }
81
135
  if (this.focus === "detail") {
82
136
  if (matchesKey(data, "up"))
83
137
  this.detailScroll = Math.max(0, this.detailScroll - 1);
84
138
  else if (matchesKey(data, "down"))
85
139
  this.detailScroll += 1;
140
+ else if (matchesKey(data, "pageUp"))
141
+ this.detailScroll = Math.max(0, this.detailScroll - this.contentViewportRows());
142
+ else if (matchesKey(data, "pageDown"))
143
+ this.detailScroll += this.contentViewportRows();
86
144
  else if (matchesKey(data, "left"))
87
- this.closeDetail();
145
+ this.backDetail();
88
146
  this.tui.requestRender();
89
147
  return;
90
148
  }
91
149
  if (this.focus === "runs") {
92
- if (matchesKey(data, "down"))
150
+ if (matchesKey(data, "left"))
151
+ this.cycleRun(runs, -1);
152
+ else if (matchesKey(data, "right"))
153
+ this.cycleRun(runs, 1);
154
+ else if (matchesKey(data, "down"))
93
155
  this.focus = "tabs";
94
156
  else if (matchesKey(data, "return") && runs.length > 0)
95
157
  this.openRunMenu();
96
158
  }
97
159
  else if (this.focus === "tabs") {
98
160
  if (matchesKey(data, "left") || matchesKey(data, "right")) {
99
- this.tab = this.tab === "communications" ? "turns" : "communications";
161
+ const current = INSPECTOR_TABS.indexOf(this.tab);
162
+ const direction = matchesKey(data, "right") ? 1 : -1;
163
+ this.tab = INSPECTOR_TABS[(current + direction + INSPECTOR_TABS.length) % INSPECTOR_TABS.length];
100
164
  this.filterControlIndex = 0;
165
+ this.recipeScroll = 0;
101
166
  this.rowIndex = 0;
102
167
  }
103
168
  else if (matchesKey(data, "up"))
104
169
  this.focus = "runs";
170
+ else if ((matchesKey(data, "down") || matchesKey(data, "return")) &&
171
+ this.tab === "recipe")
172
+ this.focus = "recipe";
105
173
  else if (matchesKey(data, "down") && this.listItemCount() > 0)
106
174
  this.focus = "list";
107
175
  else if (matchesKey(data, "return"))
@@ -109,7 +177,13 @@ export class ActorInspectorOverlay {
109
177
  }
110
178
  else if (this.focus === "list") {
111
179
  const count = this.tab === "communications" ? this.communicationPreviews().length : this.turnItems().length;
112
- if (matchesKey(data, "up")) {
180
+ if (matchesKey(data, "left"))
181
+ this.focus = "tabs";
182
+ else if (matchesKey(data, "pageUp"))
183
+ this.rowIndex = Math.max(0, this.rowIndex - this.contentViewportRows());
184
+ else if (matchesKey(data, "pageDown"))
185
+ this.rowIndex = Math.min(Math.max(0, count - 1), this.rowIndex + this.contentViewportRows());
186
+ else if (matchesKey(data, "up")) {
113
187
  if (this.rowIndex === 0)
114
188
  this.focus = "tabs";
115
189
  else
@@ -131,7 +205,7 @@ export class ActorInspectorOverlay {
131
205
  lines.push(this.row(this.renderRunControl(), innerWidth));
132
206
  let selector = this.focus === "select"
133
207
  ? this.menuLevel === "run"
134
- ? this.renderMenuBox(this.runs().map((run) => `${run.run} · ${run.status}`), this.selectorIndex)
208
+ ? this.renderMenuBox(this.runs().map((run, index, runs) => `#${runs.length - index} ${run.run} ${run.status}`), this.selectorIndex, undefined, undefined, false, false, this.contentViewportRows())
135
209
  : this.renderFilterMenus(innerWidth)
136
210
  : [];
137
211
  const selectorAnchor = this.selectorAnchor();
@@ -155,16 +229,30 @@ export class ActorInspectorOverlay {
155
229
  else
156
230
  lines.push(this.border("├", "", "┤", innerWidth));
157
231
  this.contentStripeIndices = [];
158
- const content = this.detailOpen
159
- ? this.renderDetail(innerWidth)
160
- : this.selectedRun
161
- ? this.renderTimeline(innerWidth)
162
- : this.renderRunSelector(innerWidth);
163
- const minContentRows = 16;
232
+ const baseContent = this.killConfirmation
233
+ ? this.renderKillConfirmation()
234
+ : this.detailOpen
235
+ ? this.renderDetail(innerWidth)
236
+ : this.selectedRun
237
+ ? this.renderTimeline(innerWidth)
238
+ : this.renderRunSelector(innerWidth);
239
+ const content = this.feedback && !this.killConfirmation
240
+ ? [
241
+ this.theme.fg(this.feedback.ok
242
+ ? "success"
243
+ : /^Kill (failed|rejected):/.test(this.feedback.message)
244
+ ? "error"
245
+ : "warning", ` ${this.feedback.message}`),
246
+ ...baseContent.slice(0, Math.max(0, this.contentViewportRows() - 1)),
247
+ ]
248
+ : baseContent;
249
+ const contentViewportRows = this.contentViewportRows();
164
250
  const selectorRows = selector.slice(1);
165
- for (let index = 0; index < Math.max(content.length, minContentRows); index += 1) {
251
+ for (let index = 0; index < Math.max(content.length, contentViewportRows); index += 1) {
166
252
  const base = content[index] ?? "";
167
- const stripeIndex = this.contentStripeIndices[index] ?? index;
253
+ const stripeIndex = index < content.length
254
+ ? this.contentStripeIndices[index] ?? index
255
+ : 0;
168
256
  const popup = selectorRows[index];
169
257
  if (!popup) {
170
258
  lines.push(this.stripedRow(base, innerWidth, stripeIndex));
@@ -195,7 +283,89 @@ export class ActorInspectorOverlay {
195
283
  if (!this.selectedRun)
196
284
  this.selectedRun = runs[0]?.run;
197
285
  }
286
+ contentViewportRows() {
287
+ const overlayRows = Math.floor(this.tui.terminal.rows * 0.94);
288
+ return Math.max(4, Math.min(16, overlayRows - 7));
289
+ }
290
+ selectRun(run, index) {
291
+ this.selectedRun = run;
292
+ this.turnCache = undefined;
293
+ this.runIndex = index;
294
+ this.recipeScroll = 0;
295
+ this.rowIndex = 0;
296
+ this.subagentIndex = 0;
297
+ this.communicationFrom = "all";
298
+ }
299
+ cycleRun(runs, direction) {
300
+ if (runs.length === 0)
301
+ return;
302
+ const currentIndex = Math.max(0, runs.findIndex((run) => run.run === this.selectedRun));
303
+ const nextIndex = (currentIndex + direction + runs.length) % runs.length;
304
+ const next = runs[nextIndex];
305
+ if (next)
306
+ this.selectRun(next.run, nextIndex);
307
+ }
308
+ requestKill(runs) {
309
+ this.feedback = undefined;
310
+ if (this.focus !== "runs") {
311
+ this.feedback = { ok: false, message: "Focus Run before using Kill." };
312
+ return;
313
+ }
314
+ const run = runs.find((item) => item.run === this.selectedRun);
315
+ if (!run) {
316
+ this.feedback = { ok: false, message: "Kill unavailable: no owned run." };
317
+ return;
318
+ }
319
+ if (!this.killRun) {
320
+ this.feedback = { ok: false, message: "Kill unavailable in this Inspector." };
321
+ return;
322
+ }
323
+ if (run.status !== "running") {
324
+ this.feedback = {
325
+ ok: false,
326
+ message: `Kill unavailable: run is ${run.status}.`,
327
+ };
328
+ return;
329
+ }
330
+ if (!run.runInstanceId) {
331
+ this.feedback = {
332
+ ok: false,
333
+ message: "Kill unavailable: run generation unavailable.",
334
+ };
335
+ return;
336
+ }
337
+ this.killConfirmation = {
338
+ run: run.run,
339
+ runInstanceId: run.runInstanceId,
340
+ };
341
+ }
342
+ confirmKill() {
343
+ const confirmation = this.killConfirmation;
344
+ this.killConfirmation = undefined;
345
+ if (!confirmation || !this.killRun) {
346
+ this.feedback = { ok: false, message: "Kill unavailable in this Inspector." };
347
+ return;
348
+ }
349
+ try {
350
+ this.feedback = this.killRun(confirmation.run, confirmation.runInstanceId);
351
+ }
352
+ catch (error) {
353
+ const message = error instanceof Error ? error.message : String(error);
354
+ this.feedback = {
355
+ ok: false,
356
+ message: `Kill failed: ${message.replaceAll(/\s+/g, " ").slice(0, 160)}.`,
357
+ };
358
+ }
359
+ }
360
+ renderKillConfirmation() {
361
+ return [
362
+ this.theme.fg("error", ` Kill running actor run:${this.killConfirmation?.run}?`),
363
+ this.theme.fg("muted", " This sends canonical control.kill and cannot be undone."),
364
+ ];
365
+ }
198
366
  listItemCount() {
367
+ if (this.tab === "recipe")
368
+ return 0;
199
369
  return this.tab === "communications"
200
370
  ? this.communicationPreviews().length
201
371
  : this.turnItems().length;
@@ -203,30 +373,47 @@ export class ActorInspectorOverlay {
203
373
  selectorAnchor() {
204
374
  if (this.menuLevel === "run") {
205
375
  const runControl = stripVTControlCharacters(this.renderRunControl());
206
- return Math.max(0, runControl.indexOf("Run:") - 1);
376
+ return Math.max(0, runControl.indexOf("Run:") - 2);
207
377
  }
208
- const label = this.tab === "communications" ? "Messages" : "Turns";
378
+ const label = this.tab === "communications"
379
+ ? "Messages"
380
+ : this.tab === "turns"
381
+ ? "Turns"
382
+ : "Recipe";
209
383
  const tabs = stripVTControlCharacters(this.renderTabs());
210
384
  return Math.max(0, tabs.indexOf(label) - 2);
211
385
  }
212
386
  renderKeyHints() {
213
387
  const hint = (keys, description) => `${this.theme.fg("accent", keys)}${this.theme.fg("dim", ` ${description}`)}`;
388
+ if (this.killConfirmation)
389
+ return ` ${hint("y/enter", "confirm kill")} ${hint("n/esc", "cancel")}`;
214
390
  if (this.focus === "select")
215
391
  return this.menuLevel === "value"
216
392
  ? ` ${hint("↑↓", "option")} ${hint("enter", "apply")} ${hint("←/esc", "back")}`
217
393
  : ` ${hint("↑↓", "option")} ${hint("→/enter", "open")} ${hint("←/esc", "back")}`;
394
+ if (this.focus === "recipe")
395
+ return ` ${hint("↑↓/pgup/pgdn", "scroll")} ${hint("↑ at top", "tabs")} ${hint("←/esc", "tabs")}`;
218
396
  if (this.focus === "detail")
219
- return ` ${hint("↑↓", "scroll")} ${hint("←/esc", "back")}`;
397
+ return ` ${hint("↑↓/pgup/pgdn", "scroll")} ${hint("←/esc", "back")}`;
220
398
  if (this.focus === "list")
221
- return ` ${hint("↑↓", "row")} ${hint("→/enter", "open")} ${hint("esc", "close")}`;
399
+ return ` ${hint("↑↓/pgup/pgdn", "row")} ${hint("→/enter", "open")} ${hint("←", "tabs")} ${hint("esc", "close")}`;
400
+ if (this.focus === "runs") {
401
+ const run = this.runs().find((item) => item.run === this.selectedRun);
402
+ const killHint = this.killRun && run?.status === "running" && run.runInstanceId
403
+ ? ` ${hint("k", "kill")}`
404
+ : "";
405
+ return ` ${hint("←→", "run")} ${hint("↓", "tabs")} ${hint("enter", "list")}${killHint} ${hint("esc", "close")}`;
406
+ }
222
407
  return ` ${hint("←→", "navigate")} ${hint("↑↓", "change row")} ${hint("enter", "select")} ${hint("esc", "close")}`;
223
408
  }
224
409
  renderRunControl() {
225
- const run = this.runs().find((item) => item.run === this.selectedRun);
410
+ const runs = this.runs();
411
+ const runIndex = runs.findIndex((item) => item.run === this.selectedRun);
412
+ const run = runs[runIndex];
226
413
  const focused = this.focus === "runs";
227
414
  const active = focused || (this.focus === "select" && this.menuLevel === "run");
228
- const prefix = active ? this.theme.fg("accent", " [ ") : " ";
229
- const suffix = active ? this.theme.fg("accent", " ] ") : " ";
415
+ const prefix = active ? this.theme.fg("accent", " ← ") : " ";
416
+ const suffix = active ? this.theme.fg("accent", " → ") : " ";
230
417
  if (!run) {
231
418
  const empty = this.theme.fg("muted", `${prefix}Run: none${suffix}`);
232
419
  return focused ? this.theme.bg("selectedBg", empty) : empty;
@@ -238,7 +425,8 @@ export class ActorInspectorOverlay {
238
425
  : run.status === "done"
239
426
  ? "success"
240
427
  : "muted";
241
- const value = `${prefix}${this.theme.fg("muted", "Run: ")}${this.theme.fg("accent", run.run)}${this.theme.fg("muted", " · ")}${this.theme.fg(statusColor, run.status)}${suffix}`;
428
+ const sequence = runs.length - runIndex;
429
+ const value = `${prefix}${this.theme.fg("muted", "Run: ")}${this.theme.fg("text", `#${sequence}`)} ${this.theme.fg("accent", run.run)} ${this.theme.fg(statusColor, run.status)}${suffix}`;
242
430
  return focused ? this.theme.bg("selectedBg", value) : value;
243
431
  }
244
432
  activeMessageFilters() {
@@ -263,19 +451,19 @@ export class ActorInspectorOverlay {
263
451
  };
264
452
  const messageFilters = this.activeMessageFilters();
265
453
  const subagent = this.subagents()[this.subagentIndex] ?? "all";
266
- const messagesLabel = `Messages${messageFilters.length > 0 ? ` · ${messageFilters.join(" · ")}` : ""}`;
267
- const turnsLabel = `Turns${subagent === "all" ? "" : ` · ${subagent}`}`;
268
- return `${tab("communications", messagesLabel)} ${tab("turns", turnsLabel)}`;
454
+ const messagesLabel = `Messages${messageFilters.length > 0 ? ` (${messageFilters.join(", ")})` : ""}`;
455
+ const turnsLabel = `Turns${subagent === "all" ? "" : ` (${subagent})`}`;
456
+ return `${tab("recipe", "Recipe")} ${tab("communications", messagesLabel)} ${tab("turns", turnsLabel)}`;
269
457
  }
270
458
  renderRunSelector(width) {
271
459
  const runs = this.runs();
272
460
  if (runs.length === 0)
273
461
  return [this.theme.fg("muted", " No owned actor runs")];
274
462
  this.runIndex = Math.min(this.runIndex, runs.length - 1);
275
- return runs.slice(0, 16).map((run, index) => {
463
+ return runs.slice(0, this.contentViewportRows()).map((run, index) => {
276
464
  const selected = index === this.runIndex;
277
465
  const prefix = selected ? this.theme.fg("accent", " ▶ ") : " ";
278
- const label = `${prefix}run:${run.run}`;
466
+ const label = `${prefix}#${runs.length - index} run:${run.run}`;
279
467
  const status = ` ${run.status}`;
280
468
  return selected
281
469
  ? this.theme.fg("accent", `${label}${status}`)
@@ -290,6 +478,8 @@ export class ActorInspectorOverlay {
290
478
  this.selectedRun = undefined;
291
479
  return [this.theme.fg("warning", " Selected run is no longer owned by this session")];
292
480
  }
481
+ if (this.tab === "recipe")
482
+ return this.renderRecipe(width);
293
483
  const rows = this.tab === "communications"
294
484
  ? this.communicationRows()
295
485
  : this.turnRows();
@@ -300,16 +490,20 @@ export class ActorInspectorOverlay {
300
490
  : (this.subagents()[this.subagentIndex] ?? "all") !== "all";
301
491
  return [
302
492
  this.theme.fg("muted", filtered
303
- ? " No results for the active filters · Enter on the tab to adjust"
493
+ ? " No results for the active filters. Enter on the tab to adjust."
304
494
  : this.tab === "communications"
305
495
  ? " No messages in this run"
306
496
  : " No persisted turns in this run"),
307
497
  ];
308
498
  }
309
499
  this.rowIndex = Math.min(this.rowIndex, rows.length - 1);
310
- const start = Math.max(0, Math.min(this.rowIndex - 7, Math.max(0, rows.length - 15)));
311
- const visibleRows = rows.slice(start, start + 15);
312
- this.contentStripeIndices = visibleRows.map((_, offset) => start + offset);
500
+ const viewportRows = this.contentViewportRows();
501
+ const start = Math.max(0, Math.min(this.rowIndex - Math.floor(viewportRows / 2), Math.max(0, rows.length - viewportRows)));
502
+ const stripeIndices = this.tab === "communications"
503
+ ? this.communicationPreviews().map((preview, index) => Math.max(0, (preview.sequence ?? index + 1) - 1))
504
+ : this.turnItems().map((turn) => Math.max(0, turn.index - 1));
505
+ const visibleRows = rows.slice(start, start + viewportRows);
506
+ this.contentStripeIndices = visibleRows.map((_, offset) => stripeIndices[start + offset] ?? start + offset);
313
507
  return visibleRows.map((row, offset) => {
314
508
  const index = start + offset;
315
509
  const selected = this.focus === "list" && index === this.rowIndex;
@@ -318,6 +512,28 @@ export class ActorInspectorOverlay {
318
512
  : ` ${row}`;
319
513
  });
320
514
  }
515
+ renderRecipe(width) {
516
+ if (!this.selectedRun)
517
+ return [];
518
+ const recipe = Inspector.readActorInspectorRecipe(path.join(this.stateRoot, this.selectedRun));
519
+ const logicalLines = [
520
+ ...this.detailSection("Identity", this.readableValueLines(recipe.identity, " ")),
521
+ ...(recipe.definition
522
+ ? this.detailSection("Recipe", this.readableValueLines(recipe.definition, " "))
523
+ : []),
524
+ ...this.detailSection("Launch", this.readableValueLines(recipe.launch, " ")),
525
+ ...(recipe.composition.length > 0
526
+ ? this.detailSection("Composition", this.readableValueLines(recipe.composition, " "))
527
+ : []),
528
+ ...this.detailSection("Diagnostics", recipe.diagnostics),
529
+ ];
530
+ const wrapped = this.wrapDetailLines(logicalLines.length > 0 ? logicalLines : [" No persisted recipe evidence"], width);
531
+ const viewportRows = this.contentViewportRows();
532
+ const maxStart = Math.max(0, wrapped.lines.length - viewportRows);
533
+ this.recipeScroll = Math.min(this.recipeScroll, maxStart);
534
+ this.contentStripeIndices = wrapped.stripeIndices.slice(this.recipeScroll, this.recipeScroll + viewportRows);
535
+ return wrapped.lines.slice(this.recipeScroll, this.recipeScroll + viewportRows);
536
+ }
321
537
  communicationPreviews() {
322
538
  if (!this.selectedRun)
323
539
  return [];
@@ -331,9 +547,11 @@ export class ActorInspectorOverlay {
331
547
  readKeys: this.readKeys,
332
548
  run: this.selectedRun,
333
549
  unreadOnly: this.communicationUnread,
334
- }).filter((preview) => this.communicationFrom === "all" ||
550
+ })
551
+ .filter((preview) => this.communicationFrom === "all" ||
335
552
  this.communicationActorLabel(preview.from_display ?? preview.from) ===
336
- this.communicationFrom);
553
+ this.communicationFrom)
554
+ .reverse();
337
555
  }
338
556
  communicationActorLabel(actor) {
339
557
  const value = actor ?? "unknown";
@@ -352,7 +570,7 @@ export class ActorInspectorOverlay {
352
570
  const actor = preview.from_display ?? preview.from ?? "unknown";
353
571
  const target = preview.channel === "room" ? "#all" : preview.to;
354
572
  const text = preview.summary ?? preview.body_preview ?? "(no body)";
355
- return `${preview.needs_response ? "! " : ""}${actor} → ${target} ${preview.type} ${text}`;
573
+ return `#${preview.sequence ?? "?"} ${preview.needs_response ? "! " : ""}${actor} → ${target} ${preview.type} ${text}`;
356
574
  });
357
575
  }
358
576
  renderDetail(width) {
@@ -372,66 +590,287 @@ export class ActorInspectorOverlay {
372
590
  ["inbox", preview.inbox_status],
373
591
  ["timestamp", preview.timestamp],
374
592
  ];
375
- return fields
593
+ const lines = fields
376
594
  .filter(([, value]) => value !== undefined)
377
595
  .map(([label, value]) => truncateToWidth(` ${label.padEnd(12, " ")} ${typeof value === "string" ? value : JSON.stringify(value)}`, width, "…"));
596
+ const viewportRows = this.contentViewportRows();
597
+ const maxStart = Math.max(0, lines.length - viewportRows);
598
+ this.detailScroll = Math.min(this.detailScroll, maxStart);
599
+ return lines.slice(this.detailScroll, this.detailScroll + viewportRows);
378
600
  }
379
601
  const turn = this.detailTurn;
380
602
  if (!turn)
381
603
  return [this.theme.fg("warning", " Turn row is no longer available")];
382
- const value = (input) => typeof input === "string" ? input : JSON.stringify(input);
383
- const fields = [
384
- ` command ${turn.commandId}${turn.stage ? ` · ${turn.stage}` : ""}`,
385
- ` session ${turn.sessionFile}`,
386
- ...(turn.promptFile
387
- ? [` prompt ${turn.promptFile}${turn.promptBytes ? ` · ${turn.promptBytes} bytes` : ""}`]
604
+ const logicalLines = this.turnEvidenceLines(turn);
605
+ const wrapped = this.wrapDetailLines(logicalLines, width);
606
+ const viewportRows = this.contentViewportRows();
607
+ const maxStart = Math.max(0, wrapped.lines.length - viewportRows);
608
+ this.detailScroll = Math.min(this.detailScroll, maxStart);
609
+ this.contentStripeIndices = wrapped.stripeIndices.slice(this.detailScroll, this.detailScroll + viewportRows);
610
+ return wrapped.lines.slice(this.detailScroll, this.detailScroll + viewportRows);
611
+ }
612
+ wrapDetailLines(lines, width) {
613
+ const wrappedLines = [];
614
+ const stripeIndices = [];
615
+ let sectionIndex = 0;
616
+ for (const line of lines) {
617
+ if (typeof line !== "string") {
618
+ sectionIndex += 1;
619
+ continue;
620
+ }
621
+ const physicalLines = line.replaceAll("\t", " ").split(/\r?\n/);
622
+ for (const physicalLine of physicalLines) {
623
+ if (!physicalLine.trim())
624
+ continue;
625
+ const leading = physicalLine.match(/^ */)?.[0] ?? "";
626
+ const content = physicalLine.slice(leading.length);
627
+ const contentWidth = Math.max(1, width - visibleWidth(leading));
628
+ const labeled = /^(\s*(?:-\s+)?[^:]+:\s+)(\S.*)$/u.exec(physicalLine);
629
+ let wrapped;
630
+ if (visibleWidth(physicalLine) > width &&
631
+ labeled &&
632
+ visibleWidth(labeled[1]) < width) {
633
+ const prefix = labeled[1];
634
+ const value = labeled[2];
635
+ const firstWidth = Math.max(1, width - visibleWidth(prefix));
636
+ const first = this.wrapPreferred(value, firstWidth)[0] ??
637
+ this.takeVisiblePrefix(value, firstWidth);
638
+ const remainder = value.slice(first.length).trimStart();
639
+ const continuation = `${leading} `;
640
+ const continuationWidth = Math.max(1, width - visibleWidth(continuation));
641
+ wrapped = [
642
+ `${prefix}${first}`,
643
+ ...(remainder
644
+ ? this.wrapPreferred(remainder, continuationWidth).map((visualLine) => `${continuation}${visualLine}`)
645
+ : []),
646
+ ];
647
+ }
648
+ else {
649
+ wrapped = visibleWidth(physicalLine) <= width
650
+ ? [physicalLine]
651
+ : this.wrapPreferred(content, contentWidth).map((visualLine) => `${leading}${visualLine}`);
652
+ }
653
+ for (const visualLine of wrapped) {
654
+ if (!visualLine.trim())
655
+ continue;
656
+ wrappedLines.push(visualLine);
657
+ stripeIndices.push(sectionIndex);
658
+ }
659
+ }
660
+ }
661
+ return { lines: wrappedLines, stripeIndices };
662
+ }
663
+ wrapPreferred(content, width) {
664
+ const lines = [];
665
+ let remaining = stripVTControlCharacters(content).trimEnd();
666
+ while (remaining && visibleWidth(remaining) > width) {
667
+ const candidate = this.takeVisiblePrefix(remaining, width);
668
+ let splitAt = -1;
669
+ let trimBoundary = false;
670
+ for (let index = 1; index < candidate.length; index += 1) {
671
+ if (/\s/u.test(candidate[index])) {
672
+ splitAt = index + 1;
673
+ trimBoundary = true;
674
+ }
675
+ else if (/[-+=/_,.:]/u.test(candidate[index])) {
676
+ splitAt = index;
677
+ trimBoundary = false;
678
+ }
679
+ }
680
+ if (splitAt <= 0)
681
+ splitAt = candidate.length;
682
+ const head = remaining.slice(0, splitAt).trimEnd();
683
+ if (!head) {
684
+ lines.push(candidate);
685
+ remaining = remaining.slice(candidate.length);
686
+ continue;
687
+ }
688
+ lines.push(head);
689
+ remaining = remaining.slice(splitAt);
690
+ if (trimBoundary)
691
+ remaining = remaining.trimStart();
692
+ }
693
+ if (remaining)
694
+ lines.push(remaining);
695
+ return lines;
696
+ }
697
+ detailSection(title, content) {
698
+ if (content.length === 0)
699
+ return [];
700
+ return [
701
+ ` ${title}`,
702
+ ...content.flatMap((line) => line
703
+ .split(/\r?\n/)
704
+ .filter((physicalLine) => physicalLine.trim().length > 0)
705
+ .map((physicalLine) => ` ${physicalLine}`)),
706
+ DETAIL_SECTION_BREAK,
707
+ ];
708
+ }
709
+ readableValueLines(value, indent = "", depth = 0) {
710
+ if (depth > 8)
711
+ return [`${indent}[nested value]`];
712
+ if (typeof value === "string") {
713
+ const trimmed = value.trim();
714
+ if (/^[{[]/.test(trimmed)) {
715
+ try {
716
+ return this.readableValueLines(JSON.parse(trimmed), indent, depth + 1);
717
+ }
718
+ catch {
719
+ // Keep non-JSON text as written.
720
+ }
721
+ }
722
+ return value.split(/\r?\n/).map((line) => `${indent}${line}`);
723
+ }
724
+ if (value === null || value === undefined)
725
+ return [`${indent}${String(value)}`];
726
+ if (typeof value !== "object")
727
+ return [`${indent}${String(value)}`];
728
+ if (Array.isArray(value)) {
729
+ if (value.length === 0)
730
+ return [`${indent}(empty)`];
731
+ return value.flatMap((item) => {
732
+ if (item &&
733
+ typeof item === "object" &&
734
+ item.type === "text" &&
735
+ typeof item.text === "string") {
736
+ return this.readableValueLines(item.text, indent, depth + 1);
737
+ }
738
+ if (!item || typeof item !== "object")
739
+ return [`${indent}- ${String(item)}`];
740
+ const [first = "", ...rest] = this.readableValueLines(item, `${indent} `, depth + 1);
741
+ return [
742
+ `${indent}- ${first.trimStart()}`,
743
+ ...rest,
744
+ ];
745
+ });
746
+ }
747
+ const entries = Object.entries(value);
748
+ if (entries.length === 0)
749
+ return [`${indent}(empty)`];
750
+ return entries.flatMap(([key, item]) => {
751
+ if (!item || typeof item !== "object") {
752
+ const scalar = typeof item === "string" ? item : String(item);
753
+ const [first = "", ...rest] = scalar.split(/\r?\n/);
754
+ return [
755
+ `${indent}${key}: ${first}`,
756
+ ...rest.map((line) => `${indent} ${line}`),
757
+ ];
758
+ }
759
+ return [
760
+ `${indent}${key}:`,
761
+ ...this.readableValueLines(item, `${indent} `, depth + 1),
762
+ ];
763
+ });
764
+ }
765
+ turnEvidenceLines(turn) {
766
+ const lines = [];
767
+ const commandNumber = /^command-(\d+)$/.exec(turn.commandId)?.[1];
768
+ const subagent = commandNumber
769
+ ? String(Number.parseInt(commandNumber, 10))
770
+ : turn.commandId;
771
+ const stage = turn.stage && turn.stage !== "subagent" && turn.stage !== "command"
772
+ ? ` (${turn.stage})`
773
+ : "";
774
+ lines.push(` Subagent ${subagent}${stage}`, DETAIL_SECTION_BREAK);
775
+ if (turn.userText)
776
+ lines.push(...this.detailSection("User", [this.readablePromptText(turn.userText)]));
777
+ if (turn.thinking)
778
+ lines.push(...this.detailSection("Thinking", [turn.thinking]));
779
+ if (turn.assistantText)
780
+ lines.push(...this.detailSection("Assistant", [turn.assistantText]));
781
+ for (const [index, tool] of turn.toolCalls.entries()) {
782
+ const content = [
783
+ ...(tool.arguments === undefined
784
+ ? []
785
+ : ["Input", ...this.readableValueLines(tool.arguments, " ")]),
786
+ ...(tool.result === undefined
787
+ ? []
788
+ : ["Result", ...this.readableValueLines(tool.result, " ")]),
789
+ `call id: ${tool.id}`,
790
+ ];
791
+ lines.push(...this.detailSection(`Tool ${index + 1} (${tool.name}${tool.resultError ? ", Error" : ""})`, content));
792
+ }
793
+ const execution = [
794
+ ...(!turn.thinking ? ["thinking: not persisted"] : []),
795
+ ...(turn.model ? [`model: ${turn.model}`] : []),
796
+ ...(turn.stopReason ? [`stop reason: ${turn.stopReason}`] : []),
797
+ ...(turn.error ? [`error: ${turn.error}`] : []),
798
+ ...(turn.usage === undefined
799
+ ? []
800
+ : ["Usage", ...this.readableValueLines(turn.usage, " ")]),
801
+ ...(turn.unmatchedToolResults > 0
802
+ ? [`unmatched tool results: ${turn.unmatchedToolResults}`]
388
803
  : []),
389
- ...(turn.recipeContext !== undefined
390
- ? [` recipe context ${value(turn.recipeContext)}`]
804
+ ];
805
+ if (execution.length > 0)
806
+ lines.push(...this.detailSection("Execution", execution));
807
+ if (turn.diagnostics.length > 0)
808
+ lines.push(...this.detailSection("Diagnostics", turn.diagnostics));
809
+ const provenance = [
810
+ "Session",
811
+ ` ${turn.sessionFile}`,
812
+ ...(turn.sessionTruncated ? [" turn list truncated"] : []),
813
+ ...(turn.promptFile
814
+ ? [
815
+ "Prompt",
816
+ ` ${turn.promptFile}${turn.promptBytes ? ` (${turn.promptBytes} bytes)` : ""}`,
817
+ ]
391
818
  : []),
392
- ...(turn.userText ? [` user ${turn.userText}`] : []),
393
- ...(turn.assistantText ? [` assistant ${turn.assistantText}`] : []),
394
- ` reasoning ${turn.thinking ? "persisted thinking block" : "unavailable"}`,
395
- ...(turn.thinking ? [` thinking ${turn.thinking}`] : []),
396
- ...(turn.stopReason ? [` stop reason ${turn.stopReason}`] : []),
397
- ...(turn.usage !== undefined ? [` usage ${value(turn.usage)}`] : []),
398
- ...(turn.error ? [` error ${turn.error}`] : []),
399
- ` tools ${turn.toolCalls.length}`,
400
819
  ];
401
- for (const [index, tool] of turn.toolCalls.entries()) {
402
- fields.push(` tool ${String(index + 1).padEnd(9, " ")} ${tool.name} · ${tool.id}${tool.resultError ? " · error" : ""}`);
403
- if (tool.arguments !== undefined)
404
- fields.push(` arguments ${value(tool.arguments)}`);
405
- if (tool.result !== undefined)
406
- fields.push(` result ${value(tool.result)}`);
407
- }
408
- if (turn.unmatchedToolResults > 0)
409
- fields.push(` unmatched ${turn.unmatchedToolResults}`);
410
- if (turn.sessionTruncated)
411
- fields.push(" session turn list truncated");
412
- for (const diagnostic of turn.diagnostics)
413
- fields.push(` diagnostic ${diagnostic}`);
414
- const maxStart = Math.max(0, fields.length - 16);
415
- this.detailScroll = Math.min(this.detailScroll, maxStart);
416
- return fields
417
- .slice(this.detailScroll, this.detailScroll + 16)
418
- .map((field) => truncateToWidth(field, width, "…"));
820
+ lines.push(...this.detailSection("Provenance", provenance));
821
+ return lines.length > 0 ? lines : [" No persisted turn evidence"];
419
822
  }
420
- turnItems() {
823
+ readablePromptText(text) {
824
+ const wrapped = /^\s*<file\b[^>]*>\s*([\s\S]*?)\s*<\/file>\s*$/i.exec(text);
825
+ return wrapped?.[1]?.trim() || text;
826
+ }
827
+ allTurnItems() {
421
828
  if (!this.selectedRun)
422
829
  return [];
830
+ if (this.turnCache?.run === this.selectedRun)
831
+ return this.turnCache.turns;
832
+ const turns = this.readTurns(path.join(this.stateRoot, this.selectedRun));
833
+ this.turnCache = { run: this.selectedRun, turns };
834
+ return turns;
835
+ }
836
+ turnItems() {
423
837
  const selectedSubagent = this.subagents()[this.subagentIndex];
424
- return Inspector.readActorInspectorTurns(path.join(this.stateRoot, this.selectedRun)).filter((turn) => !selectedSubagent ||
838
+ return this.allTurnItems()
839
+ .filter((turn) => !selectedSubagent ||
425
840
  selectedSubagent === "all" ||
426
- turn.commandId === selectedSubagent);
841
+ turn.commandId === selectedSubagent)
842
+ .reverse();
427
843
  }
428
844
  turnRows() {
429
- return this.turnItems().map((turn) => `${turn.commandId}${turn.stage ? `/${turn.stage}` : ""} ${turn.provider ?? "unknown"}/${turn.model ?? "unknown"} tools:${turn.toolCalls.length} ${turn.assistantText ?? turn.userText ?? "(no text)"}`);
845
+ return this.turnItems().map((turn) => {
846
+ const commandNumber = /^command-(\d+)$/.exec(turn.commandId)?.[1];
847
+ const subagent = commandNumber
848
+ ? `Subagent ${Number.parseInt(commandNumber, 10)}`
849
+ : `Subagent ${turn.commandId}`;
850
+ const stage = turn.stage && turn.stage !== "subagent" && turn.stage !== "command"
851
+ ? ` (${turn.stage})`
852
+ : "";
853
+ const toolNames = [...new Set(turn.toolCalls.map((tool) => tool.name))];
854
+ const action = turn.toolCalls.length === 0
855
+ ? ""
856
+ : turn.toolCalls.length === 1
857
+ ? ` (${toolNames[0]})`
858
+ : toolNames.length === 1
859
+ ? ` (${toolNames[0]}, ${turn.toolCalls.length} calls)`
860
+ : turn.toolCalls.length <= 2
861
+ ? ` (${toolNames.join(", ")})`
862
+ : ` (${turn.toolCalls.length} tools)`;
863
+ const failed = turn.error || turn.toolCalls.some((tool) => tool.resultError)
864
+ ? " (error)"
865
+ : "";
866
+ const preview = this.readablePromptText(turn.assistantText ?? turn.userText ?? "(no text)").replaceAll(/\s+/g, " ").trim();
867
+ return `#${turn.index} ${subagent}${stage} ${turn.model ?? "unknown model"}${action}${failed} ${preview}`;
868
+ });
430
869
  }
431
870
  subagents() {
432
871
  if (!this.selectedRun || this.tab !== "turns")
433
872
  return ["all"];
434
- const ids = Inspector.readActorInspectorTurns(path.join(this.stateRoot, this.selectedRun)).map((turn) => turn.commandId);
873
+ const ids = this.allTurnItems().map((turn) => turn.commandId);
435
874
  return ["all", ...new Set(ids)];
436
875
  }
437
876
  valueOptions() {
@@ -478,11 +917,7 @@ export class ActorInspectorOverlay {
478
917
  if (!value)
479
918
  return;
480
919
  if (this.menuLevel === "run") {
481
- this.selectedRun = value;
482
- this.runIndex = Math.max(0, this.runs().findIndex((run) => run.run === value));
483
- this.rowIndex = 0;
484
- this.subagentIndex = 0;
485
- this.communicationFrom = "all";
920
+ this.selectRun(value, Math.max(0, this.runs().findIndex((run) => run.run === value)));
486
921
  this.focus = "runs";
487
922
  return;
488
923
  }
@@ -518,12 +953,20 @@ export class ActorInspectorOverlay {
518
953
  currentFilterValue() {
519
954
  return this.filterValue(this.filterControlIndex);
520
955
  }
521
- renderMenuBox(options, focusedIndex, current, parentIndex, omitLeftBorder = false, omitRightBorder = false) {
522
- const contentWidth = Math.max(...options.map((option) => option.length + 4));
523
- const border = (left, right) => this.theme.fg("border", `${omitLeftBorder ? "" : left}${"─".repeat(contentWidth)}${omitRightBorder ? "" : right}`);
956
+ renderMenuBox(options, focusedIndex, current, parentIndex, omitLeftBorder = false, omitRightBorder = false, maxVisibleOptions = options.length) {
957
+ const visibleLimit = Math.max(1, Math.min(maxVisibleOptions, options.length));
958
+ const anchorIndex = Math.max(0, Math.min(options.length - 1, focusedIndex >= 0 ? focusedIndex : (parentIndex ?? 0)));
959
+ const maxStart = Math.max(0, options.length - visibleLimit);
960
+ const start = Math.max(0, Math.min(anchorIndex - Math.floor(visibleLimit / 2), maxStart));
961
+ const visibleOptions = options.slice(start, start + visibleLimit);
962
+ const hiddenAbove = start > 0;
963
+ const hiddenBelow = start + visibleOptions.length < options.length;
964
+ const contentWidth = Math.max(4, ...options.map((option) => visibleWidth(option) + 4));
965
+ const border = (left, right, marker = "") => this.theme.fg("border", `${omitLeftBorder ? "" : left}${marker}${"─".repeat(Math.max(0, contentWidth - visibleWidth(marker)))}${omitRightBorder ? "" : right}`);
524
966
  return [
525
- border("╭", "╮"),
526
- ...options.map((option, index) => {
967
+ border("╭", "╮", hiddenAbove ? "↑" : ""),
968
+ ...visibleOptions.map((option, localIndex) => {
969
+ const index = start + localIndex;
527
970
  const focused = index === focusedIndex;
528
971
  const row = this.fit(`${focused ? " ▶ " : " "}${option}`, contentWidth);
529
972
  const colored = focused || option === current || index === parentIndex
@@ -534,17 +977,18 @@ export class ActorInspectorOverlay {
534
977
  : colored;
535
978
  return `${omitLeftBorder ? "" : this.theme.fg("border", "│")}${styled}${omitRightBorder ? "" : this.theme.fg("border", "│")}`;
536
979
  }),
537
- border("╰", "╯"),
980
+ border("╰", "╯", hiddenBelow ? "↓" : ""),
538
981
  ];
539
982
  }
540
983
  renderFilterMenus(_width) {
541
984
  const filters = this.filterOptions().map((filter, index) => `${filter}: ${this.filterValue(index)}`);
985
+ const maxVisibleOptions = Math.max(1, this.contentViewportRows() - 1);
542
986
  const parentFocus = this.menuLevel === "filter" ? this.selectorIndex : -1;
543
- const parent = this.renderMenuBox(filters, parentFocus, undefined, this.menuLevel === "value" ? this.filterControlIndex : undefined);
987
+ const parent = this.renderMenuBox(filters, parentFocus, undefined, this.menuLevel === "value" ? this.filterControlIndex : undefined, false, false, maxVisibleOptions);
544
988
  if (this.menuLevel !== "value")
545
989
  return parent;
546
- const parentShared = this.renderMenuBox(filters, parentFocus, undefined, this.filterControlIndex, false, true);
547
- const child = this.renderMenuBox(this.valueOptions(), this.selectorIndex, this.currentFilterValue());
990
+ const parentShared = this.renderMenuBox(filters, parentFocus, undefined, this.filterControlIndex, false, true, maxVisibleOptions);
991
+ const child = this.renderMenuBox(this.valueOptions(), this.selectorIndex, this.currentFilterValue(), undefined, false, false, maxVisibleOptions);
548
992
  return Array.from({ length: Math.max(parent.length, child.length) }, (_, index) => child[index]
549
993
  ? `${parentShared[index] ?? " ".repeat(visibleWidth(parentShared[0]))}${child[index]}`
550
994
  : (parent[index] ?? ""));
@@ -563,6 +1007,9 @@ export class ActorInspectorOverlay {
563
1007
  if (this.detailOpen)
564
1008
  this.focus = "detail";
565
1009
  }
1010
+ backDetail() {
1011
+ this.closeDetail();
1012
+ }
566
1013
  closeDetail() {
567
1014
  this.detailOpen = false;
568
1015
  this.detailCommunication = undefined;