@llblab/pi-actors 0.42.2 → 0.43.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 (254) hide show
  1. package/AGENTS.md +121 -175
  2. package/CHANGELOG.md +16 -0
  3. package/README.md +113 -276
  4. package/dist/fixtures/protocol/control-endpoint.json +6 -0
  5. package/dist/fixtures/protocol/control-record.json +9 -0
  6. package/dist/fixtures/protocol/recipe-summary.json +4 -12
  7. package/dist/fixtures/protocol/trace-event.json +9 -0
  8. package/dist/lib/async-runs.d.ts +14 -38
  9. package/dist/lib/async-runs.js +158 -108
  10. package/dist/lib/control.d.ts +12 -0
  11. package/dist/lib/control.js +84 -0
  12. package/dist/lib/execution-sessions.d.ts +17 -0
  13. package/dist/lib/execution-sessions.js +85 -0
  14. package/dist/lib/file-state.d.ts +1 -0
  15. package/dist/lib/file-state.js +17 -5
  16. package/dist/lib/inspector-actions.d.ts +2 -2
  17. package/dist/lib/inspector-actions.js +2 -2
  18. package/dist/lib/inspector-command.js +3 -3
  19. package/dist/lib/inspector-overlay.d.ts +52 -70
  20. package/dist/lib/inspector-overlay.js +532 -905
  21. package/dist/lib/inspector.d.ts +3 -71
  22. package/dist/lib/inspector.js +19 -665
  23. package/dist/lib/limits.d.ts +4 -2
  24. package/dist/lib/limits.js +4 -2
  25. package/dist/lib/observability.d.ts +17 -17
  26. package/dist/lib/observability.js +45 -84
  27. package/dist/lib/pi.d.ts +1 -1
  28. package/dist/lib/prompts.d.ts +1 -1
  29. package/dist/lib/prompts.js +2 -2
  30. package/dist/lib/recipe-control.d.ts +7 -0
  31. package/dist/lib/recipe-control.js +39 -0
  32. package/dist/lib/recipes-discovery.js +2 -0
  33. package/dist/lib/recipes-references.d.ts +1 -14
  34. package/dist/lib/recipes-references.js +6 -21
  35. package/dist/lib/review-projection.js +1 -5
  36. package/dist/lib/run-ui-runtime.js +2 -2
  37. package/dist/lib/runs-control-delivery.d.ts +21 -0
  38. package/dist/lib/runs-control-delivery.js +127 -0
  39. package/dist/lib/runs-controls.d.ts +35 -0
  40. package/dist/lib/runs-controls.js +144 -0
  41. package/dist/lib/runs-retention.d.ts +7 -0
  42. package/dist/lib/runs-retention.js +27 -3
  43. package/dist/lib/runs-start.js +4 -2
  44. package/dist/lib/runs-status.js +11 -6
  45. package/dist/lib/runs-trace.d.ts +24 -0
  46. package/dist/lib/runs-trace.js +98 -0
  47. package/dist/lib/runtime-notifier.d.ts +1 -1
  48. package/dist/lib/runtime-notifier.js +1 -1
  49. package/dist/lib/tools-inspect.d.ts +3 -3
  50. package/dist/lib/tools-inspect.js +203 -708
  51. package/dist/lib/tools-local.js +2 -10
  52. package/dist/lib/tools-message.d.ts +7 -7
  53. package/dist/lib/tools-message.js +95 -396
  54. package/dist/lib/tools-response.d.ts +1 -4
  55. package/dist/lib/tools-response.js +5 -39
  56. package/dist/lib/tools-spawn.js +16 -28
  57. package/dist/lib/tools.js +1 -2
  58. package/dist/lib/trace-projection.d.ts +22 -0
  59. package/dist/lib/trace-projection.js +165 -0
  60. package/dist/recipes/draft-review.json +0 -10
  61. package/dist/recipes/lens-swarm.json +0 -14
  62. package/dist/recipes/music-player.json +10 -19
  63. package/dist/recipes/pipeline-architect-coordinator.json +0 -11
  64. package/dist/recipes/pipeline-artifact-bundle.json +1 -22
  65. package/dist/recipes/pipeline-artifact-report.json +1 -18
  66. package/dist/recipes/pipeline-artifact-write.json +1 -18
  67. package/dist/recipes/pipeline-async-run-ops.json +0 -12
  68. package/dist/recipes/pipeline-checkpoint-continuation.json +0 -14
  69. package/dist/recipes/pipeline-development-tasking.json +0 -12
  70. package/dist/recipes/pipeline-docs-maintenance.json +0 -12
  71. package/dist/recipes/pipeline-media-library.json +0 -12
  72. package/dist/recipes/pipeline-quorum-review.json +0 -12
  73. package/dist/recipes/pipeline-release-readiness.json +0 -12
  74. package/dist/recipes/pipeline-release-summary.json +0 -12
  75. package/dist/recipes/pipeline-repo-health.json +0 -12
  76. package/dist/recipes/pipeline-research-synthesis.json +0 -11
  77. package/dist/recipes/pipeline-review-readiness.json +0 -12
  78. package/dist/recipes/resource-locker.json +27 -0
  79. package/dist/recipes/subagent-artifact.json +0 -9
  80. package/dist/recipes/subagent-checkpoint.json +0 -10
  81. package/dist/recipes/subagent-conflict-report.json +0 -11
  82. package/dist/recipes/subagent-contradiction-map.json +0 -11
  83. package/dist/recipes/subagent-critic.json +0 -11
  84. package/dist/recipes/subagent-evidence-map.json +0 -11
  85. package/dist/recipes/subagent-followup.json +0 -10
  86. package/dist/recipes/subagent-judge.json +0 -11
  87. package/dist/recipes/subagent-merge.json +0 -11
  88. package/dist/recipes/subagent-normalize.json +0 -11
  89. package/dist/recipes/subagent-plan.json +0 -11
  90. package/dist/recipes/subagent-preflight.json +0 -11
  91. package/dist/recipes/subagent-prompt.json +0 -10
  92. package/dist/recipes/subagent-quorum.json +0 -10
  93. package/dist/recipes/subagent-review-coordinator.json +0 -14
  94. package/dist/recipes/subagent-review.json +0 -11
  95. package/dist/recipes/subagent-task-card.json +0 -11
  96. package/dist/recipes/subagent-tools.json +0 -10
  97. package/dist/recipes/subagent-verify.json +0 -11
  98. package/dist/recipes/subagents-prompts.json +0 -10
  99. package/dist/recipes/tool-review.json +0 -10
  100. package/dist/scripts/async-runner.mjs +25 -25
  101. package/dist/scripts/conformance.mjs +4 -2
  102. package/dist/scripts/locker.mjs +200 -66
  103. package/dist/scripts/music-player.mjs +159 -150
  104. package/dist/scripts/recipe-utils.mjs +6 -96
  105. package/dist/scripts/release-gates.mjs +60 -0
  106. package/dist/scripts/validate-recipe.mjs +3 -53
  107. package/dist/skills/actors/SKILL.md +53 -266
  108. package/dist/skills/swarm/SKILL.md +11 -33
  109. package/docs/0.43-baseline.md +44 -0
  110. package/docs/README.md +3 -3
  111. package/docs/actor-inspector.md +26 -64
  112. package/docs/actors-deep-reference.md +92 -50
  113. package/docs/async-runs.md +81 -328
  114. package/docs/command-templates.md +2 -2
  115. package/docs/component-recipes.md +30 -133
  116. package/docs/recipe-library.md +57 -182
  117. package/docs/task-first-recipes.md +10 -12
  118. package/docs/template-recipes.md +76 -289
  119. package/docs/tool-registry.md +41 -161
  120. package/fixtures/protocol/control-endpoint.json +6 -0
  121. package/fixtures/protocol/control-record.json +9 -0
  122. package/fixtures/protocol/recipe-summary.json +4 -12
  123. package/fixtures/protocol/trace-event.json +9 -0
  124. package/lib/async-runs.ts +202 -201
  125. package/lib/control.ts +102 -0
  126. package/lib/execution-sessions.ts +111 -0
  127. package/lib/file-state.ts +17 -4
  128. package/lib/inspector-actions.ts +2 -2
  129. package/lib/inspector-command.ts +3 -3
  130. package/lib/inspector-overlay.ts +577 -1121
  131. package/lib/inspector.ts +46 -979
  132. package/lib/limits.ts +4 -2
  133. package/lib/observability.ts +63 -104
  134. package/lib/pi.ts +1 -1
  135. package/lib/prompts.ts +2 -2
  136. package/lib/recipe-control.ts +45 -0
  137. package/lib/recipes-discovery.ts +2 -0
  138. package/lib/recipes-references.ts +9 -45
  139. package/lib/review-projection.ts +1 -5
  140. package/lib/run-ui-runtime.ts +2 -2
  141. package/lib/runs-control-delivery.ts +181 -0
  142. package/lib/runs-controls.ts +204 -0
  143. package/lib/runs-retention.ts +38 -3
  144. package/lib/runs-start.ts +4 -2
  145. package/lib/runs-status.ts +11 -6
  146. package/lib/runs-trace.ts +132 -0
  147. package/lib/runtime-notifier.ts +1 -1
  148. package/lib/tools-inspect.ts +240 -901
  149. package/lib/tools-local.ts +2 -12
  150. package/lib/tools-message.ts +112 -519
  151. package/lib/tools-response.ts +5 -52
  152. package/lib/tools-spawn.ts +16 -32
  153. package/lib/tools.ts +1 -2
  154. package/lib/trace-projection.ts +221 -0
  155. package/package.json +2 -1
  156. package/recipes/draft-review.json +0 -10
  157. package/recipes/lens-swarm.json +0 -14
  158. package/recipes/music-player.json +10 -19
  159. package/recipes/pipeline-architect-coordinator.json +0 -11
  160. package/recipes/pipeline-artifact-bundle.json +1 -22
  161. package/recipes/pipeline-artifact-report.json +1 -18
  162. package/recipes/pipeline-artifact-write.json +1 -18
  163. package/recipes/pipeline-async-run-ops.json +0 -12
  164. package/recipes/pipeline-checkpoint-continuation.json +0 -14
  165. package/recipes/pipeline-development-tasking.json +0 -12
  166. package/recipes/pipeline-docs-maintenance.json +0 -12
  167. package/recipes/pipeline-media-library.json +0 -12
  168. package/recipes/pipeline-quorum-review.json +0 -12
  169. package/recipes/pipeline-release-readiness.json +0 -12
  170. package/recipes/pipeline-release-summary.json +0 -12
  171. package/recipes/pipeline-repo-health.json +0 -12
  172. package/recipes/pipeline-research-synthesis.json +0 -11
  173. package/recipes/pipeline-review-readiness.json +0 -12
  174. package/recipes/resource-locker.json +27 -0
  175. package/recipes/subagent-artifact.json +0 -9
  176. package/recipes/subagent-checkpoint.json +0 -10
  177. package/recipes/subagent-conflict-report.json +0 -11
  178. package/recipes/subagent-contradiction-map.json +0 -11
  179. package/recipes/subagent-critic.json +0 -11
  180. package/recipes/subagent-evidence-map.json +0 -11
  181. package/recipes/subagent-followup.json +0 -10
  182. package/recipes/subagent-judge.json +0 -11
  183. package/recipes/subagent-merge.json +0 -11
  184. package/recipes/subagent-normalize.json +0 -11
  185. package/recipes/subagent-plan.json +0 -11
  186. package/recipes/subagent-preflight.json +0 -11
  187. package/recipes/subagent-prompt.json +0 -10
  188. package/recipes/subagent-quorum.json +0 -10
  189. package/recipes/subagent-review-coordinator.json +0 -14
  190. package/recipes/subagent-review.json +0 -11
  191. package/recipes/subagent-task-card.json +0 -11
  192. package/recipes/subagent-tools.json +0 -10
  193. package/recipes/subagent-verify.json +0 -11
  194. package/recipes/subagents-prompts.json +0 -10
  195. package/recipes/tool-review.json +0 -10
  196. package/scripts/async-runner.mjs +25 -25
  197. package/scripts/conformance.mjs +4 -2
  198. package/scripts/locker.mjs +200 -66
  199. package/scripts/music-player.mjs +159 -150
  200. package/scripts/recipe-utils.mjs +6 -96
  201. package/scripts/release-gates.mjs +60 -0
  202. package/scripts/validate-recipe.mjs +3 -53
  203. package/skills/actors/SKILL.md +53 -266
  204. package/skills/swarm/SKILL.md +11 -33
  205. package/dist/fixtures/protocol/actor-message-branch.json +0 -13
  206. package/dist/fixtures/protocol/mailbox-contract.json +0 -15
  207. package/dist/fixtures/protocol/room-message.json +0 -11
  208. package/dist/fixtures/protocol/room-roster.json +0 -11
  209. package/dist/fixtures/protocol/run-inbox-message.json +0 -9
  210. package/dist/fixtures/protocol/run-outbox-event.json +0 -9
  211. package/dist/lib/mailbox-loop.d.ts +0 -41
  212. package/dist/lib/mailbox-loop.js +0 -60
  213. package/dist/lib/messages.d.ts +0 -25
  214. package/dist/lib/messages.js +0 -122
  215. package/dist/lib/rooms.d.ts +0 -104
  216. package/dist/lib/rooms.js +0 -647
  217. package/dist/lib/runs-mailbox.d.ts +0 -25
  218. package/dist/lib/runs-mailbox.js +0 -146
  219. package/dist/lib/runs-messages.d.ts +0 -15
  220. package/dist/lib/runs-messages.js +0 -179
  221. package/dist/lib/runs-outbox.d.ts +0 -41
  222. package/dist/lib/runs-outbox.js +0 -87
  223. package/dist/lib/tools-mailbox.d.ts +0 -8
  224. package/dist/lib/tools-mailbox.js +0 -48
  225. package/dist/recipes/actor-worker.json +0 -39
  226. package/dist/recipes/coordinator-locker.json +0 -45
  227. package/dist/recipes/locker.json +0 -45
  228. package/dist/recipes/pipeline-room-swarm.json +0 -50
  229. package/dist/recipes/subagent-message.json +0 -32
  230. package/dist/recipes/utility-actor-message.json +0 -23
  231. package/dist/scripts/actor-worker.mjs +0 -214
  232. package/dist/scripts/coordinator.mjs +0 -799
  233. package/docs/actor-messages.md +0 -225
  234. package/fixtures/protocol/actor-message-branch.json +0 -13
  235. package/fixtures/protocol/mailbox-contract.json +0 -15
  236. package/fixtures/protocol/room-message.json +0 -11
  237. package/fixtures/protocol/room-roster.json +0 -11
  238. package/fixtures/protocol/run-inbox-message.json +0 -9
  239. package/fixtures/protocol/run-outbox-event.json +0 -9
  240. package/lib/mailbox-loop.ts +0 -144
  241. package/lib/messages.ts +0 -151
  242. package/lib/rooms.ts +0 -939
  243. package/lib/runs-mailbox.ts +0 -208
  244. package/lib/runs-messages.ts +0 -252
  245. package/lib/runs-outbox.ts +0 -144
  246. package/lib/tools-mailbox.ts +0 -56
  247. package/recipes/actor-worker.json +0 -39
  248. package/recipes/coordinator-locker.json +0 -45
  249. package/recipes/locker.json +0 -45
  250. package/recipes/pipeline-room-swarm.json +0 -50
  251. package/recipes/subagent-message.json +0 -32
  252. package/recipes/utility-actor-message.json +0 -23
  253. package/scripts/actor-worker.mjs +0 -214
  254. package/scripts/coordinator.mjs +0 -799
@@ -1,161 +1,125 @@
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 RunsControls from "./runs-controls.js";
11
+ import * as RunControlDelivery from "./runs-control-delivery.js";
12
+ import * as TraceProjection from "./trace-projection.js";
13
+ const TABS = ["recipe", "trace", "control"];
14
+ const TRACE_SOURCES = [
15
+ "all",
16
+ "lifecycle",
17
+ "control",
18
+ "process",
19
+ "agent",
20
+ "artifact",
21
+ "runtime",
14
22
  ];
15
- const DETAIL_SECTION_BREAK = { sectionBreak: true };
16
23
  export class ActorInspectorOverlay {
17
24
  done;
18
25
  killRun;
19
26
  ownerId;
20
- readTurns;
27
+ readRuns;
28
+ readTrace;
21
29
  stateRoot;
22
30
  theme;
23
31
  tui;
24
32
  refreshTimer;
25
- runIndex = 0;
26
- communicationChannel = "all";
27
- communicationFrom = "all";
28
- communicationUnread = false;
29
33
  contentStripeIndices = [];
30
- detailCommunication;
31
- detailOpen = false;
34
+ documentScroll = 0;
32
35
  detailScroll = 0;
33
- detailTurn;
34
36
  feedback;
37
+ focus = "runs";
35
38
  killConfirmation;
36
39
  killDialogChoice = "cancel";
37
- readKeys = new Set();
38
- focus = "tabs";
39
- filterControlIndex = 0;
40
- menuLevel = "filter";
41
- selectorIndex = 0;
42
- recipeScroll = 0;
43
40
  rowIndex = 0;
44
- selectedRun;
45
- subagentIndex = 0;
46
- tab = "recipe";
47
- turnCache;
41
+ runCache;
42
+ runIndex = 0;
43
+ selectorIndex = 0;
44
+ selectorMode;
45
+ tabIndex = 0;
46
+ traceCache;
47
+ traceDetail;
48
+ traceSourceIndex = 0;
48
49
  constructor(options) {
49
50
  this.done = options.done;
50
51
  this.killRun = options.killRun;
51
52
  this.ownerId = options.ownerId;
52
- this.readTurns = options.readTurns ?? Inspector.readActorInspectorTurns;
53
+ this.readRuns = options.readRuns ?? ActorInspector.readActorInspectorRuns;
54
+ this.readTrace = options.readTrace ?? TraceProjection.projectRunTrace;
53
55
  this.stateRoot = options.stateRoot;
54
56
  this.theme = options.theme;
55
57
  this.tui = options.tui;
56
58
  this.refreshTimer = setInterval(() => {
59
+ this.runCache = undefined;
57
60
  if (this.focus !== "list" && this.focus !== "detail")
58
- this.turnCache = undefined;
61
+ this.traceCache = undefined;
59
62
  this.tui.requestRender();
60
- }, 1000);
63
+ }, 1_000);
61
64
  this.refreshTimer.unref?.();
62
65
  }
66
+ dispose() {
67
+ clearInterval(this.refreshTimer);
68
+ }
69
+ invalidate() { }
63
70
  handleInput(data) {
64
71
  const runs = this.runs();
65
- this.ensureSelectedRun(runs);
72
+ this.runIndex = Math.min(this.runIndex, Math.max(0, runs.length - 1));
66
73
  if (matchesKey(data, "ctrl+c")) {
67
74
  this.done();
68
75
  return;
69
76
  }
70
77
  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
- }
78
+ this.handleKillInput(data);
90
79
  this.tui.requestRender();
91
80
  return;
92
81
  }
93
82
  if (data.toLowerCase() === "k") {
94
- this.requestKill(runs);
83
+ this.requestKill(runs[this.runIndex]);
95
84
  this.tui.requestRender();
96
85
  return;
97
86
  }
98
87
  if (matchesKey(data, "escape")) {
99
88
  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";
89
+ this.selectorMode = undefined;
90
+ this.focus = this.tab === "trace" ? "tabs" : "runs";
91
+ }
92
+ else if (this.focus === "detail") {
93
+ this.traceDetail = undefined;
94
+ this.detailScroll = 0;
95
+ this.focus = "list";
106
96
  }
107
- else if (this.focus === "detail")
108
- this.backDetail();
109
- else if (this.focus === "recipe")
97
+ else if (this.focus === "document" || this.focus === "list") {
110
98
  this.focus = "tabs";
111
- else
99
+ }
100
+ else {
112
101
  this.done();
102
+ }
113
103
  this.tui.requestRender();
114
104
  return;
115
105
  }
116
106
  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]);
107
+ this.handleSelectorInput(data, runs);
127
108
  this.tui.requestRender();
128
109
  return;
129
110
  }
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";
111
+ if (this.focus === "detail") {
112
+ this.handleScrollableInput(data, "detail");
145
113
  this.tui.requestRender();
146
114
  return;
147
115
  }
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();
116
+ if (this.focus === "document") {
117
+ if (matchesKey(data, "left"))
118
+ this.focus = "tabs";
119
+ else if (matchesKey(data, "up") && this.documentScroll === 0)
120
+ this.focus = "tabs";
121
+ else
122
+ this.handleScrollableInput(data, "document");
159
123
  this.tui.requestRender();
160
124
  return;
161
125
  }
@@ -166,163 +130,185 @@ export class ActorInspectorOverlay {
166
130
  this.cycleRun(runs, 1);
167
131
  else if (matchesKey(data, "down"))
168
132
  this.focus = "tabs";
169
- else if (matchesKey(data, "return") && runs.length > 0)
170
- this.openRunMenu();
133
+ else if (matchesKey(data, "return") && runs.length > 0) {
134
+ this.selectorMode = "run";
135
+ this.selectorIndex = this.runIndex;
136
+ this.focus = "select";
137
+ }
171
138
  }
172
139
  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;
140
+ if (matchesKey(data, "left") || matchesKey(data, "right") || matchesKey(data, "tab")) {
141
+ const direction = matchesKey(data, "left") ? -1 : 1;
142
+ this.tabIndex = (this.tabIndex + direction + TABS.length) % TABS.length;
143
+ this.resetContentPosition();
180
144
  }
181
- else if (matchesKey(data, "up"))
145
+ else if (matchesKey(data, "up")) {
182
146
  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();
147
+ }
148
+ else if (matchesKey(data, "down")) {
149
+ this.focus = this.tab === "trace" && this.traceItems(runs[this.runIndex]).length > 0
150
+ ? "list"
151
+ : "document";
152
+ }
153
+ else if (matchesKey(data, "return")) {
154
+ if (this.tab === "trace")
155
+ this.openSourceSelector(runs[this.runIndex]);
156
+ else
157
+ this.focus = "document";
158
+ }
190
159
  }
191
160
  else if (this.focus === "list") {
192
- const count = this.tab === "communications" ? this.communicationPreviews().length : this.turnItems().length;
161
+ const count = this.traceItems(runs[this.runIndex]).length;
193
162
  if (matchesKey(data, "left"))
194
163
  this.focus = "tabs";
195
- else if (matchesKey(data, "pageUp"))
196
- this.rowIndex = Math.max(0, this.rowIndex - this.contentViewportRows());
197
- else if (matchesKey(data, "pageDown"))
198
- this.rowIndex = Math.min(Math.max(0, count - 1), this.rowIndex + this.contentViewportRows());
199
164
  else if (matchesKey(data, "up")) {
200
165
  if (this.rowIndex === 0)
201
166
  this.focus = "tabs";
202
167
  else
203
168
  this.rowIndex -= 1;
204
169
  }
205
- else if (matchesKey(data, "down"))
170
+ else if (matchesKey(data, "down")) {
206
171
  this.rowIndex = Math.min(Math.max(0, count - 1), this.rowIndex + 1);
207
- else if (matchesKey(data, "return") || matchesKey(data, "right"))
208
- this.openDetail();
172
+ }
173
+ else if (matchesKey(data, "pageUp")) {
174
+ this.rowIndex = Math.max(0, this.rowIndex - this.contentViewportRows());
175
+ }
176
+ else if (matchesKey(data, "pageDown")) {
177
+ this.rowIndex = Math.min(Math.max(0, count - 1), this.rowIndex + this.contentViewportRows());
178
+ }
179
+ else if (matchesKey(data, "return") || matchesKey(data, "right")) {
180
+ const item = this.traceItems(runs[this.runIndex])[this.rowIndex];
181
+ if (item) {
182
+ this.traceDetail = item;
183
+ this.detailScroll = 0;
184
+ this.focus = "detail";
185
+ }
186
+ }
187
+ }
188
+ if (data.toLowerCase() === "f" && this.tab === "trace") {
189
+ this.cycleTraceSource(runs[this.runIndex]);
209
190
  }
210
191
  this.tui.requestRender();
211
192
  }
212
193
  render(width) {
213
- this.ensureSelectedRun(this.runs());
214
194
  const safeWidth = Math.max(24, width);
215
- const innerWidth = Math.max(1, safeWidth - 2);
195
+ const innerWidth = safeWidth - 2;
196
+ const runs = this.runs();
197
+ this.runIndex = Math.min(this.runIndex, Math.max(0, runs.length - 1));
198
+ const run = runs[this.runIndex];
216
199
  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));
200
+ return this.renderKillDialog(innerWidth);
201
+ const lines = [this.border("╭", " Actor Inspector ", "╮", innerWidth)];
202
+ lines.push(this.row(this.renderRunControl(run, runs.length - this.runIndex), innerWidth));
203
+ lines.push(this.row(this.renderTabs(run), innerWidth));
204
+ lines.push(this.border("├", "", "┤", innerWidth));
246
205
  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));
206
+ let content = this.renderContent(run, innerWidth);
207
+ if (this.feedback) {
208
+ const color = this.feedback.ok
209
+ ? "success"
210
+ : /^Kill (failed|rejected)/u.test(this.feedback.message)
211
+ ? "error"
212
+ : "warning";
213
+ content = [this.theme.fg(color, ` ${this.feedback.message}`), ...content];
214
+ this.contentStripeIndices = [0, ...this.contentStripeIndices];
215
+ }
216
+ const viewportRows = this.contentViewportRows();
217
+ content = content.slice(0, viewportRows);
218
+ for (let index = 0; index < viewportRows; index += 1) {
219
+ lines.push(this.stripedRow(content[index] ?? "", innerWidth, this.contentStripeIndices[index] ?? index));
282
220
  }
283
221
  lines.push(this.footerBorder(this.renderKeyHints(), innerWidth));
284
222
  return lines;
285
223
  }
286
- invalidate() { }
287
- dispose() {
288
- clearInterval(this.refreshTimer);
224
+ get tab() {
225
+ return TABS[this.tabIndex];
289
226
  }
290
227
  runs() {
291
- return Inspector.readActorInspectorRuns(this.stateRoot, this.ownerId);
292
- }
293
- ensureSelectedRun(runs) {
294
- if (!this.selectedRun)
295
- this.selectedRun = runs[0]?.run;
228
+ if (this.runCache)
229
+ return this.runCache;
230
+ this.runCache = this.readRuns(this.stateRoot, this.ownerId);
231
+ return this.runCache;
296
232
  }
297
233
  contentViewportRows() {
298
- const overlayRows = Math.floor(this.tui.terminal.rows * 0.94);
234
+ const rows = this.tui.terminal?.rows ?? 30;
235
+ const overlayRows = Math.floor(rows * 0.94);
299
236
  return Math.max(4, Math.min(24, overlayRows - 5));
300
237
  }
301
- selectRun(run, index) {
302
- this.selectedRun = run;
303
- this.turnCache = undefined;
304
- this.runIndex = index;
305
- this.recipeScroll = 0;
238
+ resetContentPosition() {
239
+ this.documentScroll = 0;
240
+ this.detailScroll = 0;
306
241
  this.rowIndex = 0;
307
- this.subagentIndex = 0;
308
- this.communicationFrom = "all";
242
+ this.traceDetail = undefined;
309
243
  }
310
244
  cycleRun(runs, direction) {
311
245
  if (runs.length === 0)
312
246
  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);
247
+ this.runIndex = (this.runIndex + direction + runs.length) % runs.length;
248
+ this.resetContentPosition();
249
+ }
250
+ handleScrollableInput(data, target) {
251
+ const key = target === "detail" ? "detailScroll" : "documentScroll";
252
+ if (matchesKey(data, "up"))
253
+ this[key] = Math.max(0, this[key] - 1);
254
+ else if (matchesKey(data, "down"))
255
+ this[key] += 1;
256
+ else if (matchesKey(data, "pageUp")) {
257
+ this[key] = Math.max(0, this[key] - this.contentViewportRows());
258
+ }
259
+ else if (matchesKey(data, "pageDown")) {
260
+ this[key] += this.contentViewportRows();
261
+ }
262
+ else if (matchesKey(data, "left") && target === "detail") {
263
+ this.traceDetail = undefined;
264
+ this.detailScroll = 0;
265
+ this.focus = "list";
266
+ }
267
+ }
268
+ handleSelectorInput(data, runs) {
269
+ const run = runs[this.runIndex];
270
+ const options = this.selectorMode === "run"
271
+ ? runs.map((item) => item.run)
272
+ : this.traceSources(run);
273
+ if (matchesKey(data, "up"))
274
+ this.selectorIndex = Math.max(0, this.selectorIndex - 1);
275
+ else if (matchesKey(data, "down")) {
276
+ this.selectorIndex = Math.min(Math.max(0, options.length - 1), this.selectorIndex + 1);
277
+ }
278
+ else if (matchesKey(data, "left")) {
279
+ this.focus = this.selectorMode === "run" ? "runs" : "tabs";
280
+ this.selectorMode = undefined;
281
+ }
282
+ else if (matchesKey(data, "return") || matchesKey(data, "right")) {
283
+ if (this.selectorMode === "run") {
284
+ this.runIndex = this.selectorIndex;
285
+ this.focus = "runs";
286
+ }
287
+ else {
288
+ this.traceSourceIndex = this.selectorIndex;
289
+ this.focus = "tabs";
290
+ }
291
+ this.selectorMode = undefined;
292
+ this.resetContentPosition();
293
+ }
318
294
  }
319
- requestKill(runs) {
295
+ openSourceSelector(run) {
296
+ const sources = this.traceSources(run);
297
+ this.selectorMode = "source";
298
+ this.selectorIndex = Math.min(this.traceSourceIndex, sources.length - 1);
299
+ this.focus = "select";
300
+ }
301
+ cycleTraceSource(run) {
302
+ const sources = this.traceSources(run);
303
+ this.traceSourceIndex = (this.traceSourceIndex + 1) % sources.length;
304
+ this.resetContentPosition();
305
+ }
306
+ requestKill(run) {
320
307
  this.feedback = undefined;
321
308
  if (this.focus !== "runs") {
322
309
  this.feedback = { ok: false, message: "Focus Run before using Kill." };
323
310
  return;
324
311
  }
325
- const run = runs.find((item) => item.run === this.selectedRun);
326
312
  if (!run) {
327
313
  this.feedback = { ok: false, message: "Kill unavailable: no owned run." };
328
314
  return;
@@ -332,17 +318,11 @@ export class ActorInspectorOverlay {
332
318
  return;
333
319
  }
334
320
  if (run.status !== "running") {
335
- this.feedback = {
336
- ok: false,
337
- message: `Kill unavailable: run is ${run.status}.`,
338
- };
321
+ this.feedback = { ok: false, message: `Kill unavailable: run is ${run.status}.` };
339
322
  return;
340
323
  }
341
324
  if (!run.runInstanceId) {
342
- this.feedback = {
343
- ok: false,
344
- message: "Kill unavailable: run generation unavailable.",
345
- };
325
+ this.feedback = { ok: false, message: "Kill unavailable: run generation unavailable." };
346
326
  return;
347
327
  }
348
328
  this.killConfirmation = {
@@ -352,19 +332,34 @@ export class ActorInspectorOverlay {
352
332
  };
353
333
  this.killDialogChoice = "cancel";
354
334
  }
355
- cancelKill() {
356
- this.feedback = { ok: false, message: "Kill cancelled." };
357
- this.killConfirmation = undefined;
358
- this.killDialogChoice = "cancel";
335
+ handleKillInput(data) {
336
+ const key = data.toLowerCase();
337
+ if (matchesKey(data, "escape") || key === "n") {
338
+ this.feedback = { ok: false, message: "Kill cancelled." };
339
+ this.killConfirmation = undefined;
340
+ this.killDialogChoice = "cancel";
341
+ }
342
+ else if (matchesKey(data, "left") || matchesKey(data, "right") || matchesKey(data, "tab")) {
343
+ this.killDialogChoice = this.killDialogChoice === "cancel" ? "kill" : "cancel";
344
+ }
345
+ else if (key === "y") {
346
+ this.confirmKill();
347
+ }
348
+ else if (matchesKey(data, "return")) {
349
+ if (this.killDialogChoice === "kill")
350
+ this.confirmKill();
351
+ else {
352
+ this.feedback = { ok: false, message: "Kill cancelled." };
353
+ this.killConfirmation = undefined;
354
+ }
355
+ }
359
356
  }
360
357
  confirmKill() {
361
358
  const confirmation = this.killConfirmation;
362
359
  this.killConfirmation = undefined;
363
360
  this.killDialogChoice = "cancel";
364
- if (!confirmation || !this.killRun) {
365
- this.feedback = { ok: false, message: "Kill unavailable in this Inspector." };
361
+ if (!confirmation || !this.killRun)
366
362
  return;
367
- }
368
363
  try {
369
364
  this.feedback = this.killRun(confirmation.run, confirmation.runInstanceId);
370
365
  }
@@ -376,14 +371,13 @@ export class ActorInspectorOverlay {
376
371
  };
377
372
  }
378
373
  }
379
- renderKillDialog(_width, innerWidth) {
374
+ renderKillDialog(innerWidth) {
380
375
  const confirmation = this.killConfirmation;
381
- const totalRows = this.contentViewportRows() + 5;
382
376
  const cancel = this.killDialogChoice === "cancel"
383
- ? this.theme.bg("selectedBg", this.theme.fg("accent", " Cancel "))
377
+ ? this.theme.bg("customMessageBg", this.theme.fg("accent", " Cancel "))
384
378
  : this.theme.fg("muted", " Cancel ");
385
379
  const kill = this.killDialogChoice === "kill"
386
- ? this.theme.bg("selectedBg", this.theme.fg("error", " Kill actor "))
380
+ ? this.theme.bg("customMessageBg", this.theme.fg("error", " Kill actor "))
387
381
  : this.theme.fg("error", " Kill actor ");
388
382
  const body = [
389
383
  "",
@@ -392,686 +386,356 @@ export class ActorInspectorOverlay {
392
386
  `${this.theme.fg("muted", "Run:")} ${this.theme.fg("accent", `run:${confirmation.run}`)}`,
393
387
  `${this.theme.fg("muted", "Current status:")} ${this.theme.fg("warning", confirmation.status)}`,
394
388
  "",
395
- this.theme.fg("text", "This sends canonical control.kill."),
389
+ this.theme.fg("text", "This sends canonical kill Control."),
396
390
  this.theme.fg("error", "The action is destructive and cannot be undone."),
397
391
  "",
398
392
  `${cancel} ${kill}`,
399
393
  "",
400
394
  ];
401
395
  const lines = [this.border("╭", " Confirm Actor Kill ", "╮", innerWidth)];
402
- const availableRows = Math.max(1, totalRows - 2);
396
+ const availableRows = this.contentViewportRows() + 3;
403
397
  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)
398
+ const rows = [...Array.from({ length: topPadding }, () => ""), ...body].slice(0, availableRows);
399
+ while (rows.length < availableRows)
400
+ rows.push("");
401
+ for (const line of rows)
411
402
  lines.push(this.row(this.center(line, innerWidth), innerWidth));
412
403
  lines.push(this.footerBorder(this.renderKeyHints(), innerWidth));
413
404
  return lines;
414
405
  }
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");
406
+ renderRunControl(run, sequence) {
407
+ const active = this.focus === "runs" || (this.focus === "select" && this.selectorMode === "run");
471
408
  const prefix = active ? this.theme.fg("accent", " ← ") : " ";
472
409
  const suffix = active ? this.theme.fg("accent", " → ") : " ";
473
410
  if (!run) {
474
411
  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} `;
412
+ return this.focus === "runs" ? this.theme.bg("customMessageBg", empty) : empty;
413
+ }
414
+ 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}`;
415
+ return this.focus === "runs" ? this.theme.bg("customMessageBg", value) : value;
416
+ }
417
+ renderTabs(run) {
418
+ const source = this.traceSource(run);
419
+ return TABS.map((tab, index) => {
420
+ const base = `${tab[0].toUpperCase()}${tab.slice(1)}`;
421
+ const label = tab === "trace" && source !== "all" ? `${base} (${source})` : base;
422
+ const selected = index === this.tabIndex;
423
+ const display = selected && this.focus !== "runs" ? `[ ${label} ]` : ` ${label} `;
502
424
  const value = ` ${display} `;
503
425
  if (!selected)
504
426
  return this.theme.fg("muted", value);
505
427
  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)}`;
428
+ return this.focus === "tabs" ? this.theme.bg("customMessageBg", colored) : colored;
429
+ }).join(" ");
513
430
  }
514
- renderRunSelector(width) {
515
- const runs = this.runs();
516
- if (runs.length === 0)
431
+ renderContent(run, width) {
432
+ if (!run)
517
433
  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
- });
434
+ if (this.focus === "select")
435
+ return this.renderSelector(run);
436
+ if (this.focus === "detail")
437
+ return this.renderTraceDetail(width);
438
+ if (this.tab === "trace")
439
+ return this.renderTraceList(run);
440
+ const stateDir = path.join(this.stateRoot, run.run);
441
+ const value = this.tab === "recipe"
442
+ ? ActorInspector.readActorInspectorRecipe(stateDir)
443
+ : this.controlDocument(run, stateDir);
444
+ const projection = this.documentProjection(value, width);
445
+ const maxStart = Math.max(0, projection.lines.length - this.contentViewportRows());
446
+ this.documentScroll = Math.min(this.documentScroll, maxStart);
447
+ this.contentStripeIndices = projection.stripes.slice(this.documentScroll, this.documentScroll + this.contentViewportRows());
448
+ return projection.lines.slice(this.documentScroll, this.documentScroll + this.contentViewportRows());
449
+ }
450
+ renderSelector(run) {
451
+ const options = this.selectorMode === "run"
452
+ ? this.runs().map((item) => `run:${item.run} ${item.status}`)
453
+ : this.traceSources(run).map((source) => `Trace source: ${source}`);
454
+ const lines = this.renderMenuBox(options, this.selectorIndex);
455
+ this.contentStripeIndices = lines.map(() => 0);
456
+ return lines;
528
457
  }
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")];
536
- }
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
- ];
554
- }
555
- this.rowIndex = Math.min(this.rowIndex, rows.length - 1);
458
+ renderMenuBox(options, focusedIndex) {
459
+ if (options.length === 0)
460
+ return [this.theme.fg("muted", " No options")];
461
+ const visibleLimit = Math.max(1, Math.min(this.contentViewportRows(), options.length));
462
+ const maxStart = Math.max(0, options.length - visibleLimit);
463
+ const start = Math.max(0, Math.min(focusedIndex - Math.floor(visibleLimit / 2), maxStart));
464
+ const visible = options.slice(start, start + visibleLimit);
465
+ const width = Math.max(8, ...options.map((option) => visibleWidth(option) + 4));
466
+ const border = (left, right, marker = "") => this.theme.fg("borderAccent", `${left}${marker}${"─".repeat(Math.max(0, width - visibleWidth(marker)))}${right}`);
467
+ return [
468
+ border("╭", "╮", start > 0 ? "↑" : ""),
469
+ ...visible.map((option, offset) => {
470
+ const index = start + offset;
471
+ const row = this.fit(`${index === focusedIndex ? " ▶ " : " "}${option}`, width);
472
+ const colored = index === focusedIndex ? this.theme.fg("accent", row) : row;
473
+ const styled = index === focusedIndex ? this.theme.bg("customMessageBg", colored) : colored;
474
+ return `${this.theme.fg("borderAccent", "│")}${styled}${this.theme.fg("borderAccent", "│")}`;
475
+ }),
476
+ border("╰", "╯", start + visible.length < options.length ? "↓" : ""),
477
+ ];
478
+ }
479
+ renderTraceList(run) {
480
+ const items = this.traceItems(run);
481
+ if (items.length === 0)
482
+ return [this.theme.fg("muted", " No Trace evidence")];
483
+ this.rowIndex = Math.min(this.rowIndex, items.length - 1);
556
484
  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) => {
485
+ const maxStart = Math.max(0, items.length - viewportRows);
486
+ const start = Math.max(0, Math.min(this.rowIndex - Math.floor(viewportRows / 2), maxStart));
487
+ this.contentStripeIndices = items
488
+ .slice(start, start + viewportRows)
489
+ .map((_item, offset) => start + offset);
490
+ return items.slice(start, start + viewportRows).map((item, offset) => {
564
491
  const index = start + offset;
565
- const selected = this.focus === "list" && index === this.rowIndex;
566
- return selected
567
- ? this.theme.fg("accent", ` ▶ ${row}`)
568
- : ` ${row}`;
492
+ const detail = item.detail && typeof item.detail === "object"
493
+ ? item.detail
494
+ : {};
495
+ const marker = item.level === "error"
496
+ ? this.theme.fg("error", "!")
497
+ : detail.attention
498
+ ? this.theme.fg("warning", "•")
499
+ : this.theme.fg("muted", "·");
500
+ const prefix = this.focus === "list" && index === this.rowIndex
501
+ ? this.theme.fg("accent", " ▶ ")
502
+ : " ";
503
+ const row = `${prefix}${this.theme.fg("text", `#${index + 1}`)} ${marker} ${this.theme.fg("muted", item.source)}/${this.theme.fg(item.level === "error" ? "error" : "accent", item.kind)} ${this.theme.fg("text", item.summary)}`;
504
+ return this.focus === "list" && index === this.rowIndex
505
+ ? this.theme.fg("accent", row)
506
+ : row;
569
507
  });
570
508
  }
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);
509
+ renderTraceDetail(width) {
510
+ if (!this.traceDetail)
511
+ return [this.theme.fg("warning", " Trace row is no longer available")];
512
+ const projection = this.documentProjection(this.traceDetail, width);
513
+ const maxStart = Math.max(0, projection.lines.length - this.contentViewportRows());
514
+ this.detailScroll = Math.min(this.detailScroll, maxStart);
515
+ this.contentStripeIndices = projection.stripes.slice(this.detailScroll, this.detailScroll + this.contentViewportRows());
516
+ return projection.lines.slice(this.detailScroll, this.detailScroll + this.contentViewportRows());
517
+ }
518
+ controlDocument(run, stateDir) {
519
+ const endpoint = run.runInstanceId
520
+ ? RunControlDelivery.readRunControlEndpoint(stateDir, run.runInstanceId)
521
+ : undefined;
522
+ const meta = this.readRunMeta(stateDir);
523
+ return {
524
+ status: run.status,
525
+ run_instance_id: run.runInstanceId,
526
+ actor_actions: Array.isArray(meta.control) ? meta.control : [],
527
+ runtime_actions: run.status === "running" ? ["kill"] : ["archive", "prune"],
528
+ endpoint,
529
+ recent_controls: RunsControls.readRunControlsFromStateDir(stateDir).slice(-20).reverse(),
530
+ };
592
531
  }
593
- communicationPreviews() {
594
- if (!this.selectedRun)
532
+ allTraceItems(run) {
533
+ if (!run)
595
534
  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)];
623
- }
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}`;
535
+ if (this.traceCache?.run === run.run &&
536
+ this.traceCache.runInstanceId === run.runInstanceId) {
537
+ return this.traceCache.items;
538
+ }
539
+ const items = this.readTrace(path.join(this.stateRoot, run.run), {
540
+ limit: 100,
541
+ source: "all",
630
542
  });
543
+ this.traceCache = { run: run.run, runInstanceId: run.runInstanceId, items };
544
+ return items;
631
545
  }
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);
546
+ traceSources(run) {
547
+ const present = new Set(this.allTraceItems(run).map((item) => item.source));
548
+ return TRACE_SOURCES.filter((source) => source === "all" || present.has(source));
667
549
  }
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
- }
715
- }
716
- }
717
- return { lines: wrappedLines, stripeIndices };
550
+ traceSource(run) {
551
+ const sources = this.traceSources(run);
552
+ this.traceSourceIndex %= sources.length;
553
+ return sources[this.traceSourceIndex] ?? "all";
554
+ }
555
+ traceItems(run) {
556
+ const source = this.traceSource(run);
557
+ const items = this.allTraceItems(run);
558
+ return source === "all" ? items : items.filter((item) => item.source === source);
718
559
  }
719
- wrapPreferred(content, width) {
560
+ readRunMeta(stateDir) {
561
+ try {
562
+ return JSON.parse(readFileSync(path.join(stateDir, "run.json"), "utf8"));
563
+ }
564
+ catch {
565
+ return {};
566
+ }
567
+ }
568
+ documentProjection(value, width) {
569
+ const sections = value && typeof value === "object" && !Array.isArray(value)
570
+ ? Object.entries(value).map(([key, item]) => {
571
+ const inline = this.inlineDocumentValue(item);
572
+ return inline !== undefined
573
+ ? [`${key}: ${inline}`]
574
+ : item && typeof item === "object"
575
+ ? [`${key}:`, ...this.document(item, 1)]
576
+ : this.labeledScalarLines(key, item);
577
+ })
578
+ : [this.document(value)];
720
579
  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
- }
580
+ const stripes = [];
581
+ sections.forEach((section, sectionIndex) => {
582
+ const wrapped = this.wrapDocument(section, width);
583
+ lines.push(...wrapped.map((line) => this.styleDocumentLine(line)));
584
+ stripes.push(...wrapped.map(() => sectionIndex));
585
+ });
586
+ return { lines, stripes };
587
+ }
588
+ inlineDocumentValue(value) {
589
+ if (Array.isArray(value)) {
590
+ if (value.length === 0)
591
+ return "[]";
592
+ if (value.every((item) => (item === null || typeof item !== "object") &&
593
+ !(typeof item === "string" && /[\r\n]/u.test(item)))) {
594
+ return `[${value.map((item) => String(item ?? "none")).join(", ")}]`;
735
595
  }
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;
596
+ return undefined;
597
+ }
598
+ if (value && typeof value === "object") {
599
+ const entries = Object.entries(value);
600
+ if (entries.length === 0)
601
+ return "{}";
602
+ if (entries.length <= 3 &&
603
+ entries.every(([, item]) => (item === null || typeof item !== "object") &&
604
+ !(typeof item === "string" && /[\r\n]/u.test(item)))) {
605
+ return `{ ${entries
606
+ .map(([key, item]) => `${key}: ${String(item ?? "none")}`)
607
+ .join(", ")} }`;
743
608
  }
744
- lines.push(head);
745
- remaining = remaining.slice(splitAt);
746
- if (trimBoundary)
747
- remaining = remaining.trimStart();
748
609
  }
749
- if (remaining)
750
- lines.push(remaining);
751
- return lines;
610
+ return undefined;
752
611
  }
753
- detailSection(title, content) {
754
- if (content.length === 0)
755
- return [];
612
+ labeledScalarLines(key, value, depth = 0) {
613
+ const indent = " ".repeat(depth);
614
+ const [first = "", ...rest] = String(value ?? "none").split(/\r?\n/u);
756
615
  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,
616
+ `${indent}${key}: ${first}`,
617
+ ...rest.map((line) => `${indent} ${line}`),
763
618
  ];
764
619
  }
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
- }
620
+ document(value, depth = 0) {
780
621
  if (value === null || value === undefined)
781
- return [`${indent}${String(value)}`];
782
- if (typeof value !== "object")
783
- return [`${indent}${String(value)}`];
622
+ return [`${" ".repeat(depth)}none`];
623
+ const inline = this.inlineDocumentValue(value);
624
+ if (inline !== undefined)
625
+ return [`${" ".repeat(depth)}${inline}`];
784
626
  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);
627
+ return value.flatMap((item, index) => {
628
+ const marker = `${" ".repeat(depth)}- #${index + 1}`;
629
+ const itemInline = this.inlineDocumentValue(item);
630
+ return itemInline !== undefined
631
+ ? [`${marker}: ${itemInline}`]
632
+ : [marker, ...this.document(item, depth + 1)];
633
+ });
634
+ }
635
+ if (typeof value === "object") {
636
+ return Object.entries(value).flatMap(([key, item]) => {
637
+ const itemInline = this.inlineDocumentValue(item);
638
+ if (itemInline !== undefined) {
639
+ return [`${" ".repeat(depth)}${key}: ${itemInline}`];
793
640
  }
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
- ];
641
+ if (item && typeof item === "object") {
642
+ return [`${" ".repeat(depth)}${key}:`, ...this.document(item, depth + 1)];
643
+ }
644
+ return this.labeledScalarLines(key, item, depth);
801
645
  });
802
646
  }
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
- ];
647
+ return String(value)
648
+ .split(/\r?\n/u)
649
+ .map((line) => `${" ".repeat(depth)}${line}`);
650
+ }
651
+ wrapDocument(lines, width) {
652
+ return lines.flatMap((line) => {
653
+ const normalized = line.replaceAll("\t", " ");
654
+ if (visibleWidth(normalized) <= width)
655
+ return [normalized];
656
+ const leading = normalized.match(/^ */u)?.[0] ?? "";
657
+ const continuation = `${leading} `;
658
+ const wrapped = [];
659
+ let rest = normalized.slice(leading.length);
660
+ let prefix = leading;
661
+ while (rest && visibleWidth(`${prefix}${rest}`) > width) {
662
+ const available = Math.max(1, width - visibleWidth(prefix));
663
+ const candidate = this.visiblePrefix(rest, available);
664
+ const minimumBreak = Math.floor(candidate.length * 0.6);
665
+ const preferred = Math.max(candidate.lastIndexOf(" "), candidate.lastIndexOf("/"), candidate.lastIndexOf(","));
666
+ const cut = preferred >= minimumBreak ? preferred + 1 : candidate.length;
667
+ wrapped.push(`${prefix}${rest.slice(0, cut).trimEnd()}`);
668
+ rest = rest.slice(cut).trimStart();
669
+ prefix = continuation;
814
670
  }
815
- return [
816
- `${indent}${key}:`,
817
- ...this.readableValueLines(item, `${indent} `, depth + 1),
818
- ];
671
+ if (rest)
672
+ wrapped.push(`${prefix}${rest}`);
673
+ return wrapped;
819
674
  });
820
675
  }
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}`,
846
- ];
847
- lines.push(...this.detailSection(`Tool ${index + 1} (${tool.name}${tool.resultError ? ", Error" : ""})`, content));
676
+ styleDocumentLine(line) {
677
+ const indent = line.match(/^ */u)?.[0] ?? "";
678
+ const content = line.slice(indent.length);
679
+ if (/^- #\d+$/u.test(content) || content.endsWith(":")) {
680
+ return `${indent}${this.theme.fg("accent", content)}`;
848
681
  }
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}`;
924
- });
925
- }
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;
682
+ const labeled = /^([^:]+:)(.*)$/u.exec(content);
683
+ if (labeled) {
684
+ return `${indent}${this.theme.fg("muted", labeled[1])}${this.theme.fg("text", labeled[2])}`;
958
685
  }
959
- this.focus = this.menuLevel === "run" ? "runs" : "tabs";
686
+ return `${indent}${this.theme.fg(content === "none" || content === "[]" ? "muted" : "text", content)}`;
960
687
  }
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";
966
- }
967
- openFilterMenu() {
968
- this.selectorIndex = this.filterControlIndex;
969
- this.menuLevel = "filter";
970
- this.focus = "select";
688
+ visiblePrefix(value, width) {
689
+ let consumed = 0;
690
+ let result = "";
691
+ for (const character of value) {
692
+ const characterWidth = visibleWidth(character);
693
+ if (consumed + characterWidth > width)
694
+ break;
695
+ result += character;
696
+ consumed += characterWidth;
697
+ }
698
+ return result;
971
699
  }
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;
700
+ renderKeyHints() {
701
+ const hint = (keys, description) => `${this.theme.fg("accent", keys)}${this.theme.fg("borderAccent", ` ${description}`)}`;
702
+ const divider = this.theme.fg("borderAccent", " ─ ");
703
+ const hints = (...items) => items.map(([keys, description]) => hint(keys, description)).join(divider);
704
+ if (this.killConfirmation) {
705
+ return hints(["←→/tab", "choose"], ["enter/y", "confirm"], ["esc/n", "cancel"]);
979
706
  }
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;
707
+ if (this.focus === "select") {
708
+ return hints(["↑↓", "option"], ["enter/→", "apply"], ["←/esc", "back"]);
987
709
  }
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));
710
+ if (this.focus === "detail") {
711
+ return hints(["↑↓/pgup/pgdn", "scroll"], ["←/esc", "back"]);
1058
712
  }
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();
713
+ if (this.focus === "document") {
714
+ return hints(["↑↓/pgup/pgdn", "scroll"], ["↑ at top", "tabs"], ["←/esc", "tabs"]);
715
+ }
716
+ if (this.focus === "list") {
717
+ return hints(["↑↓/pgup/pgdn", "row"], ["→/enter", "open"], ["←", "tabs"], ["esc", "close"]);
718
+ }
719
+ if (this.focus === "runs") {
720
+ const items = [["←→", "run"], ["↓", "tabs"], ["enter", "list"]];
721
+ const run = this.runs()[this.runIndex];
722
+ if (this.killRun && run?.status === "running" && run.runInstanceId)
723
+ items.push(["k", "kill"]);
724
+ items.push(["esc", "close"]);
725
+ return hints(...items);
726
+ }
727
+ return this.tab === "trace"
728
+ ? hints(["←→", "tabs"], ["↑↓", "focus"], ["enter/f", "source"], ["esc", "close"])
729
+ : hints(["←→", "tabs"], ["↑↓", "focus"], ["enter", "open"], ["esc", "close"]);
1068
730
  }
1069
- closeDetail() {
1070
- this.detailOpen = false;
1071
- this.detailCommunication = undefined;
1072
- this.detailTurn = undefined;
1073
- this.detailScroll = 0;
1074
- this.focus = "list";
731
+ statusColor(status) {
732
+ if (status === "failed")
733
+ return "error";
734
+ if (status === "running")
735
+ return "warning";
736
+ if (status === "done" || status === "terminal")
737
+ return "success";
738
+ return "muted";
1075
739
  }
1076
740
  footerBorder(hints, width) {
1077
741
  const prefix = " ";
@@ -1080,63 +744,26 @@ export class ActorInspectorOverlay {
1080
744
  const fill = "─".repeat(Math.max(0, width - visibleWidth(prefix) - visibleWidth(suffix) - visibleWidth(content)));
1081
745
  return `${this.theme.fg("borderAccent", `╰${prefix}`)}${content}${this.theme.fg("borderAccent", `${suffix}${fill}╯`)}`;
1082
746
  }
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
747
  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("");
748
+ return index % 2 === 0 ? content : this.theme.bg("customMessageBg", content);
1095
749
  }
1096
750
  stripedRow(content, width, index) {
1097
- return this.row(this.stripeBackground(this.fit(content, width), index), width, true);
751
+ const fitted = this.fit(content, width);
752
+ return `${this.theme.fg("borderAccent", "│")}${this.stripeBackground(fitted, index)}${this.theme.fg("borderAccent", "│")}`;
1098
753
  }
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", "│")}`;
754
+ row(content, width) {
755
+ return `${this.theme.fg("borderAccent", "│")}${this.fit(content, width)}${this.theme.fg("borderAccent", "│")}`;
1102
756
  }
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);
1127
- }
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}`;
757
+ border(left, label, right, width) {
758
+ const bounded = truncateToWidth(label, width, "");
759
+ return this.theme.fg("borderAccent", `${left}${bounded}${"─".repeat(Math.max(0, width - visibleWidth(bounded)))}${right}`);
1132
760
  }
1133
761
  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, "…");
762
+ const bounded = truncateToWidth(content, width, "");
1140
763
  return `${bounded}${" ".repeat(Math.max(0, width - visibleWidth(bounded)))}`;
1141
764
  }
765
+ center(content, width) {
766
+ const padding = Math.max(0, Math.floor((width - visibleWidth(content)) / 2));
767
+ return `${" ".repeat(padding)}${content}`;
768
+ }
1142
769
  }