@llblab/pi-actors 0.42.3 → 0.43.0

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