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