@llblab/pi-actors 0.42.3 → 0.43.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 (279) hide show
  1. package/AGENTS.md +127 -175
  2. package/BACKLOG.md +189 -1
  3. package/CHANGELOG.md +183 -292
  4. package/README.md +115 -276
  5. package/dist/fixtures/protocol/control-endpoint.json +6 -0
  6. package/dist/fixtures/protocol/control-record.json +9 -0
  7. package/dist/fixtures/protocol/recipe-summary.json +4 -12
  8. package/dist/fixtures/protocol/trace-event.json +9 -0
  9. package/dist/index.js +1 -1
  10. package/dist/lib/async-runs.d.ts +15 -38
  11. package/dist/lib/async-runs.js +173 -111
  12. package/dist/lib/automatic-review-runtime.d.ts +1 -1
  13. package/dist/lib/automatic-review-runtime.js +5 -5
  14. package/dist/lib/command-templates.d.ts +2 -0
  15. package/dist/lib/command-templates.js +38 -4
  16. package/dist/lib/control-projection.d.ts +20 -0
  17. package/dist/lib/control-projection.js +66 -0
  18. package/dist/lib/control.d.ts +15 -0
  19. package/dist/lib/control.js +97 -0
  20. package/dist/lib/draft-sleep.js +3 -3
  21. package/dist/lib/execution-sessions.d.ts +17 -0
  22. package/dist/lib/execution-sessions.js +85 -0
  23. package/dist/lib/file-state.d.ts +2 -0
  24. package/dist/lib/file-state.js +114 -46
  25. package/dist/lib/inspector-actions.d.ts +2 -2
  26. package/dist/lib/inspector-actions.js +2 -2
  27. package/dist/lib/inspector-command.js +3 -3
  28. package/dist/lib/inspector-overlay.d.ts +54 -70
  29. package/dist/lib/inspector-overlay.js +576 -910
  30. package/dist/lib/inspector.d.ts +3 -71
  31. package/dist/lib/inspector.js +19 -665
  32. package/dist/lib/limits.d.ts +7 -3
  33. package/dist/lib/limits.js +7 -3
  34. package/dist/lib/observability.d.ts +16 -16
  35. package/dist/lib/observability.js +43 -82
  36. package/dist/lib/prompts.d.ts +1 -1
  37. package/dist/lib/prompts.js +3 -3
  38. package/dist/lib/recipe-control.d.ts +7 -0
  39. package/dist/lib/recipe-control.js +43 -0
  40. package/dist/lib/recipes-discovery.js +2 -0
  41. package/dist/lib/recipes-references.d.ts +1 -14
  42. package/dist/lib/recipes-references.js +6 -21
  43. package/dist/lib/review-control.d.ts +1 -1
  44. package/dist/lib/review-control.js +4 -5
  45. package/dist/lib/review-projection.js +1 -5
  46. package/dist/lib/run-ui-runtime.js +2 -2
  47. package/dist/lib/runs-control-delivery.d.ts +28 -0
  48. package/dist/lib/runs-control-delivery.js +150 -0
  49. package/dist/lib/runs-controls.d.ts +37 -0
  50. package/dist/lib/runs-controls.js +146 -0
  51. package/dist/lib/runs-retention.d.ts +7 -0
  52. package/dist/lib/runs-retention.js +27 -3
  53. package/dist/lib/runs-start.js +4 -2
  54. package/dist/lib/runs-status.js +11 -6
  55. package/dist/lib/runs-trace.d.ts +24 -0
  56. package/dist/lib/runs-trace.js +102 -0
  57. package/dist/lib/runtime-identity.d.ts +7 -0
  58. package/dist/lib/runtime-identity.js +35 -0
  59. package/dist/lib/runtime-notifier.d.ts +1 -1
  60. package/dist/lib/runtime-notifier.js +1 -1
  61. package/dist/lib/runtime-triage.d.ts +29 -0
  62. package/dist/lib/runtime-triage.js +76 -0
  63. package/dist/lib/tool-review-scheduler.js +7 -7
  64. package/dist/lib/tools-inspect.d.ts +3 -3
  65. package/dist/lib/tools-inspect.js +241 -707
  66. package/dist/lib/tools-local.js +2 -10
  67. package/dist/lib/tools-message.d.ts +6 -7
  68. package/dist/lib/tools-message.js +95 -396
  69. package/dist/lib/tools-response.d.ts +1 -5
  70. package/dist/lib/tools-response.js +5 -48
  71. package/dist/lib/tools-spawn.js +16 -28
  72. package/dist/lib/tools.d.ts +1 -1
  73. package/dist/lib/tools.js +2 -3
  74. package/dist/lib/trace-projection.d.ts +22 -0
  75. package/dist/lib/trace-projection.js +185 -0
  76. package/dist/recipes/draft-review.json +0 -10
  77. package/dist/recipes/lens-swarm.json +0 -14
  78. package/dist/recipes/music-player.json +10 -19
  79. package/dist/recipes/pipeline-architect-coordinator.json +0 -11
  80. package/dist/recipes/pipeline-artifact-bundle.json +1 -22
  81. package/dist/recipes/pipeline-artifact-report.json +1 -18
  82. package/dist/recipes/pipeline-artifact-write.json +1 -18
  83. package/dist/recipes/pipeline-async-run-ops.json +0 -12
  84. package/dist/recipes/pipeline-checkpoint-continuation.json +0 -14
  85. package/dist/recipes/pipeline-development-tasking.json +0 -12
  86. package/dist/recipes/pipeline-docs-maintenance.json +0 -12
  87. package/dist/recipes/pipeline-media-library.json +0 -12
  88. package/dist/recipes/pipeline-quorum-review.json +0 -12
  89. package/dist/recipes/pipeline-release-readiness.json +0 -12
  90. package/dist/recipes/pipeline-release-summary.json +0 -12
  91. package/dist/recipes/pipeline-repo-health.json +0 -12
  92. package/dist/recipes/pipeline-research-synthesis.json +0 -11
  93. package/dist/recipes/pipeline-review-readiness.json +0 -12
  94. package/dist/recipes/resource-locker.json +27 -0
  95. package/dist/recipes/subagent-artifact.json +0 -9
  96. package/dist/recipes/subagent-checkpoint.json +0 -10
  97. package/dist/recipes/subagent-conflict-report.json +0 -11
  98. package/dist/recipes/subagent-contradiction-map.json +0 -11
  99. package/dist/recipes/subagent-critic.json +0 -11
  100. package/dist/recipes/subagent-evidence-map.json +0 -11
  101. package/dist/recipes/subagent-followup.json +0 -10
  102. package/dist/recipes/subagent-judge.json +0 -11
  103. package/dist/recipes/subagent-merge.json +0 -11
  104. package/dist/recipes/subagent-normalize.json +0 -11
  105. package/dist/recipes/subagent-plan.json +0 -11
  106. package/dist/recipes/subagent-preflight.json +0 -11
  107. package/dist/recipes/subagent-prompt.json +0 -10
  108. package/dist/recipes/subagent-quorum.json +0 -10
  109. package/dist/recipes/subagent-review-coordinator.json +0 -14
  110. package/dist/recipes/subagent-review.json +0 -11
  111. package/dist/recipes/subagent-task-card.json +0 -11
  112. package/dist/recipes/subagent-tools.json +0 -10
  113. package/dist/recipes/subagent-verify.json +0 -11
  114. package/dist/recipes/subagents-prompts.json +0 -10
  115. package/dist/recipes/tool-review.json +0 -10
  116. package/dist/scripts/async-runner.mjs +25 -25
  117. package/dist/scripts/conformance.mjs +4 -2
  118. package/dist/scripts/locker.mjs +196 -69
  119. package/dist/scripts/music-player.mjs +162 -159
  120. package/dist/scripts/recipe-utils.mjs +6 -96
  121. package/dist/scripts/release-gates.mjs +91 -0
  122. package/dist/scripts/validate-recipe.mjs +8 -57
  123. package/dist/skills/actors/SKILL.md +57 -265
  124. package/dist/skills/swarm/SKILL.md +10 -34
  125. package/docs/0.43-baseline.md +39 -0
  126. package/docs/README.md +4 -6
  127. package/docs/actor-inspector.md +26 -64
  128. package/docs/async-runs.md +81 -328
  129. package/docs/command-templates.md +8 -118
  130. package/docs/recipe-library.md +55 -182
  131. package/docs/releasing.md +28 -0
  132. package/docs/template-recipes.md +76 -289
  133. package/docs/tool-registry.md +41 -161
  134. package/fixtures/protocol/control-endpoint.json +6 -0
  135. package/fixtures/protocol/control-record.json +9 -0
  136. package/fixtures/protocol/recipe-summary.json +4 -12
  137. package/fixtures/protocol/trace-event.json +9 -0
  138. package/index.ts +1 -1
  139. package/lib/async-runs.ts +218 -204
  140. package/lib/automatic-review-runtime.ts +7 -7
  141. package/lib/command-templates.ts +44 -4
  142. package/lib/control-projection.ts +105 -0
  143. package/lib/control.ts +117 -0
  144. package/lib/draft-sleep.ts +3 -3
  145. package/lib/execution-sessions.ts +111 -0
  146. package/lib/file-state.ts +84 -64
  147. package/lib/inspector-actions.ts +2 -2
  148. package/lib/inspector-command.ts +3 -3
  149. package/lib/inspector-overlay.ts +617 -1126
  150. package/lib/inspector.ts +46 -979
  151. package/lib/limits.ts +7 -3
  152. package/lib/observability.ts +60 -101
  153. package/lib/prompts.ts +3 -3
  154. package/lib/recipe-control.ts +52 -0
  155. package/lib/recipes-discovery.ts +2 -0
  156. package/lib/recipes-references.ts +9 -45
  157. package/lib/review-control.ts +4 -5
  158. package/lib/review-projection.ts +1 -5
  159. package/lib/run-ui-runtime.ts +2 -2
  160. package/lib/runs-control-delivery.ts +209 -0
  161. package/lib/runs-controls.ts +213 -0
  162. package/lib/runs-retention.ts +38 -3
  163. package/lib/runs-start.ts +4 -2
  164. package/lib/runs-status.ts +11 -6
  165. package/lib/runs-trace.ts +136 -0
  166. package/lib/runtime-identity.ts +39 -0
  167. package/lib/runtime-notifier.ts +1 -1
  168. package/lib/runtime-triage.ts +120 -0
  169. package/lib/tool-review-scheduler.ts +7 -7
  170. package/lib/tools-inspect.ts +283 -900
  171. package/lib/tools-local.ts +2 -12
  172. package/lib/tools-message.ts +112 -520
  173. package/lib/tools-response.ts +5 -64
  174. package/lib/tools-spawn.ts +16 -32
  175. package/lib/tools.ts +5 -6
  176. package/lib/trace-projection.ts +244 -0
  177. package/package.json +2 -1
  178. package/recipes/draft-review.json +0 -10
  179. package/recipes/lens-swarm.json +0 -14
  180. package/recipes/music-player.json +10 -19
  181. package/recipes/pipeline-architect-coordinator.json +0 -11
  182. package/recipes/pipeline-artifact-bundle.json +1 -22
  183. package/recipes/pipeline-artifact-report.json +1 -18
  184. package/recipes/pipeline-artifact-write.json +1 -18
  185. package/recipes/pipeline-async-run-ops.json +0 -12
  186. package/recipes/pipeline-checkpoint-continuation.json +0 -14
  187. package/recipes/pipeline-development-tasking.json +0 -12
  188. package/recipes/pipeline-docs-maintenance.json +0 -12
  189. package/recipes/pipeline-media-library.json +0 -12
  190. package/recipes/pipeline-quorum-review.json +0 -12
  191. package/recipes/pipeline-release-readiness.json +0 -12
  192. package/recipes/pipeline-release-summary.json +0 -12
  193. package/recipes/pipeline-repo-health.json +0 -12
  194. package/recipes/pipeline-research-synthesis.json +0 -11
  195. package/recipes/pipeline-review-readiness.json +0 -12
  196. package/recipes/resource-locker.json +27 -0
  197. package/recipes/subagent-artifact.json +0 -9
  198. package/recipes/subagent-checkpoint.json +0 -10
  199. package/recipes/subagent-conflict-report.json +0 -11
  200. package/recipes/subagent-contradiction-map.json +0 -11
  201. package/recipes/subagent-critic.json +0 -11
  202. package/recipes/subagent-evidence-map.json +0 -11
  203. package/recipes/subagent-followup.json +0 -10
  204. package/recipes/subagent-judge.json +0 -11
  205. package/recipes/subagent-merge.json +0 -11
  206. package/recipes/subagent-normalize.json +0 -11
  207. package/recipes/subagent-plan.json +0 -11
  208. package/recipes/subagent-preflight.json +0 -11
  209. package/recipes/subagent-prompt.json +0 -10
  210. package/recipes/subagent-quorum.json +0 -10
  211. package/recipes/subagent-review-coordinator.json +0 -14
  212. package/recipes/subagent-review.json +0 -11
  213. package/recipes/subagent-task-card.json +0 -11
  214. package/recipes/subagent-tools.json +0 -10
  215. package/recipes/subagent-verify.json +0 -11
  216. package/recipes/subagents-prompts.json +0 -10
  217. package/recipes/tool-review.json +0 -10
  218. package/scripts/async-runner.mjs +25 -25
  219. package/scripts/conformance.mjs +4 -2
  220. package/scripts/locker.mjs +196 -69
  221. package/scripts/music-player.mjs +162 -159
  222. package/scripts/recipe-utils.mjs +6 -96
  223. package/scripts/release-gates.mjs +91 -0
  224. package/scripts/validate-recipe.mjs +8 -57
  225. package/skills/actors/SKILL.md +57 -265
  226. package/skills/swarm/SKILL.md +10 -34
  227. package/dist/fixtures/protocol/actor-message-branch.json +0 -13
  228. package/dist/fixtures/protocol/mailbox-contract.json +0 -15
  229. package/dist/fixtures/protocol/room-message.json +0 -11
  230. package/dist/fixtures/protocol/room-roster.json +0 -11
  231. package/dist/fixtures/protocol/run-inbox-message.json +0 -9
  232. package/dist/fixtures/protocol/run-outbox-event.json +0 -9
  233. package/dist/lib/mailbox-loop.d.ts +0 -41
  234. package/dist/lib/mailbox-loop.js +0 -60
  235. package/dist/lib/messages.d.ts +0 -25
  236. package/dist/lib/messages.js +0 -122
  237. package/dist/lib/rooms.d.ts +0 -104
  238. package/dist/lib/rooms.js +0 -647
  239. package/dist/lib/runs-mailbox.d.ts +0 -25
  240. package/dist/lib/runs-mailbox.js +0 -146
  241. package/dist/lib/runs-messages.d.ts +0 -15
  242. package/dist/lib/runs-messages.js +0 -179
  243. package/dist/lib/runs-outbox.d.ts +0 -41
  244. package/dist/lib/runs-outbox.js +0 -87
  245. package/dist/lib/tools-mailbox.d.ts +0 -8
  246. package/dist/lib/tools-mailbox.js +0 -48
  247. package/dist/recipes/actor-worker.json +0 -39
  248. package/dist/recipes/coordinator-locker.json +0 -45
  249. package/dist/recipes/locker.json +0 -45
  250. package/dist/recipes/pipeline-room-swarm.json +0 -50
  251. package/dist/recipes/subagent-message.json +0 -32
  252. package/dist/recipes/utility-actor-message.json +0 -23
  253. package/dist/scripts/actor-worker.mjs +0 -214
  254. package/dist/scripts/coordinator.mjs +0 -799
  255. package/docs/actor-messages.md +0 -225
  256. package/docs/actors-deep-reference.md +0 -66
  257. package/docs/component-recipes.md +0 -148
  258. package/docs/task-first-recipes.md +0 -263
  259. package/fixtures/protocol/actor-message-branch.json +0 -13
  260. package/fixtures/protocol/mailbox-contract.json +0 -15
  261. package/fixtures/protocol/room-message.json +0 -11
  262. package/fixtures/protocol/room-roster.json +0 -11
  263. package/fixtures/protocol/run-inbox-message.json +0 -9
  264. package/fixtures/protocol/run-outbox-event.json +0 -9
  265. package/lib/mailbox-loop.ts +0 -144
  266. package/lib/messages.ts +0 -151
  267. package/lib/rooms.ts +0 -939
  268. package/lib/runs-mailbox.ts +0 -208
  269. package/lib/runs-messages.ts +0 -252
  270. package/lib/runs-outbox.ts +0 -144
  271. package/lib/tools-mailbox.ts +0 -56
  272. package/recipes/actor-worker.json +0 -39
  273. package/recipes/coordinator-locker.json +0 -45
  274. package/recipes/locker.json +0 -45
  275. package/recipes/pipeline-room-swarm.json +0 -50
  276. package/recipes/subagent-message.json +0 -32
  277. package/recipes/utility-actor-message.json +0 -23
  278. package/scripts/actor-worker.mjs +0 -214
  279. package/scripts/coordinator.mjs +0 -799
@@ -1,161 +1,134 @@
1
1
  /**
2
- * Keyboard-driven actor inspector overlay.
3
- * Zones: overlay shell, tabs, owned-run/subagent selection, compact striped rows
4
- * Owns interactive TUI navigation; evidence parsing remains in inspector/session domains.
2
+ * Keyboard-driven Actor Inspector overlay for concrete Run instances.
3
+ * Zones: owned actor selection, Recipe/Trace/Control tabs, safe Run Kill confirmation
4
+ * Owns manual kernel navigation; evidence parsing and lifecycle mutation stay in ports.
5
5
  */
6
+ import { readFileSync } from "node:fs";
6
7
  import * as path from "node:path";
7
- import { stripVTControlCharacters } from "node:util";
8
8
  import { matchesKey, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
9
- import * as Inspector from "./inspector.js";
10
- const INSPECTOR_TABS = [
11
- "recipe",
12
- "communications",
13
- "turns",
9
+ import * as ActorInspector from "./inspector.js";
10
+ import * as ControlProjection from "./control-projection.js";
11
+ import * as RunsControls from "./runs-controls.js";
12
+ import * as RunControlDelivery from "./runs-control-delivery.js";
13
+ import * as TraceProjection from "./trace-projection.js";
14
+ const TABS = ["recipe", "trace", "control"];
15
+ const TRACE_SOURCES = [
16
+ "all",
17
+ "lifecycle",
18
+ "control",
19
+ "process",
20
+ "agent",
21
+ "artifact",
22
+ "runtime",
14
23
  ];
15
- const DETAIL_SECTION_BREAK = { sectionBreak: true };
24
+ function newestTraceItems(items) {
25
+ return [...items].sort((left, right) => right.ts.localeCompare(left.ts) || right.id.localeCompare(left.id));
26
+ }
16
27
  export class ActorInspectorOverlay {
17
28
  done;
18
29
  killRun;
19
30
  ownerId;
20
- readTurns;
31
+ readRuns;
32
+ readTrace;
21
33
  stateRoot;
22
34
  theme;
23
35
  tui;
24
36
  refreshTimer;
25
- runIndex = 0;
26
- communicationChannel = "all";
27
- communicationFrom = "all";
28
- communicationUnread = false;
29
37
  contentStripeIndices = [];
30
- detailCommunication;
31
- detailOpen = false;
38
+ documentScroll = 0;
32
39
  detailScroll = 0;
33
- detailTurn;
34
40
  feedback;
41
+ focus = "runs";
35
42
  killConfirmation;
36
43
  killDialogChoice = "cancel";
37
- readKeys = new Set();
38
- focus = "tabs";
39
- filterControlIndex = 0;
40
- menuLevel = "filter";
41
- selectorIndex = 0;
42
- recipeScroll = 0;
43
44
  rowIndex = 0;
44
- selectedRun;
45
- subagentIndex = 0;
46
- tab = "recipe";
47
- turnCache;
45
+ runCache;
46
+ selectedTraceId;
47
+ runIndex = 0;
48
+ selectorIndex = 0;
49
+ selectorMode;
50
+ tabIndex = 0;
51
+ traceCache;
52
+ traceDetail;
53
+ traceSourceIndex = 0;
48
54
  constructor(options) {
49
55
  this.done = options.done;
50
56
  this.killRun = options.killRun;
51
57
  this.ownerId = options.ownerId;
52
- this.readTurns = options.readTurns ?? Inspector.readActorInspectorTurns;
58
+ this.readRuns = options.readRuns ?? ActorInspector.readActorInspectorRuns;
59
+ this.readTrace = options.readTrace ?? TraceProjection.projectRunTrace;
53
60
  this.stateRoot = options.stateRoot;
54
61
  this.theme = options.theme;
55
62
  this.tui = options.tui;
56
63
  this.refreshTimer = setInterval(() => {
57
- if (this.focus !== "list" && this.focus !== "detail")
58
- this.turnCache = undefined;
64
+ this.invalidate();
59
65
  this.tui.requestRender();
60
- }, 1000);
66
+ }, 1_000);
61
67
  this.refreshTimer.unref?.();
62
68
  }
69
+ dispose() {
70
+ clearInterval(this.refreshTimer);
71
+ }
72
+ invalidate() {
73
+ this.runCache = undefined;
74
+ this.traceCache = undefined;
75
+ }
63
76
  handleInput(data) {
64
77
  const runs = this.runs();
65
- this.ensureSelectedRun(runs);
78
+ this.runIndex = Math.min(this.runIndex, Math.max(0, runs.length - 1));
66
79
  if (matchesKey(data, "ctrl+c")) {
67
80
  this.done();
68
81
  return;
69
82
  }
70
83
  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
- }
84
+ this.handleKillInput(data);
90
85
  this.tui.requestRender();
91
86
  return;
92
87
  }
93
88
  if (data.toLowerCase() === "k") {
94
- this.requestKill(runs);
89
+ this.requestKill(runs[this.runIndex]);
95
90
  this.tui.requestRender();
96
91
  return;
97
92
  }
98
93
  if (matchesKey(data, "escape")) {
99
94
  if (this.focus === "select") {
100
- if (this.menuLevel === "value") {
101
- this.menuLevel = "filter";
102
- this.selectorIndex = this.filterControlIndex;
103
- }
104
- else
105
- this.focus = this.menuLevel === "run" ? "runs" : "tabs";
95
+ this.selectorMode = undefined;
96
+ this.focus = this.tab === "trace" ? "tabs" : "runs";
97
+ }
98
+ else if (this.focus === "detail") {
99
+ this.selectedTraceId = this.traceDetail?.id;
100
+ this.traceDetail = undefined;
101
+ this.detailScroll = 0;
102
+ this.focus = "list";
106
103
  }
107
- else if (this.focus === "detail")
108
- this.backDetail();
109
- else if (this.focus === "recipe")
104
+ else if (this.focus === "document" || this.focus === "list") {
105
+ if (this.focus === "list")
106
+ this.selectedTraceId = undefined;
110
107
  this.focus = "tabs";
111
- else
108
+ }
109
+ else {
112
110
  this.done();
111
+ }
113
112
  this.tui.requestRender();
114
113
  return;
115
114
  }
116
115
  if (this.focus === "select") {
117
- const options = this.menuOptions();
118
- if (matchesKey(data, "up"))
119
- this.selectorIndex = Math.max(0, this.selectorIndex - 1);
120
- else if (matchesKey(data, "down"))
121
- this.selectorIndex = Math.min(options.length - 1, this.selectorIndex + 1);
122
- else if (matchesKey(data, "left"))
123
- this.backMenu();
124
- else if (matchesKey(data, "return") ||
125
- (matchesKey(data, "right") && this.menuLevel !== "value"))
126
- this.applyMenuOption(options[this.selectorIndex]);
116
+ this.handleSelectorInput(data, runs);
127
117
  this.tui.requestRender();
128
118
  return;
129
119
  }
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";
120
+ if (this.focus === "detail") {
121
+ this.handleScrollableInput(data, "detail");
145
122
  this.tui.requestRender();
146
123
  return;
147
124
  }
148
- if (this.focus === "detail") {
149
- if (matchesKey(data, "up"))
150
- this.detailScroll = Math.max(0, this.detailScroll - 1);
151
- else if (matchesKey(data, "down"))
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();
157
- else if (matchesKey(data, "left"))
158
- this.backDetail();
125
+ if (this.focus === "document") {
126
+ if (matchesKey(data, "left"))
127
+ this.focus = "tabs";
128
+ else if (matchesKey(data, "up") && this.documentScroll === 0)
129
+ this.focus = "tabs";
130
+ else
131
+ this.handleScrollableInput(data, "document");
159
132
  this.tui.requestRender();
160
133
  return;
161
134
  }
@@ -166,163 +139,197 @@ export class ActorInspectorOverlay {
166
139
  this.cycleRun(runs, 1);
167
140
  else if (matchesKey(data, "down"))
168
141
  this.focus = "tabs";
169
- else if (matchesKey(data, "return") && runs.length > 0)
170
- this.openRunMenu();
142
+ else if (matchesKey(data, "return") && runs.length > 0) {
143
+ this.selectorMode = "run";
144
+ this.selectorIndex = this.runIndex;
145
+ this.focus = "select";
146
+ }
171
147
  }
172
148
  else if (this.focus === "tabs") {
173
- if (matchesKey(data, "left") || matchesKey(data, "right")) {
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];
177
- this.filterControlIndex = 0;
178
- this.recipeScroll = 0;
179
- this.rowIndex = 0;
149
+ if (matchesKey(data, "left") || matchesKey(data, "right") || matchesKey(data, "tab")) {
150
+ const direction = matchesKey(data, "left") ? -1 : 1;
151
+ this.tabIndex = (this.tabIndex + direction + TABS.length) % TABS.length;
152
+ this.resetContentPosition();
180
153
  }
181
- else if (matchesKey(data, "up"))
154
+ else if (matchesKey(data, "up")) {
182
155
  this.focus = "runs";
183
- else if ((matchesKey(data, "down") || matchesKey(data, "return")) &&
184
- this.tab === "recipe")
185
- this.focus = "recipe";
186
- else if (matchesKey(data, "down") && this.listItemCount() > 0)
187
- this.focus = "list";
188
- else if (matchesKey(data, "return"))
189
- this.openFilterMenu();
156
+ }
157
+ else if (matchesKey(data, "down")) {
158
+ const items = this.tab === "trace" ? this.traceItems(runs[this.runIndex]) : [];
159
+ this.focus = items.length > 0 ? "list" : "document";
160
+ if (this.focus === "list") {
161
+ this.rowIndex = 0;
162
+ this.selectedTraceId = items[0]?.id;
163
+ }
164
+ }
165
+ else if (matchesKey(data, "return")) {
166
+ if (this.tab === "trace")
167
+ this.openSourceSelector(runs[this.runIndex]);
168
+ else
169
+ this.focus = "document";
170
+ }
190
171
  }
191
172
  else if (this.focus === "list") {
192
- const count = this.tab === "communications" ? this.communicationPreviews().length : this.turnItems().length;
193
- if (matchesKey(data, "left"))
173
+ const items = this.traceItems(runs[this.runIndex]);
174
+ const count = items.length;
175
+ if (matchesKey(data, "left")) {
176
+ this.selectedTraceId = undefined;
194
177
  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());
178
+ }
199
179
  else if (matchesKey(data, "up")) {
200
- if (this.rowIndex === 0)
180
+ if (this.rowIndex === 0) {
181
+ this.selectedTraceId = undefined;
201
182
  this.focus = "tabs";
183
+ }
202
184
  else
203
185
  this.rowIndex -= 1;
204
186
  }
205
- else if (matchesKey(data, "down"))
187
+ else if (matchesKey(data, "down")) {
206
188
  this.rowIndex = Math.min(Math.max(0, count - 1), this.rowIndex + 1);
207
- else if (matchesKey(data, "return") || matchesKey(data, "right"))
208
- this.openDetail();
189
+ }
190
+ else if (matchesKey(data, "pageUp")) {
191
+ this.rowIndex = Math.max(0, this.rowIndex - this.contentViewportRows());
192
+ }
193
+ else if (matchesKey(data, "pageDown")) {
194
+ this.rowIndex = Math.min(Math.max(0, count - 1), this.rowIndex + this.contentViewportRows());
195
+ }
196
+ else if (matchesKey(data, "return") || matchesKey(data, "right")) {
197
+ const item = items[this.rowIndex];
198
+ if (item) {
199
+ this.traceDetail = item;
200
+ this.detailScroll = 0;
201
+ this.focus = "detail";
202
+ }
203
+ }
204
+ if (this.focus === "list")
205
+ this.selectedTraceId = items[this.rowIndex]?.id;
206
+ }
207
+ if (data.toLowerCase() === "f" && this.tab === "trace") {
208
+ this.cycleTraceSource(runs[this.runIndex]);
209
209
  }
210
210
  this.tui.requestRender();
211
211
  }
212
212
  render(width) {
213
- this.ensureSelectedRun(this.runs());
214
213
  const safeWidth = Math.max(24, width);
215
- const innerWidth = Math.max(1, safeWidth - 2);
214
+ const innerWidth = safeWidth - 2;
215
+ const runs = this.runs();
216
+ this.runIndex = Math.min(this.runIndex, Math.max(0, runs.length - 1));
217
+ const run = runs[this.runIndex];
216
218
  if (this.killConfirmation)
217
- return this.renderKillDialog(safeWidth, innerWidth);
218
- const lines = [];
219
- lines.push(this.border("╭", " Actor Inspector ", "╮", innerWidth));
220
- lines.push(this.row(this.renderRunControl(), innerWidth));
221
- let selector = this.focus === "select"
222
- ? this.menuLevel === "run"
223
- ? this.renderMenuBox(this.runs().map((run, index, runs) => `#${runs.length - index} ${run.run} ${run.status}`), this.selectorIndex, undefined, undefined, false, false, this.contentViewportRows())
224
- : this.renderFilterMenus(innerWidth)
225
- : [];
226
- const selectorAnchor = this.selectorAnchor();
227
- if (this.menuLevel === "run" && selector.length > 0) {
228
- const popup = selector[0];
229
- const popupWidth = visibleWidth(popup);
230
- const leading = this.fit(this.takeVisiblePrefix(this.renderTabs(), selectorAnchor), selectorAnchor);
231
- const trailingWidth = Math.max(0, innerWidth - selectorAnchor - popupWidth);
232
- const trailing = this.fit(this.dropVisiblePrefix(this.renderTabs(), selectorAnchor + popupWidth), trailingWidth);
233
- lines.push(this.row(`${leading}${popup}${trailing}`, innerWidth, true));
234
- selector = selector.slice(1);
235
- }
236
- else
237
- lines.push(this.row(this.renderTabs(), innerWidth));
238
- const selectorTop = selector[0];
239
- if (selectorTop) {
240
- const leadingBorder = "─".repeat(selectorAnchor);
241
- const remainingBorder = "─".repeat(Math.max(0, innerWidth - visibleWidth(selectorTop) - selectorAnchor));
242
- lines.push(`${this.theme.fg("borderAccent", `├${leadingBorder}`)}${selectorTop}${this.theme.fg("borderAccent", `${remainingBorder}┤`)}`);
243
- }
244
- else
245
- lines.push(this.border("├", "", "┤", innerWidth));
219
+ return this.renderKillDialog(innerWidth);
220
+ const lines = [this.border("╭", " Actor Inspector ", "╮", innerWidth)];
221
+ lines.push(this.row(this.renderRunControl(run, runs.length - this.runIndex), innerWidth));
222
+ lines.push(this.row(this.renderTabs(run), innerWidth));
223
+ lines.push(this.border("├", "", "┤", innerWidth));
246
224
  this.contentStripeIndices = [];
247
- const baseContent = this.detailOpen
248
- ? this.renderDetail(innerWidth)
249
- : this.selectedRun
250
- ? this.renderTimeline(innerWidth)
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;
262
- const contentViewportRows = this.contentViewportRows();
263
- const selectorRows = selector.slice(1);
264
- for (let index = 0; index < Math.max(content.length, contentViewportRows); index += 1) {
265
- const base = content[index] ?? "";
266
- const stripeIndex = index < content.length
267
- ? this.contentStripeIndices[index] ?? index
268
- : 0;
269
- const popup = selectorRows[index];
270
- if (!popup) {
271
- lines.push(this.stripedRow(base, innerWidth, stripeIndex));
272
- continue;
273
- }
274
- const transparentPrefix = popup.match(/^ +/)?.[0].length ?? 0;
275
- const visiblePopup = popup.slice(transparentPrefix);
276
- const popupWidth = visibleWidth(visiblePopup);
277
- const popupAnchor = selectorAnchor + transparentPrefix;
278
- const baseWidth = Math.max(0, innerWidth - popupWidth - popupAnchor);
279
- const leadingBase = this.stripeBackground(this.fit(this.takeVisiblePrefix(base, popupAnchor), popupAnchor), stripeIndex);
280
- const preservedBase = this.stripeBackground(this.fit(this.dropVisiblePrefix(base, popupWidth + popupAnchor), baseWidth), stripeIndex);
281
- lines.push(this.row(`${leadingBase}${visiblePopup}${preservedBase}`, innerWidth, true));
225
+ let content = this.renderContent(run, innerWidth);
226
+ if (this.feedback) {
227
+ const color = this.feedback.ok
228
+ ? "success"
229
+ : /^Kill (failed|rejected)/u.test(this.feedback.message)
230
+ ? "error"
231
+ : "warning";
232
+ content = [this.theme.fg(color, ` ${this.feedback.message}`), ...content];
233
+ this.contentStripeIndices = [0, ...this.contentStripeIndices];
234
+ }
235
+ const viewportRows = this.contentViewportRows();
236
+ content = content.slice(0, viewportRows);
237
+ for (let index = 0; index < viewportRows; index += 1) {
238
+ lines.push(this.stripedRow(content[index] ?? "", innerWidth, this.contentStripeIndices[index] ?? index));
282
239
  }
283
240
  lines.push(this.footerBorder(this.renderKeyHints(), innerWidth));
284
241
  return lines;
285
242
  }
286
- invalidate() { }
287
- dispose() {
288
- clearInterval(this.refreshTimer);
243
+ get tab() {
244
+ return TABS[this.tabIndex];
289
245
  }
290
246
  runs() {
291
- return Inspector.readActorInspectorRuns(this.stateRoot, this.ownerId);
292
- }
293
- ensureSelectedRun(runs) {
294
- if (!this.selectedRun)
295
- this.selectedRun = runs[0]?.run;
247
+ if (this.runCache)
248
+ return this.runCache;
249
+ this.runCache = this.readRuns(this.stateRoot, this.ownerId);
250
+ return this.runCache;
296
251
  }
297
252
  contentViewportRows() {
298
- const overlayRows = Math.floor(this.tui.terminal.rows * 0.94);
253
+ const rows = this.tui.terminal?.rows ?? 30;
254
+ const overlayRows = Math.floor(rows * 0.94);
299
255
  return Math.max(4, Math.min(24, overlayRows - 5));
300
256
  }
301
- selectRun(run, index) {
302
- this.selectedRun = run;
303
- this.turnCache = undefined;
304
- this.runIndex = index;
305
- this.recipeScroll = 0;
257
+ resetContentPosition() {
258
+ this.documentScroll = 0;
259
+ this.detailScroll = 0;
306
260
  this.rowIndex = 0;
307
- this.subagentIndex = 0;
308
- this.communicationFrom = "all";
261
+ this.selectedTraceId = undefined;
262
+ this.traceDetail = undefined;
309
263
  }
310
264
  cycleRun(runs, direction) {
311
265
  if (runs.length === 0)
312
266
  return;
313
- const currentIndex = Math.max(0, runs.findIndex((run) => run.run === this.selectedRun));
314
- const nextIndex = (currentIndex + direction + runs.length) % runs.length;
315
- const next = runs[nextIndex];
316
- if (next)
317
- this.selectRun(next.run, nextIndex);
267
+ this.runIndex = (this.runIndex + direction + runs.length) % runs.length;
268
+ this.resetContentPosition();
269
+ }
270
+ handleScrollableInput(data, target) {
271
+ const key = target === "detail" ? "detailScroll" : "documentScroll";
272
+ if (matchesKey(data, "up"))
273
+ this[key] = Math.max(0, this[key] - 1);
274
+ else if (matchesKey(data, "down"))
275
+ this[key] += 1;
276
+ else if (matchesKey(data, "pageUp")) {
277
+ this[key] = Math.max(0, this[key] - this.contentViewportRows());
278
+ }
279
+ else if (matchesKey(data, "pageDown")) {
280
+ this[key] += this.contentViewportRows();
281
+ }
282
+ else if (matchesKey(data, "left") && target === "detail") {
283
+ this.selectedTraceId = this.traceDetail?.id;
284
+ this.traceDetail = undefined;
285
+ this.detailScroll = 0;
286
+ this.focus = "list";
287
+ }
288
+ }
289
+ handleSelectorInput(data, runs) {
290
+ const run = runs[this.runIndex];
291
+ const options = this.selectorMode === "run"
292
+ ? runs.map((item) => item.run)
293
+ : this.traceSources(run);
294
+ if (matchesKey(data, "up"))
295
+ this.selectorIndex = Math.max(0, this.selectorIndex - 1);
296
+ else if (matchesKey(data, "down")) {
297
+ this.selectorIndex = Math.min(Math.max(0, options.length - 1), this.selectorIndex + 1);
298
+ }
299
+ else if (matchesKey(data, "left")) {
300
+ this.focus = this.selectorMode === "run" ? "runs" : "tabs";
301
+ this.selectorMode = undefined;
302
+ }
303
+ else if (matchesKey(data, "return") || matchesKey(data, "right")) {
304
+ if (this.selectorMode === "run") {
305
+ this.runIndex = this.selectorIndex;
306
+ this.focus = "runs";
307
+ }
308
+ else {
309
+ this.traceSourceIndex = this.selectorIndex;
310
+ this.focus = "tabs";
311
+ }
312
+ this.selectorMode = undefined;
313
+ this.resetContentPosition();
314
+ }
318
315
  }
319
- requestKill(runs) {
316
+ openSourceSelector(run) {
317
+ const sources = this.traceSources(run);
318
+ this.selectorMode = "source";
319
+ this.selectorIndex = Math.min(this.traceSourceIndex, sources.length - 1);
320
+ this.focus = "select";
321
+ }
322
+ cycleTraceSource(run) {
323
+ const sources = this.traceSources(run);
324
+ this.traceSourceIndex = (this.traceSourceIndex + 1) % sources.length;
325
+ this.resetContentPosition();
326
+ }
327
+ requestKill(run) {
320
328
  this.feedback = undefined;
321
329
  if (this.focus !== "runs") {
322
330
  this.feedback = { ok: false, message: "Focus Run before using Kill." };
323
331
  return;
324
332
  }
325
- const run = runs.find((item) => item.run === this.selectedRun);
326
333
  if (!run) {
327
334
  this.feedback = { ok: false, message: "Kill unavailable: no owned run." };
328
335
  return;
@@ -332,17 +339,11 @@ export class ActorInspectorOverlay {
332
339
  return;
333
340
  }
334
341
  if (run.status !== "running") {
335
- this.feedback = {
336
- ok: false,
337
- message: `Kill unavailable: run is ${run.status}.`,
338
- };
342
+ this.feedback = { ok: false, message: `Kill unavailable: run is ${run.status}.` };
339
343
  return;
340
344
  }
341
345
  if (!run.runInstanceId) {
342
- this.feedback = {
343
- ok: false,
344
- message: "Kill unavailable: run generation unavailable.",
345
- };
346
+ this.feedback = { ok: false, message: "Kill unavailable: run generation unavailable." };
346
347
  return;
347
348
  }
348
349
  this.killConfirmation = {
@@ -352,21 +353,39 @@ export class ActorInspectorOverlay {
352
353
  };
353
354
  this.killDialogChoice = "cancel";
354
355
  }
355
- cancelKill() {
356
- this.feedback = { ok: false, message: "Kill cancelled." };
357
- this.killConfirmation = undefined;
358
- this.killDialogChoice = "cancel";
356
+ handleKillInput(data) {
357
+ const key = data.toLowerCase();
358
+ if (matchesKey(data, "escape") || key === "n") {
359
+ this.feedback = { ok: false, message: "Kill cancelled." };
360
+ this.killConfirmation = undefined;
361
+ this.killDialogChoice = "cancel";
362
+ }
363
+ else if (matchesKey(data, "left") || matchesKey(data, "right") || matchesKey(data, "tab")) {
364
+ this.killDialogChoice = this.killDialogChoice === "cancel" ? "kill" : "cancel";
365
+ }
366
+ else if (key === "y") {
367
+ this.confirmKill();
368
+ }
369
+ else if (matchesKey(data, "return")) {
370
+ if (this.killDialogChoice === "kill")
371
+ this.confirmKill();
372
+ else {
373
+ this.feedback = { ok: false, message: "Kill cancelled." };
374
+ this.killConfirmation = undefined;
375
+ }
376
+ }
359
377
  }
360
378
  confirmKill() {
361
379
  const confirmation = this.killConfirmation;
362
380
  this.killConfirmation = undefined;
363
381
  this.killDialogChoice = "cancel";
364
- if (!confirmation || !this.killRun) {
365
- this.feedback = { ok: false, message: "Kill unavailable in this Inspector." };
382
+ if (!confirmation || !this.killRun)
366
383
  return;
367
- }
368
384
  try {
369
- this.feedback = this.killRun(confirmation.run, confirmation.runInstanceId);
385
+ const result = this.killRun(confirmation.run, confirmation.runInstanceId);
386
+ this.feedback = result.ok ? undefined : result;
387
+ if (result.ok)
388
+ this.invalidate();
370
389
  }
371
390
  catch (error) {
372
391
  const message = error instanceof Error ? error.message : String(error);
@@ -376,14 +395,13 @@ export class ActorInspectorOverlay {
376
395
  };
377
396
  }
378
397
  }
379
- renderKillDialog(_width, innerWidth) {
398
+ renderKillDialog(innerWidth) {
380
399
  const confirmation = this.killConfirmation;
381
- const totalRows = this.contentViewportRows() + 5;
382
400
  const cancel = this.killDialogChoice === "cancel"
383
- ? this.theme.bg("selectedBg", this.theme.fg("accent", " Cancel "))
401
+ ? this.theme.bg("customMessageBg", this.theme.fg("accent", " Cancel "))
384
402
  : this.theme.fg("muted", " Cancel ");
385
403
  const kill = this.killDialogChoice === "kill"
386
- ? this.theme.bg("selectedBg", this.theme.fg("error", " Kill actor "))
404
+ ? this.theme.bg("customMessageBg", this.theme.fg("error", " Kill actor "))
387
405
  : this.theme.fg("error", " Kill actor ");
388
406
  const body = [
389
407
  "",
@@ -392,686 +410,371 @@ export class ActorInspectorOverlay {
392
410
  `${this.theme.fg("muted", "Run:")} ${this.theme.fg("accent", `run:${confirmation.run}`)}`,
393
411
  `${this.theme.fg("muted", "Current status:")} ${this.theme.fg("warning", confirmation.status)}`,
394
412
  "",
395
- this.theme.fg("text", "This sends canonical control.kill."),
396
- this.theme.fg("error", "The action is destructive and cannot be undone."),
413
+ this.theme.fg("text", "This sends canonical kill Control."),
397
414
  "",
398
415
  `${cancel} ${kill}`,
399
416
  "",
400
417
  ];
401
418
  const lines = [this.border("╭", " Confirm Actor Kill ", "╮", innerWidth)];
402
- const availableRows = Math.max(1, totalRows - 2);
419
+ const availableRows = this.contentViewportRows() + 3;
403
420
  const topPadding = Math.max(0, Math.floor((availableRows - body.length) / 2));
404
- const dialogRows = [
405
- ...Array.from({ length: topPadding }, () => ""),
406
- ...body,
407
- ].slice(0, availableRows);
408
- while (dialogRows.length < availableRows)
409
- dialogRows.push("");
410
- for (const line of dialogRows)
421
+ const rows = [...Array.from({ length: topPadding }, () => ""), ...body].slice(0, availableRows);
422
+ while (rows.length < availableRows)
423
+ rows.push("");
424
+ for (const line of rows)
411
425
  lines.push(this.row(this.center(line, innerWidth), innerWidth));
412
426
  lines.push(this.footerBorder(this.renderKeyHints(), innerWidth));
413
427
  return lines;
414
428
  }
415
- listItemCount() {
416
- if (this.tab === "recipe")
417
- return 0;
418
- return this.tab === "communications"
419
- ? this.communicationPreviews().length
420
- : this.turnItems().length;
421
- }
422
- selectorAnchor() {
423
- if (this.menuLevel === "run") {
424
- const runControl = stripVTControlCharacters(this.renderRunControl());
425
- return Math.max(0, runControl.indexOf("Run:") - 2);
426
- }
427
- const label = this.tab === "communications"
428
- ? "Messages"
429
- : this.tab === "turns"
430
- ? "Turns"
431
- : "Recipe";
432
- const tabs = stripVTControlCharacters(this.renderTabs());
433
- return Math.max(0, tabs.indexOf(label) - 2);
434
- }
435
- renderKeyHints() {
436
- const hint = (keys, description) => `${this.theme.fg("accent", keys)}${this.theme.fg("borderAccent", ` ${description}`)}`;
437
- const divider = this.theme.fg("borderAccent", " ─ ");
438
- const hints = (...items) => items.map(([keys, description]) => hint(keys, description)).join(divider);
439
- if (this.killConfirmation)
440
- return hints(["←→/tab", "choose"], ["enter/y", "confirm"], ["esc/n", "cancel"]);
441
- if (this.focus === "select")
442
- return this.menuLevel === "value"
443
- ? hints(["↑↓", "option"], ["enter", "apply"], ["←/esc", "back"])
444
- : hints(["↑↓", "option"], ["→/enter", "open"], ["←/esc", "back"]);
445
- if (this.focus === "recipe")
446
- return hints(["↑↓/pgup/pgdn", "scroll"], ["↑ at top", "tabs"], ["←/esc", "tabs"]);
447
- if (this.focus === "detail")
448
- return hints(["↑↓/pgup/pgdn", "scroll"], ["←/esc", "back"]);
449
- if (this.focus === "list")
450
- return hints(["↑↓/pgup/pgdn", "row"], ["→/enter", "open"], ["←", "tabs"], ["esc", "close"]);
451
- if (this.focus === "runs") {
452
- const items = [
453
- ["←→", "run"],
454
- ["↓", "tabs"],
455
- ["enter", "list"],
456
- ];
457
- const run = this.runs().find((item) => item.run === this.selectedRun);
458
- if (this.killRun && run?.status === "running" && run.runInstanceId)
459
- items.push(["k", "kill"]);
460
- items.push(["esc", "close"]);
461
- return hints(...items);
462
- }
463
- return hints(["←→", "navigate"], ["↑↓", "change row"], ["enter", "select"], ["esc", "close"]);
464
- }
465
- renderRunControl() {
466
- const runs = this.runs();
467
- const runIndex = runs.findIndex((item) => item.run === this.selectedRun);
468
- const run = runs[runIndex];
469
- const focused = this.focus === "runs";
470
- const active = focused || (this.focus === "select" && this.menuLevel === "run");
429
+ renderRunControl(run, sequence) {
430
+ const active = this.focus === "runs" || (this.focus === "select" && this.selectorMode === "run");
471
431
  const prefix = active ? this.theme.fg("accent", " ← ") : " ";
472
432
  const suffix = active ? this.theme.fg("accent", " → ") : " ";
473
433
  if (!run) {
474
434
  const empty = this.theme.fg("muted", `${prefix}Run: none${suffix}`);
475
- return focused ? this.theme.bg("selectedBg", empty) : empty;
476
- }
477
- const statusColor = run.status === "failed"
478
- ? "error"
479
- : run.status === "running"
480
- ? "warning"
481
- : run.status === "done"
482
- ? "success"
483
- : "muted";
484
- const sequence = runs.length - runIndex;
485
- 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}`;
486
- return focused ? this.theme.bg("selectedBg", value) : value;
487
- }
488
- activeMessageFilters() {
489
- return [
490
- ...(this.communicationChannel === "all" ? [] : [this.communicationChannel]),
491
- ...(this.communicationUnread ? ["unread"] : []),
492
- ...(this.communicationFrom === "all" ? [] : [this.communicationFrom]),
493
- ];
494
- }
495
- renderTabs() {
496
- const tab = (id, label) => {
497
- const selected = this.tab === id;
498
- const focused = selected && this.focus === "tabs";
499
- const runActive = this.focus === "runs" ||
500
- (this.focus === "select" && this.menuLevel === "run");
501
- const display = selected && !runActive ? `[ ${label} ]` : ` ${label} `;
435
+ return this.focus === "runs" ? this.theme.bg("customMessageBg", empty) : empty;
436
+ }
437
+ const value = `${prefix}${this.theme.fg("muted", "Run: ")}${this.theme.fg("text", `#${sequence}`)} ${this.theme.fg("accent", run.run)} ${this.theme.fg(this.statusColor(run.status), run.status)}${suffix}`;
438
+ return this.focus === "runs" ? this.theme.bg("customMessageBg", value) : value;
439
+ }
440
+ renderTabs(run) {
441
+ const source = this.traceSource(run);
442
+ return TABS.map((tab, index) => {
443
+ const base = `${tab[0].toUpperCase()}${tab.slice(1)}`;
444
+ const label = tab === "trace" && source !== "all" ? `${base} (${source})` : base;
445
+ const selected = index === this.tabIndex;
446
+ const display = selected && this.focus !== "runs" ? `[ ${label} ]` : ` ${label} `;
502
447
  const value = ` ${display} `;
503
448
  if (!selected)
504
449
  return this.theme.fg("muted", value);
505
450
  const colored = this.theme.fg("accent", value);
506
- return focused ? this.theme.bg("selectedBg", colored) : colored;
507
- };
508
- const messageFilters = this.activeMessageFilters();
509
- const subagent = this.subagents()[this.subagentIndex] ?? "all";
510
- const messagesLabel = `Messages${messageFilters.length > 0 ? ` (${messageFilters.join(", ")})` : ""}`;
511
- const turnsLabel = `Turns${subagent === "all" ? "" : ` (${subagent})`}`;
512
- return `${tab("recipe", "Recipe")} ${tab("communications", messagesLabel)} ${tab("turns", turnsLabel)}`;
451
+ return this.focus === "tabs" ? this.theme.bg("customMessageBg", colored) : colored;
452
+ }).join(" ");
513
453
  }
514
- renderRunSelector(width) {
515
- const runs = this.runs();
516
- if (runs.length === 0)
454
+ renderContent(run, width) {
455
+ if (!run)
517
456
  return [this.theme.fg("muted", " No owned actor runs")];
518
- this.runIndex = Math.min(this.runIndex, runs.length - 1);
519
- return runs.slice(0, this.contentViewportRows()).map((run, index) => {
520
- const selected = index === this.runIndex;
521
- const prefix = selected ? this.theme.fg("accent", " ▶ ") : " ";
522
- const label = `${prefix}#${runs.length - index} run:${run.run}`;
523
- const status = ` ${run.status}`;
524
- return selected
525
- ? this.theme.fg("accent", `${label}${status}`)
526
- : `${label}${this.theme.fg("muted", status)}`;
527
- });
457
+ if (this.focus === "select")
458
+ return this.renderSelector(run);
459
+ if (this.focus === "detail")
460
+ return this.renderTraceDetail(width);
461
+ if (this.tab === "trace")
462
+ return this.renderTraceList(run);
463
+ const stateDir = path.join(this.stateRoot, run.run);
464
+ const value = this.tab === "recipe"
465
+ ? ActorInspector.readActorInspectorRecipe(stateDir)
466
+ : this.controlDocument(run, stateDir);
467
+ const projection = this.documentProjection(value, width);
468
+ const maxStart = Math.max(0, projection.lines.length - this.contentViewportRows());
469
+ this.documentScroll = Math.min(this.documentScroll, maxStart);
470
+ this.contentStripeIndices = projection.stripes.slice(this.documentScroll, this.documentScroll + this.contentViewportRows());
471
+ return projection.lines.slice(this.documentScroll, this.documentScroll + this.contentViewportRows());
472
+ }
473
+ renderSelector(run) {
474
+ const options = this.selectorMode === "run"
475
+ ? this.runs().map((item) => `run:${item.run} ${item.status}`)
476
+ : this.traceSources(run).map((source) => `Trace source: ${source}`);
477
+ const lines = this.renderMenuBox(options, this.selectorIndex);
478
+ this.contentStripeIndices = lines.map(() => 0);
479
+ return lines;
528
480
  }
529
- renderTimeline(width) {
530
- if (!this.selectedRun)
531
- return [];
532
- const run = this.runs().find((item) => item.run === this.selectedRun);
533
- if (!run) {
534
- this.selectedRun = undefined;
535
- return [this.theme.fg("warning", " Selected run is no longer owned by this session")];
481
+ renderMenuBox(options, focusedIndex) {
482
+ if (options.length === 0)
483
+ return [this.theme.fg("muted", " No options")];
484
+ const visibleLimit = Math.max(1, Math.min(this.contentViewportRows(), options.length));
485
+ const maxStart = Math.max(0, options.length - visibleLimit);
486
+ const start = Math.max(0, Math.min(focusedIndex - Math.floor(visibleLimit / 2), maxStart));
487
+ const visible = options.slice(start, start + visibleLimit);
488
+ const width = Math.max(8, ...options.map((option) => visibleWidth(option) + 4));
489
+ const border = (left, right, marker = "") => this.theme.fg("borderAccent", `${left}${marker}${"─".repeat(Math.max(0, width - visibleWidth(marker)))}${right}`);
490
+ return [
491
+ border("╭", "╮", start > 0 ? "↑" : ""),
492
+ ...visible.map((option, offset) => {
493
+ const index = start + offset;
494
+ const row = this.fit(`${index === focusedIndex ? " ▶ " : " "}${option}`, width);
495
+ const colored = index === focusedIndex ? this.theme.fg("accent", row) : row;
496
+ const styled = index === focusedIndex ? this.theme.bg("customMessageBg", colored) : colored;
497
+ return `${this.theme.fg("borderAccent", "│")}${styled}${this.theme.fg("borderAccent", "│")}`;
498
+ }),
499
+ border("╰", "╯", start + visible.length < options.length ? "↓" : ""),
500
+ ];
501
+ }
502
+ renderTraceList(run) {
503
+ const items = this.traceItems(run);
504
+ if (items.length === 0)
505
+ return [this.theme.fg("muted", " No Trace evidence")];
506
+ if (this.selectedTraceId) {
507
+ const selectedIndex = items.findIndex((item) => item.id === this.selectedTraceId);
508
+ if (selectedIndex >= 0)
509
+ this.rowIndex = selectedIndex;
510
+ else
511
+ this.selectedTraceId = undefined;
536
512
  }
537
- if (this.tab === "recipe")
538
- return this.renderRecipe(width);
539
- const rows = this.tab === "communications"
540
- ? this.communicationRows()
541
- : this.turnRows();
542
- if (rows.length === 0) {
543
- this.contentStripeIndices = [0];
544
- const filtered = this.tab === "communications"
545
- ? this.activeMessageFilters().length > 0
546
- : (this.subagents()[this.subagentIndex] ?? "all") !== "all";
547
- return [
548
- this.theme.fg("muted", filtered
549
- ? " No results for the active filters. Enter on the tab to adjust."
550
- : this.tab === "communications"
551
- ? " No messages in this run"
552
- : " No persisted turns in this run"),
553
- ];
513
+ this.rowIndex = Math.min(this.rowIndex, items.length - 1);
514
+ if (this.focus === "list" && !this.selectedTraceId) {
515
+ this.selectedTraceId = items[this.rowIndex]?.id;
554
516
  }
555
- this.rowIndex = Math.min(this.rowIndex, rows.length - 1);
556
517
  const viewportRows = this.contentViewportRows();
557
- const start = Math.max(0, Math.min(this.rowIndex - Math.floor(viewportRows / 2), Math.max(0, rows.length - viewportRows)));
558
- const stripeIndices = this.tab === "communications"
559
- ? this.communicationPreviews().map((preview, index) => Math.max(0, (preview.sequence ?? index + 1) - 1))
560
- : this.turnItems().map((turn) => Math.max(0, turn.index - 1));
561
- const visibleRows = rows.slice(start, start + viewportRows);
562
- this.contentStripeIndices = visibleRows.map((_, offset) => stripeIndices[start + offset] ?? start + offset);
563
- return visibleRows.map((row, offset) => {
518
+ const maxStart = Math.max(0, items.length - viewportRows);
519
+ const start = Math.max(0, Math.min(this.rowIndex - Math.floor(viewportRows / 2), maxStart));
520
+ this.contentStripeIndices = items
521
+ .slice(start, start + viewportRows)
522
+ .map((_item, offset) => start + offset);
523
+ return items.slice(start, start + viewportRows).map((item, offset) => {
564
524
  const index = start + offset;
565
- const selected = this.focus === "list" && index === this.rowIndex;
566
- return selected
567
- ? this.theme.fg("accent", ` ▶ ${row}`)
568
- : ` ${row}`;
525
+ const detail = item.detail && typeof item.detail === "object"
526
+ ? item.detail
527
+ : {};
528
+ const marker = item.level === "error"
529
+ ? this.theme.fg("error", "!")
530
+ : detail.attention
531
+ ? this.theme.fg("warning", "A")
532
+ : " ";
533
+ const prefix = this.focus === "list" && index === this.rowIndex
534
+ ? this.theme.fg("accent", " ▶ ")
535
+ : " ";
536
+ const row = `${prefix}${this.theme.fg("text", `#${items.length - index}`)} ${marker} ${this.theme.fg("muted", item.source)}/${this.theme.fg(item.level === "error" ? "error" : "accent", item.kind)} ${this.theme.fg("text", item.summary)}`;
537
+ return this.focus === "list" && index === this.rowIndex
538
+ ? this.theme.fg("accent", row)
539
+ : row;
569
540
  });
570
541
  }
571
- renderRecipe(width) {
572
- if (!this.selectedRun)
573
- return [];
574
- const recipe = Inspector.readActorInspectorRecipe(path.join(this.stateRoot, this.selectedRun));
575
- const logicalLines = [
576
- ...this.detailSection("Identity", this.readableValueLines(recipe.identity, " ")),
577
- ...(recipe.definition
578
- ? this.detailSection("Recipe", this.readableValueLines(recipe.definition, " "))
579
- : []),
580
- ...this.detailSection("Launch", this.readableValueLines(recipe.launch, " ")),
581
- ...(recipe.composition.length > 0
582
- ? this.detailSection("Composition", this.readableValueLines(recipe.composition, " "))
583
- : []),
584
- ...this.detailSection("Diagnostics", recipe.diagnostics),
585
- ];
586
- const wrapped = this.wrapDetailLines(logicalLines.length > 0 ? logicalLines : [" No persisted recipe evidence"], width);
587
- const viewportRows = this.contentViewportRows();
588
- const maxStart = Math.max(0, wrapped.lines.length - viewportRows);
589
- this.recipeScroll = Math.min(this.recipeScroll, maxStart);
590
- this.contentStripeIndices = wrapped.stripeIndices.slice(this.recipeScroll, this.recipeScroll + viewportRows);
591
- return wrapped.lines.slice(this.recipeScroll, this.recipeScroll + viewportRows);
542
+ renderTraceDetail(width) {
543
+ if (!this.traceDetail)
544
+ return [this.theme.fg("warning", " Trace row is no longer available")];
545
+ const projection = this.documentProjection(this.traceDetail, width);
546
+ const maxStart = Math.max(0, projection.lines.length - this.contentViewportRows());
547
+ this.detailScroll = Math.min(this.detailScroll, maxStart);
548
+ this.contentStripeIndices = projection.stripes.slice(this.detailScroll, this.detailScroll + this.contentViewportRows());
549
+ return projection.lines.slice(this.detailScroll, this.detailScroll + this.contentViewportRows());
550
+ }
551
+ controlDocument(run, stateDir) {
552
+ const endpoint = run.runInstanceId
553
+ ? RunControlDelivery.readRunControlEndpoint(stateDir, run.runInstanceId)
554
+ : undefined;
555
+ const meta = this.readRunMeta(stateDir);
556
+ return {
557
+ status: run.status,
558
+ run_instance_id: run.runInstanceId,
559
+ actor_actions: Array.isArray(meta.control) ? meta.control : [],
560
+ runtime_actions: run.status === "running" ? ["kill"] : ["archive", "prune"],
561
+ endpoint,
562
+ recent_controls: RunsControls.readRunControlsFromStateDir(stateDir)
563
+ .slice(-20)
564
+ .reverse()
565
+ .map(ControlProjection.projectRunControl),
566
+ };
592
567
  }
593
- communicationPreviews() {
594
- if (!this.selectedRun)
568
+ allTraceItems(run) {
569
+ if (!run)
595
570
  return [];
596
- const channels = this.communicationChannel === "all"
597
- ? undefined
598
- : [this.communicationChannel];
599
- return Inspector.readActorInspectorPreviews(this.stateRoot, 100, {
600
- channels,
601
- currentRunOnly: false,
602
- ownerId: this.ownerId,
603
- readKeys: this.readKeys,
604
- run: this.selectedRun,
605
- unreadOnly: this.communicationUnread,
606
- })
607
- .filter((preview) => this.communicationFrom === "all" ||
608
- this.communicationActorLabel(preview.from_display ?? preview.from) ===
609
- this.communicationFrom)
610
- .reverse();
611
- }
612
- communicationActorLabel(actor) {
613
- const value = actor ?? "unknown";
614
- if (value.startsWith("run:"))
615
- return value.slice("run:".length);
616
- return value.replace(/^branch:[^/]+\//, "");
617
- }
618
- communicationFromOptions() {
619
- if (!this.selectedRun)
620
- return ["all"];
621
- const actors = Inspector.readActorInspectorRoster(this.stateRoot, this.selectedRun).map((member) => this.communicationActorLabel(member.display ?? member.address));
622
- return ["all", ...new Set(actors)];
571
+ if (this.traceCache?.run === run.run &&
572
+ this.traceCache.runInstanceId === run.runInstanceId) {
573
+ return this.traceCache.items;
574
+ }
575
+ const items = newestTraceItems(this.readTrace(path.join(this.stateRoot, run.run), {
576
+ limit: 100,
577
+ source: "all",
578
+ }));
579
+ this.traceCache = { run: run.run, runInstanceId: run.runInstanceId, items };
580
+ return items;
581
+ }
582
+ traceSources(run) {
583
+ const present = new Set(this.allTraceItems(run).map((item) => item.source));
584
+ return TRACE_SOURCES.filter((source) => source === "all" || present.has(source));
585
+ }
586
+ traceSource(run) {
587
+ const sources = this.traceSources(run);
588
+ this.traceSourceIndex %= sources.length;
589
+ return sources[this.traceSourceIndex] ?? "all";
590
+ }
591
+ traceItems(run) {
592
+ const source = this.traceSource(run);
593
+ const items = this.allTraceItems(run);
594
+ return source === "all" ? items : items.filter((item) => item.source === source);
595
+ }
596
+ readRunMeta(stateDir) {
597
+ try {
598
+ return JSON.parse(readFileSync(path.join(stateDir, "run.json"), "utf8"));
599
+ }
600
+ catch {
601
+ return {};
602
+ }
623
603
  }
624
- communicationRows() {
625
- return this.communicationPreviews().map((preview) => {
626
- const actor = preview.from_display ?? preview.from ?? "unknown";
627
- const target = preview.channel === "room" ? "#all" : preview.to;
628
- const text = preview.summary ?? preview.body_preview ?? "(no body)";
629
- return `#${preview.sequence ?? "?"} ${preview.needs_response ? "! " : ""}${actor} → ${target} ${preview.type} ${text}`;
604
+ documentProjection(value, width) {
605
+ const sections = value && typeof value === "object" && !Array.isArray(value)
606
+ ? Object.entries(value).map(([key, item]) => this.labeledDocumentLines(key, item))
607
+ : [this.document(value)];
608
+ const lines = [];
609
+ const stripes = [];
610
+ sections.forEach((section, sectionIndex) => {
611
+ const wrapped = this.wrapDocument(section, width);
612
+ lines.push(...wrapped.map((line) => this.styleDocumentLine(line)));
613
+ stripes.push(...wrapped.map(() => sectionIndex));
630
614
  });
615
+ return { lines, stripes };
631
616
  }
632
- renderDetail(width) {
633
- if (this.tab === "communications") {
634
- const preview = this.detailCommunication;
635
- if (!preview)
636
- return [this.theme.fg("warning", " Communication row is no longer available")];
637
- const fields = [
638
- ["channel", preview.channel],
639
- ["run", preview.run],
640
- ["from", preview.from_display ?? preview.from],
641
- ["to", preview.to],
642
- ["type", preview.type],
643
- ["summary", preview.summary],
644
- ["body", preview.body_preview],
645
- ["attention", preview.needs_response],
646
- ["inbox", preview.inbox_status],
647
- ["timestamp", preview.timestamp],
648
- ];
649
- const lines = fields
650
- .filter(([, value]) => value !== undefined)
651
- .map(([label, value]) => truncateToWidth(` ${label.padEnd(12, " ")} ${typeof value === "string" ? value : JSON.stringify(value)}`, width, "…"));
652
- const viewportRows = this.contentViewportRows();
653
- const maxStart = Math.max(0, lines.length - viewportRows);
654
- this.detailScroll = Math.min(this.detailScroll, maxStart);
655
- return lines.slice(this.detailScroll, this.detailScroll + viewportRows);
656
- }
657
- const turn = this.detailTurn;
658
- if (!turn)
659
- return [this.theme.fg("warning", " Turn row is no longer available")];
660
- const logicalLines = this.turnEvidenceLines(turn);
661
- const wrapped = this.wrapDetailLines(logicalLines, width);
662
- const viewportRows = this.contentViewportRows();
663
- const maxStart = Math.max(0, wrapped.lines.length - viewportRows);
664
- this.detailScroll = Math.min(this.detailScroll, maxStart);
665
- this.contentStripeIndices = wrapped.stripeIndices.slice(this.detailScroll, this.detailScroll + viewportRows);
666
- return wrapped.lines.slice(this.detailScroll, this.detailScroll + viewportRows);
667
- }
668
- wrapDetailLines(lines, width) {
669
- const wrappedLines = [];
670
- const stripeIndices = [];
671
- let sectionIndex = 0;
672
- for (const line of lines) {
673
- if (typeof line !== "string") {
674
- sectionIndex += 1;
675
- continue;
676
- }
677
- const physicalLines = line.replaceAll("\t", " ").split(/\r?\n/);
678
- for (const physicalLine of physicalLines) {
679
- if (!physicalLine.trim())
680
- continue;
681
- const leading = physicalLine.match(/^ */)?.[0] ?? "";
682
- const content = physicalLine.slice(leading.length);
683
- const contentWidth = Math.max(1, width - visibleWidth(leading));
684
- const labeled = /^(\s*(?:-\s+)?[^:]+:\s+)(\S.*)$/u.exec(physicalLine);
685
- let wrapped;
686
- if (visibleWidth(physicalLine) > width &&
687
- labeled &&
688
- visibleWidth(labeled[1]) < width) {
689
- const prefix = labeled[1];
690
- const value = labeled[2];
691
- const firstWidth = Math.max(1, width - visibleWidth(prefix));
692
- const first = this.wrapPreferred(value, firstWidth)[0] ??
693
- this.takeVisiblePrefix(value, firstWidth);
694
- const remainder = value.slice(first.length).trimStart();
695
- const continuation = `${leading} `;
696
- const continuationWidth = Math.max(1, width - visibleWidth(continuation));
697
- wrapped = [
698
- `${prefix}${first}`,
699
- ...(remainder
700
- ? this.wrapPreferred(remainder, continuationWidth).map((visualLine) => `${continuation}${visualLine}`)
701
- : []),
702
- ];
703
- }
704
- else {
705
- wrapped = visibleWidth(physicalLine) <= width
706
- ? [physicalLine]
707
- : this.wrapPreferred(content, contentWidth).map((visualLine) => `${leading}${visualLine}`);
708
- }
709
- for (const visualLine of wrapped) {
710
- if (!visualLine.trim())
711
- continue;
712
- wrappedLines.push(visualLine);
713
- stripeIndices.push(sectionIndex);
714
- }
617
+ inlineDocumentValue(value) {
618
+ if (Array.isArray(value)) {
619
+ if (value.length === 0)
620
+ return "[]";
621
+ if (value.every((item) => (item === null || typeof item !== "object") &&
622
+ !(typeof item === "string" && /[\r\n]/u.test(item)))) {
623
+ return `[${value.map((item) => String(item ?? "none")).join(", ")}]`;
715
624
  }
625
+ return undefined;
716
626
  }
717
- return { lines: wrappedLines, stripeIndices };
627
+ if (value && typeof value === "object") {
628
+ if (Object.keys(value).length === 0)
629
+ return "{}";
630
+ }
631
+ return undefined;
718
632
  }
719
- wrapPreferred(content, width) {
720
- const lines = [];
721
- let remaining = stripVTControlCharacters(content).trimEnd();
722
- while (remaining && visibleWidth(remaining) > width) {
723
- const candidate = this.takeVisiblePrefix(remaining, width);
724
- let splitAt = -1;
725
- let trimBoundary = false;
726
- for (let index = 1; index < candidate.length; index += 1) {
727
- if (/\s/u.test(candidate[index])) {
728
- splitAt = index + 1;
729
- trimBoundary = true;
730
- }
731
- else if (/[-+=/_,.:]/u.test(candidate[index])) {
732
- splitAt = index;
733
- trimBoundary = false;
734
- }
735
- }
736
- if (splitAt <= 0)
737
- splitAt = candidate.length;
738
- const head = remaining.slice(0, splitAt).trimEnd();
739
- if (!head) {
740
- lines.push(candidate);
741
- remaining = remaining.slice(candidate.length);
742
- continue;
743
- }
744
- lines.push(head);
745
- remaining = remaining.slice(splitAt);
746
- if (trimBoundary)
747
- remaining = remaining.trimStart();
633
+ labeledDocumentLines(key, value, depth = 0) {
634
+ const inline = this.inlineDocumentValue(value);
635
+ if (inline !== undefined)
636
+ return [`${" ".repeat(depth)}${key}: ${inline}`];
637
+ if (value && typeof value === "object") {
638
+ const indent = " ".repeat(depth);
639
+ const nested = this.document(value, depth);
640
+ return [`${indent}${key}: ${nested[0].slice(indent.length)}`, ...nested.slice(1)];
748
641
  }
749
- if (remaining)
750
- lines.push(remaining);
751
- return lines;
642
+ return this.labeledScalarLines(key, value, depth);
752
643
  }
753
- detailSection(title, content) {
754
- if (content.length === 0)
755
- return [];
644
+ labeledScalarLines(key, value, depth = 0) {
645
+ const indent = " ".repeat(depth);
646
+ const [first = "", ...rest] = String(value ?? "none").split(/\r?\n/u);
756
647
  return [
757
- ` ${title}`,
758
- ...content.flatMap((line) => line
759
- .split(/\r?\n/)
760
- .filter((physicalLine) => physicalLine.trim().length > 0)
761
- .map((physicalLine) => ` ${physicalLine}`)),
762
- DETAIL_SECTION_BREAK,
648
+ `${indent}${key}: ${first}`,
649
+ ...rest.map((line) => `${indent} ${line}`),
763
650
  ];
764
651
  }
765
- readableValueLines(value, indent = "", depth = 0) {
766
- if (depth > 8)
767
- return [`${indent}[nested value]`];
768
- if (typeof value === "string") {
769
- const trimmed = value.trim();
770
- if (/^[{[]/.test(trimmed)) {
771
- try {
772
- return this.readableValueLines(JSON.parse(trimmed), indent, depth + 1);
773
- }
774
- catch {
775
- // Keep non-JSON text as written.
776
- }
777
- }
778
- return value.split(/\r?\n/).map((line) => `${indent}${line}`);
779
- }
652
+ document(value, depth = 0) {
780
653
  if (value === null || value === undefined)
781
- return [`${indent}${String(value)}`];
782
- if (typeof value !== "object")
783
- return [`${indent}${String(value)}`];
654
+ return [`${" ".repeat(depth)}none`];
655
+ const inline = this.inlineDocumentValue(value);
656
+ if (inline !== undefined)
657
+ return [`${" ".repeat(depth)}${inline}`];
784
658
  if (Array.isArray(value)) {
785
- if (value.length === 0)
786
- return [`${indent}(empty)`];
787
- return value.flatMap((item) => {
788
- if (item &&
789
- typeof item === "object" &&
790
- item.type === "text" &&
791
- typeof item.text === "string") {
792
- return this.readableValueLines(item.text, indent, depth + 1);
793
- }
794
- if (!item || typeof item !== "object")
795
- return [`${indent}- ${String(item)}`];
796
- const [first = "", ...rest] = this.readableValueLines(item, `${indent} `, depth + 1);
797
- return [
798
- `${indent}- ${first.trimStart()}`,
799
- ...rest,
800
- ];
801
- });
802
- }
803
- const entries = Object.entries(value);
804
- if (entries.length === 0)
805
- return [`${indent}(empty)`];
806
- return entries.flatMap(([key, item]) => {
807
- if (!item || typeof item !== "object") {
808
- const scalar = typeof item === "string" ? item : String(item);
809
- const [first = "", ...rest] = scalar.split(/\r?\n/);
810
- return [
811
- `${indent}${key}: ${first}`,
812
- ...rest.map((line) => `${indent} ${line}`),
813
- ];
814
- }
659
+ const indent = " ".repeat(depth);
815
660
  return [
816
- `${indent}${key}:`,
817
- ...this.readableValueLines(item, `${indent} `, depth + 1),
661
+ `${indent}[`,
662
+ ...value.flatMap((item, index) => {
663
+ const marker = `${indent} - #${index + 1}`;
664
+ const itemInline = this.inlineDocumentValue(item);
665
+ return itemInline !== undefined
666
+ ? [`${marker}: ${itemInline}`]
667
+ : [marker, ...this.document(item, depth + 2)];
668
+ }),
669
+ `${indent}]`,
818
670
  ];
819
- });
820
- }
821
- turnEvidenceLines(turn) {
822
- const lines = [];
823
- const commandNumber = /^command-(\d+)$/.exec(turn.commandId)?.[1];
824
- const subagent = commandNumber
825
- ? String(Number.parseInt(commandNumber, 10))
826
- : turn.commandId;
827
- const stage = turn.stage && turn.stage !== "subagent" && turn.stage !== "command"
828
- ? ` (${turn.stage})`
829
- : "";
830
- lines.push(` Subagent ${subagent}${stage}`, DETAIL_SECTION_BREAK);
831
- if (turn.userText)
832
- lines.push(...this.detailSection("User", [this.readablePromptText(turn.userText)]));
833
- if (turn.thinking)
834
- lines.push(...this.detailSection("Thinking", [turn.thinking]));
835
- if (turn.assistantText)
836
- lines.push(...this.detailSection("Assistant", [turn.assistantText]));
837
- for (const [index, tool] of turn.toolCalls.entries()) {
838
- const content = [
839
- ...(tool.arguments === undefined
840
- ? []
841
- : ["Input", ...this.readableValueLines(tool.arguments, " ")]),
842
- ...(tool.result === undefined
843
- ? []
844
- : ["Result", ...this.readableValueLines(tool.result, " ")]),
845
- `call id: ${tool.id}`,
671
+ }
672
+ if (typeof value === "object") {
673
+ const indent = " ".repeat(depth);
674
+ const entries = Object.entries(value);
675
+ return [
676
+ `${indent}{`,
677
+ ...entries.flatMap(([key, item], index) => {
678
+ const lines = this.labeledDocumentLines(key, item, depth + 1);
679
+ if (index < entries.length - 1)
680
+ lines[lines.length - 1] += ",";
681
+ return lines;
682
+ }),
683
+ `${indent}}`,
846
684
  ];
847
- lines.push(...this.detailSection(`Tool ${index + 1} (${tool.name}${tool.resultError ? ", Error" : ""})`, content));
848
685
  }
849
- const execution = [
850
- ...(!turn.thinking ? ["thinking: not persisted"] : []),
851
- ...(turn.model ? [`model: ${turn.model}`] : []),
852
- ...(turn.stopReason ? [`stop reason: ${turn.stopReason}`] : []),
853
- ...(turn.error ? [`error: ${turn.error}`] : []),
854
- ...(turn.usage === undefined
855
- ? []
856
- : ["Usage", ...this.readableValueLines(turn.usage, " ")]),
857
- ...(turn.unmatchedToolResults > 0
858
- ? [`unmatched tool results: ${turn.unmatchedToolResults}`]
859
- : []),
860
- ];
861
- if (execution.length > 0)
862
- lines.push(...this.detailSection("Execution", execution));
863
- if (turn.diagnostics.length > 0)
864
- lines.push(...this.detailSection("Diagnostics", turn.diagnostics));
865
- const provenance = [
866
- "Session",
867
- ` ${turn.sessionFile}`,
868
- ...(turn.sessionTruncated ? [" turn list truncated"] : []),
869
- ...(turn.promptFile
870
- ? [
871
- "Prompt",
872
- ` ${turn.promptFile}${turn.promptBytes ? ` (${turn.promptBytes} bytes)` : ""}`,
873
- ]
874
- : []),
875
- ];
876
- lines.push(...this.detailSection("Provenance", provenance));
877
- return lines.length > 0 ? lines : [" No persisted turn evidence"];
878
- }
879
- readablePromptText(text) {
880
- const wrapped = /^\s*<file\b[^>]*>\s*([\s\S]*?)\s*<\/file>\s*$/i.exec(text);
881
- return wrapped?.[1]?.trim() || text;
882
- }
883
- allTurnItems() {
884
- if (!this.selectedRun)
885
- return [];
886
- if (this.turnCache?.run === this.selectedRun)
887
- return this.turnCache.turns;
888
- const turns = this.readTurns(path.join(this.stateRoot, this.selectedRun));
889
- this.turnCache = { run: this.selectedRun, turns };
890
- return turns;
891
- }
892
- turnItems() {
893
- const selectedSubagent = this.subagents()[this.subagentIndex];
894
- return this.allTurnItems()
895
- .filter((turn) => !selectedSubagent ||
896
- selectedSubagent === "all" ||
897
- turn.commandId === selectedSubagent)
898
- .reverse();
899
- }
900
- turnRows() {
901
- return this.turnItems().map((turn) => {
902
- const commandNumber = /^command-(\d+)$/.exec(turn.commandId)?.[1];
903
- const subagent = commandNumber
904
- ? `Subagent ${Number.parseInt(commandNumber, 10)}`
905
- : `Subagent ${turn.commandId}`;
906
- const stage = turn.stage && turn.stage !== "subagent" && turn.stage !== "command"
907
- ? ` (${turn.stage})`
908
- : "";
909
- const toolNames = [...new Set(turn.toolCalls.map((tool) => tool.name))];
910
- const action = turn.toolCalls.length === 0
911
- ? ""
912
- : turn.toolCalls.length === 1
913
- ? ` (${toolNames[0]})`
914
- : toolNames.length === 1
915
- ? ` (${toolNames[0]}, ${turn.toolCalls.length} calls)`
916
- : turn.toolCalls.length <= 2
917
- ? ` (${toolNames.join(", ")})`
918
- : ` (${turn.toolCalls.length} tools)`;
919
- const failed = turn.error || turn.toolCalls.some((tool) => tool.resultError)
920
- ? " (error)"
921
- : "";
922
- const preview = this.readablePromptText(turn.assistantText ?? turn.userText ?? "(no text)").replaceAll(/\s+/g, " ").trim();
923
- return `#${turn.index} ${subagent}${stage} ${turn.model ?? "unknown model"}${action}${failed} ${preview}`;
686
+ return String(value)
687
+ .split(/\r?\n/u)
688
+ .map((line) => `${" ".repeat(depth)}${line}`);
689
+ }
690
+ wrapDocument(lines, width) {
691
+ return lines.flatMap((line) => {
692
+ const normalized = line.replaceAll("\t", " ");
693
+ if (visibleWidth(normalized) <= width)
694
+ return [normalized];
695
+ const leading = normalized.match(/^ */u)?.[0] ?? "";
696
+ const continuation = `${leading} `;
697
+ const wrapped = [];
698
+ let rest = normalized.slice(leading.length);
699
+ let prefix = leading;
700
+ while (rest && visibleWidth(`${prefix}${rest}`) > width) {
701
+ const available = Math.max(1, width - visibleWidth(prefix));
702
+ const candidate = this.visiblePrefix(rest, available);
703
+ const minimumBreak = Math.floor(candidate.length * 0.6);
704
+ const preferred = Math.max(candidate.lastIndexOf(" "), candidate.lastIndexOf("/"), candidate.lastIndexOf(","));
705
+ const cut = preferred >= minimumBreak ? preferred + 1 : candidate.length;
706
+ wrapped.push(`${prefix}${rest.slice(0, cut).trimEnd()}`);
707
+ rest = rest.slice(cut).trimStart();
708
+ prefix = continuation;
709
+ }
710
+ if (rest)
711
+ wrapped.push(`${prefix}${rest}`);
712
+ return wrapped;
924
713
  });
925
714
  }
926
- subagents() {
927
- if (!this.selectedRun || this.tab !== "turns")
928
- return ["all"];
929
- const ids = this.allTurnItems().map((turn) => turn.commandId);
930
- return ["all", ...new Set(ids)];
931
- }
932
- valueOptions() {
933
- if (this.tab === "turns")
934
- return this.subagents();
935
- if (this.filterControlIndex === 0)
936
- return ["all", "room", "direct", "broadcast"];
937
- if (this.filterControlIndex === 1)
938
- return ["all", "unread"];
939
- return this.communicationFromOptions();
940
- }
941
- filterOptions() {
942
- return this.tab === "communications"
943
- ? ["Channel", "State", "From"]
944
- : ["Subagent"];
945
- }
946
- menuOptions() {
947
- if (this.menuLevel === "run")
948
- return this.runs().map((run) => run.run);
949
- if (this.menuLevel === "filter")
950
- return this.filterOptions();
951
- return this.valueOptions();
952
- }
953
- backMenu() {
954
- if (this.menuLevel === "value") {
955
- this.menuLevel = "filter";
956
- this.selectorIndex = this.filterControlIndex;
957
- return;
715
+ styleDocumentLine(line) {
716
+ const indent = line.match(/^ */u)?.[0] ?? "";
717
+ const content = line.slice(indent.length);
718
+ if (/^- #\d+$/u.test(content) || content.endsWith(":")) {
719
+ return `${indent}${this.theme.fg("accent", content)}`;
958
720
  }
959
- this.focus = this.menuLevel === "run" ? "runs" : "tabs";
960
- }
961
- openRunMenu() {
962
- const options = this.runs();
963
- this.selectorIndex = Math.max(0, options.findIndex((run) => run.run === this.selectedRun));
964
- this.menuLevel = "run";
965
- this.focus = "select";
721
+ const labeled = /^([^:]+:)(.*)$/u.exec(content);
722
+ if (labeled) {
723
+ return `${indent}${this.theme.fg("muted", labeled[1])}${this.theme.fg("text", labeled[2])}`;
724
+ }
725
+ return `${indent}${this.theme.fg(content === "none" || content === "[]" ? "muted" : "text", content)}`;
966
726
  }
967
- openFilterMenu() {
968
- this.selectorIndex = this.filterControlIndex;
969
- this.menuLevel = "filter";
970
- this.focus = "select";
727
+ visiblePrefix(value, width) {
728
+ let consumed = 0;
729
+ let result = "";
730
+ for (const character of value) {
731
+ const characterWidth = visibleWidth(character);
732
+ if (consumed + characterWidth > width)
733
+ break;
734
+ result += character;
735
+ consumed += characterWidth;
736
+ }
737
+ return result;
971
738
  }
972
- applyMenuOption(value) {
973
- if (!value)
974
- return;
975
- if (this.menuLevel === "run") {
976
- this.selectRun(value, Math.max(0, this.runs().findIndex((run) => run.run === value)));
977
- this.focus = "runs";
978
- return;
739
+ renderKeyHints() {
740
+ const hint = (keys, description) => `${this.theme.fg("accent", keys)}${this.theme.fg("borderAccent", ` ${description}`)}`;
741
+ const divider = this.theme.fg("borderAccent", " ─ ");
742
+ const hints = (...items) => items.map(([keys, description]) => hint(keys, description)).join(divider);
743
+ if (this.killConfirmation) {
744
+ return hints(["←→/tab", "choose"], ["enter/y", "confirm"], ["esc/n", "cancel"]);
979
745
  }
980
- if (this.menuLevel === "filter") {
981
- this.filterControlIndex = this.selectorIndex;
982
- const options = this.valueOptions();
983
- const current = this.currentFilterValue();
984
- this.selectorIndex = Math.max(0, options.indexOf(current));
985
- this.menuLevel = "value";
986
- return;
746
+ if (this.focus === "select") {
747
+ return hints(["↑↓", "option"], ["enter/→", "apply"], ["←/esc", "back"]);
987
748
  }
988
- if (this.tab === "turns")
989
- this.subagentIndex = Math.max(0, this.subagents().indexOf(value));
990
- else if (this.filterControlIndex === 0)
991
- this.communicationChannel = value;
992
- else if (this.filterControlIndex === 1)
993
- this.communicationUnread = value === "unread";
994
- else
995
- this.communicationFrom = value;
996
- this.rowIndex = 0;
997
- this.menuLevel = "filter";
998
- this.selectorIndex = this.filterControlIndex;
999
- }
1000
- filterValue(index) {
1001
- if (this.tab === "turns")
1002
- return this.subagents()[this.subagentIndex] ?? "all";
1003
- if (index === 0)
1004
- return this.communicationChannel;
1005
- if (index === 1)
1006
- return this.communicationUnread ? "unread" : "all";
1007
- return this.communicationFrom;
1008
- }
1009
- currentFilterValue() {
1010
- return this.filterValue(this.filterControlIndex);
1011
- }
1012
- renderMenuBox(options, focusedIndex, current, parentIndex, omitLeftBorder = false, omitRightBorder = false, maxVisibleOptions = options.length) {
1013
- const visibleLimit = Math.max(1, Math.min(maxVisibleOptions, options.length));
1014
- const anchorIndex = Math.max(0, Math.min(options.length - 1, focusedIndex >= 0 ? focusedIndex : (parentIndex ?? 0)));
1015
- const maxStart = Math.max(0, options.length - visibleLimit);
1016
- const start = Math.max(0, Math.min(anchorIndex - Math.floor(visibleLimit / 2), maxStart));
1017
- const visibleOptions = options.slice(start, start + visibleLimit);
1018
- const hiddenAbove = start > 0;
1019
- const hiddenBelow = start + visibleOptions.length < options.length;
1020
- const contentWidth = Math.max(4, ...options.map((option) => visibleWidth(option) + 4));
1021
- const border = (left, right, marker = "") => this.theme.fg("borderAccent", `${omitLeftBorder ? "" : left}${marker}${"─".repeat(Math.max(0, contentWidth - visibleWidth(marker)))}${omitRightBorder ? "" : right}`);
1022
- return [
1023
- border("╭", "╮", hiddenAbove ? "↑" : ""),
1024
- ...visibleOptions.map((option, localIndex) => {
1025
- const index = start + localIndex;
1026
- const focused = index === focusedIndex;
1027
- const row = this.fit(`${focused ? " ▶ " : " "}${option}`, contentWidth);
1028
- const colored = focused || option === current || index === parentIndex
1029
- ? this.theme.fg("accent", row)
1030
- : row;
1031
- const styled = focused || index === parentIndex
1032
- ? this.theme.bg("selectedBg", colored)
1033
- : colored;
1034
- return `${omitLeftBorder ? "" : this.theme.fg("borderAccent", "│")}${styled}${omitRightBorder ? "" : this.theme.fg("borderAccent", "│")}`;
1035
- }),
1036
- border("╰", "╯", hiddenBelow ? "↓" : ""),
1037
- ];
1038
- }
1039
- renderFilterMenus(_width) {
1040
- const filters = this.filterOptions().map((filter, index) => `${filter}: ${this.filterValue(index)}`);
1041
- const maxVisibleOptions = Math.max(1, this.contentViewportRows() - 1);
1042
- const parentFocus = this.menuLevel === "filter" ? this.selectorIndex : -1;
1043
- const parent = this.renderMenuBox(filters, parentFocus, undefined, this.menuLevel === "value" ? this.filterControlIndex : undefined, false, false, maxVisibleOptions);
1044
- if (this.menuLevel !== "value")
1045
- return parent;
1046
- const parentShared = this.renderMenuBox(filters, parentFocus, undefined, this.filterControlIndex, false, true, maxVisibleOptions);
1047
- const child = this.renderMenuBox(this.valueOptions(), this.selectorIndex, this.currentFilterValue(), undefined, false, false, maxVisibleOptions);
1048
- return Array.from({ length: Math.max(parent.length, child.length) }, (_, index) => child[index]
1049
- ? `${parentShared[index] ?? " ".repeat(visibleWidth(parentShared[0]))}${child[index]}`
1050
- : (parent[index] ?? ""));
1051
- }
1052
- openDetail() {
1053
- if (this.tab === "communications") {
1054
- const preview = this.communicationPreviews()[this.rowIndex];
1055
- this.detailCommunication = preview;
1056
- if (preview)
1057
- this.readKeys.add(Inspector.inspectorPreviewReadKey(preview));
749
+ if (this.focus === "detail") {
750
+ return hints(["↑↓/pgup/pgdn", "scroll"], ["←/esc", "back"]);
1058
751
  }
1059
- else
1060
- this.detailTurn = this.turnItems()[this.rowIndex];
1061
- this.detailScroll = 0;
1062
- this.detailOpen = Boolean(this.detailCommunication || this.detailTurn);
1063
- if (this.detailOpen)
1064
- this.focus = "detail";
1065
- }
1066
- backDetail() {
1067
- this.closeDetail();
752
+ if (this.focus === "document") {
753
+ return hints(["↑↓/pgup/pgdn", "scroll"], ["↑ at top", "tabs"], ["←/esc", "tabs"]);
754
+ }
755
+ if (this.focus === "list") {
756
+ return hints(["↑↓/pgup/pgdn", "row"], ["→/enter", "open"], ["←", "tabs"], ["esc", "close"]);
757
+ }
758
+ if (this.focus === "runs") {
759
+ const items = [["←→", "run"], ["↓", "tabs"], ["enter", "list"]];
760
+ const run = this.runs()[this.runIndex];
761
+ if (this.killRun && run?.status === "running" && run.runInstanceId)
762
+ items.push(["k", "kill"]);
763
+ items.push(["esc", "close"]);
764
+ return hints(...items);
765
+ }
766
+ return this.tab === "trace"
767
+ ? hints(["←→", "tabs"], ["↑↓", "focus"], ["enter/f", "source"], ["esc", "close"])
768
+ : hints(["←→", "tabs"], ["↑↓", "focus"], ["enter", "open"], ["esc", "close"]);
1068
769
  }
1069
- closeDetail() {
1070
- this.detailOpen = false;
1071
- this.detailCommunication = undefined;
1072
- this.detailTurn = undefined;
1073
- this.detailScroll = 0;
1074
- this.focus = "list";
770
+ statusColor(status) {
771
+ if (status === "failed")
772
+ return "error";
773
+ if (status === "running")
774
+ return "warning";
775
+ if (status === "done" || status === "terminal")
776
+ return "success";
777
+ return "muted";
1075
778
  }
1076
779
  footerBorder(hints, width) {
1077
780
  const prefix = " ";
@@ -1080,63 +783,26 @@ export class ActorInspectorOverlay {
1080
783
  const fill = "─".repeat(Math.max(0, width - visibleWidth(prefix) - visibleWidth(suffix) - visibleWidth(content)));
1081
784
  return `${this.theme.fg("borderAccent", `╰${prefix}`)}${content}${this.theme.fg("borderAccent", `${suffix}${fill}╯`)}`;
1082
785
  }
1083
- border(left, title, right, width) {
1084
- const titleText = truncateToWidth(title, width, "");
1085
- const fill = "─".repeat(Math.max(0, width - visibleWidth(titleText)));
1086
- return this.theme.fg("borderAccent", `${left}${titleText}${fill}${right}`);
1087
- }
1088
786
  stripeBackground(content, index) {
1089
- if (index % 2 === 0)
1090
- return content;
1091
- const segments = content.split("…");
1092
- return segments
1093
- .map((segment, segmentIndex) => `${this.theme.bg("customMessageBg", segment)}${segmentIndex < segments.length - 1 ? this.theme.bg("customMessageBg", "…") : ""}`)
1094
- .join("");
787
+ return index % 2 === 0 ? content : this.theme.bg("customMessageBg", content);
1095
788
  }
1096
789
  stripedRow(content, width, index) {
1097
- return this.row(this.stripeBackground(this.fit(content, width), index), width, true);
1098
- }
1099
- row(content, width, fitted = false) {
1100
- const body = fitted ? content : this.fit(content, width);
1101
- return `${this.theme.fg("borderAccent", "│")}${body}${this.theme.fg("borderAccent", "│")}`;
790
+ const fitted = this.fit(content, width);
791
+ return `${this.theme.fg("borderAccent", "│")}${this.stripeBackground(fitted, index)}${this.theme.fg("borderAccent", "│")}`;
1102
792
  }
1103
- takeVisiblePrefix(content, width) {
1104
- const plain = stripVTControlCharacters(content);
1105
- let consumed = 0;
1106
- let result = "";
1107
- for (const character of plain) {
1108
- const characterWidth = visibleWidth(character);
1109
- if (consumed + characterWidth > width)
1110
- break;
1111
- result += character;
1112
- consumed += characterWidth;
1113
- }
1114
- return result;
1115
- }
1116
- dropVisiblePrefix(content, width) {
1117
- const plain = stripVTControlCharacters(content);
1118
- let consumed = 0;
1119
- let index = 0;
1120
- for (const character of plain) {
1121
- if (consumed >= width)
1122
- break;
1123
- consumed += visibleWidth(character);
1124
- index += character.length;
1125
- }
1126
- return plain.slice(index);
793
+ row(content, width) {
794
+ return `${this.theme.fg("borderAccent", "│")}${this.fit(content, width)}${this.theme.fg("borderAccent", "│")}`;
1127
795
  }
1128
- center(content, width) {
1129
- const contentWidth = visibleWidth(content);
1130
- const leading = Math.max(0, Math.floor((width - contentWidth) / 2));
1131
- return `${" ".repeat(leading)}${content}`;
796
+ border(left, label, right, width) {
797
+ const bounded = truncateToWidth(label, width, "");
798
+ return this.theme.fg("borderAccent", `${left}${bounded}${"─".repeat(Math.max(0, width - visibleWidth(bounded)))}${right}`);
1132
799
  }
1133
800
  fit(content, width) {
1134
- // A Component render entry must remain exactly one terminal row. Persisted
1135
- // prompts, messages, and tool results may contain line breaks or tabs;
1136
- // passing those through lets them escape the overlay compositor and corrupt
1137
- // the underlying TUI during tab changes.
1138
- const singleLine = content.replace(/[\r\n\t]/g, " ");
1139
- const bounded = truncateToWidth(singleLine, width, "…");
801
+ const bounded = truncateToWidth(content, width, "");
1140
802
  return `${bounded}${" ".repeat(Math.max(0, width - visibleWidth(bounded)))}`;
1141
803
  }
804
+ center(content, width) {
805
+ const padding = Math.max(0, Math.floor((width - visibleWidth(content)) / 2));
806
+ return `${" ".repeat(padding)}${content}`;
807
+ }
1142
808
  }