@llblab/pi-actors 0.40.1 → 0.41.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -3
- package/CHANGELOG.md +32 -0
- package/README.md +2 -1
- package/dist/index.js +80 -58
- package/dist/lib/inspector-overlay.d.ts +95 -0
- package/dist/lib/inspector-overlay.js +873 -0
- package/dist/lib/inspector.d.ts +17 -33
- package/dist/lib/inspector.js +101 -151
- package/dist/lib/limits.d.ts +3 -0
- package/dist/lib/limits.js +3 -0
- package/dist/lib/observability.d.ts +38 -3
- package/dist/lib/observability.js +151 -33
- package/dist/lib/recipes-context.d.ts +5 -0
- package/dist/lib/recipes-context.js +15 -0
- package/dist/lib/session-evidence.d.ts +45 -0
- package/dist/lib/session-evidence.js +204 -0
- package/dist/lib/tools-response.d.ts +3 -0
- package/dist/lib/tools-response.js +29 -0
- package/dist/scripts/async-runner.mjs +40 -5
- package/dist/scripts/coordinator.mjs +55 -9
- package/dist/skills/actors/SKILL.md +5 -11
- package/dist/skills/swarm/SKILL.md +1 -1
- package/docs/README.md +1 -0
- package/docs/actor-inspector.md +77 -0
- package/docs/async-runs.md +1 -1
- package/index.ts +80 -70
- package/lib/inspector-overlay.ts +1071 -0
- package/lib/inspector.ts +135 -204
- package/lib/limits.ts +3 -0
- package/lib/observability.ts +205 -39
- package/lib/recipes-context.ts +25 -0
- package/lib/session-evidence.ts +302 -0
- package/lib/tools-response.ts +32 -0
- package/package.json +1 -1
- package/scripts/async-runner.mjs +40 -5
- package/scripts/coordinator.mjs +55 -9
- package/skills/actors/SKILL.md +5 -11
- package/skills/swarm/SKILL.md +1 -1
|
@@ -0,0 +1,1071 @@
|
|
|
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.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import { stripVTControlCharacters } from "node:util";
|
|
9
|
+
|
|
10
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import {
|
|
12
|
+
matchesKey,
|
|
13
|
+
truncateToWidth,
|
|
14
|
+
visibleWidth,
|
|
15
|
+
wrapTextWithAnsi,
|
|
16
|
+
type TUI,
|
|
17
|
+
} from "@earendil-works/pi-tui";
|
|
18
|
+
|
|
19
|
+
import * as Inspector from "./inspector.ts";
|
|
20
|
+
|
|
21
|
+
export type ActorInspectorOverlayTab = "communications" | "turns";
|
|
22
|
+
|
|
23
|
+
type DetailLogicalLine = string | { sectionBreak: true };
|
|
24
|
+
|
|
25
|
+
const DETAIL_SECTION_BREAK: DetailLogicalLine = { sectionBreak: true };
|
|
26
|
+
|
|
27
|
+
export interface ActorInspectorOverlayOptions {
|
|
28
|
+
done: () => void;
|
|
29
|
+
ownerId: string;
|
|
30
|
+
stateRoot: string;
|
|
31
|
+
theme: Theme;
|
|
32
|
+
tui: TUI;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class ActorInspectorOverlay {
|
|
36
|
+
private readonly done: () => void;
|
|
37
|
+
private readonly ownerId: string;
|
|
38
|
+
private readonly stateRoot: string;
|
|
39
|
+
private readonly theme: Theme;
|
|
40
|
+
private readonly tui: TUI;
|
|
41
|
+
private readonly refreshTimer: NodeJS.Timeout;
|
|
42
|
+
private runIndex = 0;
|
|
43
|
+
private communicationChannel: "all" | "broadcast" | "direct" | "room" = "all";
|
|
44
|
+
private communicationFrom = "all";
|
|
45
|
+
private communicationUnread = false;
|
|
46
|
+
private contentStripeIndices: number[] = [];
|
|
47
|
+
private detailCommunication?: Inspector.ActorInspectorPreview;
|
|
48
|
+
private detailOpen = false;
|
|
49
|
+
private detailScroll = 0;
|
|
50
|
+
private detailTurn?: Inspector.ActorInspectorTurnItem;
|
|
51
|
+
private detailView: "evidence" | "transcript" = "evidence";
|
|
52
|
+
private readonly readKeys = new Set<string>();
|
|
53
|
+
private focus: "runs" | "tabs" | "list" | "detail" | "select" = "tabs";
|
|
54
|
+
private filterControlIndex = 0;
|
|
55
|
+
private menuLevel: "run" | "filter" | "value" = "filter";
|
|
56
|
+
private selectorIndex = 0;
|
|
57
|
+
private rowIndex = 0;
|
|
58
|
+
private selectedRun?: string;
|
|
59
|
+
private subagentIndex = 0;
|
|
60
|
+
private tab: ActorInspectorOverlayTab = "communications";
|
|
61
|
+
|
|
62
|
+
constructor(options: ActorInspectorOverlayOptions) {
|
|
63
|
+
this.done = options.done;
|
|
64
|
+
this.ownerId = options.ownerId;
|
|
65
|
+
this.stateRoot = options.stateRoot;
|
|
66
|
+
this.theme = options.theme;
|
|
67
|
+
this.tui = options.tui;
|
|
68
|
+
this.refreshTimer = setInterval(() => this.tui.requestRender(), 1000);
|
|
69
|
+
this.refreshTimer.unref?.();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
handleInput(data: string): void {
|
|
73
|
+
const runs = this.runs();
|
|
74
|
+
this.ensureSelectedRun(runs);
|
|
75
|
+
if (matchesKey(data, "ctrl+c")) {
|
|
76
|
+
this.done();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (matchesKey(data, "escape")) {
|
|
80
|
+
if (this.focus === "select") {
|
|
81
|
+
if (this.menuLevel === "value") {
|
|
82
|
+
this.menuLevel = "filter";
|
|
83
|
+
this.selectorIndex = this.filterControlIndex;
|
|
84
|
+
} else this.focus = this.menuLevel === "run" ? "runs" : "tabs";
|
|
85
|
+
} else if (this.focus === "detail") this.backDetail();
|
|
86
|
+
else this.done();
|
|
87
|
+
this.tui.requestRender();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (this.focus === "select") {
|
|
91
|
+
const options = this.menuOptions();
|
|
92
|
+
if (matchesKey(data, "up"))
|
|
93
|
+
this.selectorIndex = Math.max(0, this.selectorIndex - 1);
|
|
94
|
+
else if (matchesKey(data, "down"))
|
|
95
|
+
this.selectorIndex = Math.min(options.length - 1, this.selectorIndex + 1);
|
|
96
|
+
else if (matchesKey(data, "left")) this.backMenu();
|
|
97
|
+
else if (
|
|
98
|
+
matchesKey(data, "return") ||
|
|
99
|
+
(matchesKey(data, "right") && this.menuLevel !== "value")
|
|
100
|
+
)
|
|
101
|
+
this.applyMenuOption(options[this.selectorIndex]);
|
|
102
|
+
this.tui.requestRender();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (this.focus === "detail") {
|
|
106
|
+
if (matchesKey(data, "up")) this.detailScroll = Math.max(0, this.detailScroll - 1);
|
|
107
|
+
else if (matchesKey(data, "down")) this.detailScroll += 1;
|
|
108
|
+
else if (matchesKey(data, "left")) this.backDetail();
|
|
109
|
+
else if (
|
|
110
|
+
(matchesKey(data, "right") || matchesKey(data, "return")) &&
|
|
111
|
+
this.detailView === "evidence" &&
|
|
112
|
+
this.tab === "turns"
|
|
113
|
+
) {
|
|
114
|
+
this.detailView = "transcript";
|
|
115
|
+
this.detailScroll = 0;
|
|
116
|
+
}
|
|
117
|
+
this.tui.requestRender();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (this.focus === "runs") {
|
|
121
|
+
if (matchesKey(data, "left")) this.cycleRun(runs, -1);
|
|
122
|
+
else if (matchesKey(data, "right")) this.cycleRun(runs, 1);
|
|
123
|
+
else if (matchesKey(data, "down")) this.focus = "tabs";
|
|
124
|
+
else if (matchesKey(data, "return") && runs.length > 0) this.openRunMenu();
|
|
125
|
+
} else if (this.focus === "tabs") {
|
|
126
|
+
if (matchesKey(data, "left") || matchesKey(data, "right")) {
|
|
127
|
+
this.tab = this.tab === "communications" ? "turns" : "communications";
|
|
128
|
+
this.filterControlIndex = 0;
|
|
129
|
+
this.rowIndex = 0;
|
|
130
|
+
} else if (matchesKey(data, "up")) this.focus = "runs";
|
|
131
|
+
else if (matchesKey(data, "down") && this.listItemCount() > 0)
|
|
132
|
+
this.focus = "list";
|
|
133
|
+
else if (matchesKey(data, "return")) this.openFilterMenu();
|
|
134
|
+
} else if (this.focus === "list") {
|
|
135
|
+
const count = this.tab === "communications" ? this.communicationPreviews().length : this.turnItems().length;
|
|
136
|
+
if (matchesKey(data, "up")) {
|
|
137
|
+
if (this.rowIndex === 0) this.focus = "tabs";
|
|
138
|
+
else this.rowIndex -= 1;
|
|
139
|
+
} else if (matchesKey(data, "down"))
|
|
140
|
+
this.rowIndex = Math.min(Math.max(0, count - 1), this.rowIndex + 1);
|
|
141
|
+
else if (matchesKey(data, "return") || matchesKey(data, "right"))
|
|
142
|
+
this.openDetail();
|
|
143
|
+
}
|
|
144
|
+
this.tui.requestRender();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
render(width: number): string[] {
|
|
148
|
+
this.ensureSelectedRun(this.runs());
|
|
149
|
+
const safeWidth = Math.max(24, width);
|
|
150
|
+
const innerWidth = Math.max(1, safeWidth - 2);
|
|
151
|
+
const lines: string[] = [];
|
|
152
|
+
lines.push(this.border("╭", " Actor Inspector ", "╮", innerWidth));
|
|
153
|
+
lines.push(this.row(this.renderRunControl(), innerWidth));
|
|
154
|
+
let selector = this.focus === "select"
|
|
155
|
+
? this.menuLevel === "run"
|
|
156
|
+
? this.renderMenuBox(
|
|
157
|
+
this.runs().map(
|
|
158
|
+
(run, index, runs) =>
|
|
159
|
+
`#${runs.length - index} ${run.run} ${run.status}`,
|
|
160
|
+
),
|
|
161
|
+
this.selectorIndex,
|
|
162
|
+
undefined,
|
|
163
|
+
undefined,
|
|
164
|
+
false,
|
|
165
|
+
false,
|
|
166
|
+
this.contentViewportRows(),
|
|
167
|
+
)
|
|
168
|
+
: this.renderFilterMenus(innerWidth)
|
|
169
|
+
: [];
|
|
170
|
+
const selectorAnchor = this.selectorAnchor();
|
|
171
|
+
if (this.menuLevel === "run" && selector.length > 0) {
|
|
172
|
+
const popup = selector[0];
|
|
173
|
+
const popupWidth = visibleWidth(popup);
|
|
174
|
+
const leading = this.fit(
|
|
175
|
+
this.takeVisiblePrefix(this.renderTabs(), selectorAnchor),
|
|
176
|
+
selectorAnchor,
|
|
177
|
+
);
|
|
178
|
+
const trailingWidth = Math.max(0, innerWidth - selectorAnchor - popupWidth);
|
|
179
|
+
const trailing = this.fit(
|
|
180
|
+
this.dropVisiblePrefix(this.renderTabs(), selectorAnchor + popupWidth),
|
|
181
|
+
trailingWidth,
|
|
182
|
+
);
|
|
183
|
+
lines.push(this.row(`${leading}${popup}${trailing}`, innerWidth, true));
|
|
184
|
+
selector = selector.slice(1);
|
|
185
|
+
} else lines.push(this.row(this.renderTabs(), innerWidth));
|
|
186
|
+
const selectorTop = selector[0];
|
|
187
|
+
if (selectorTop) {
|
|
188
|
+
const leadingBorder = "─".repeat(selectorAnchor);
|
|
189
|
+
const remainingBorder = "─".repeat(
|
|
190
|
+
Math.max(0, innerWidth - visibleWidth(selectorTop) - selectorAnchor),
|
|
191
|
+
);
|
|
192
|
+
lines.push(
|
|
193
|
+
`${this.theme.fg("border", `├${leadingBorder}`)}${selectorTop}${this.theme.fg("border", `${remainingBorder}┤`)}`,
|
|
194
|
+
);
|
|
195
|
+
} else lines.push(this.border("├", "", "┤", innerWidth));
|
|
196
|
+
this.contentStripeIndices = [];
|
|
197
|
+
const content = this.detailOpen
|
|
198
|
+
? this.renderDetail(innerWidth)
|
|
199
|
+
: this.selectedRun
|
|
200
|
+
? this.renderTimeline(innerWidth)
|
|
201
|
+
: this.renderRunSelector(innerWidth);
|
|
202
|
+
const contentViewportRows = this.contentViewportRows();
|
|
203
|
+
const selectorRows = selector.slice(1);
|
|
204
|
+
for (let index = 0; index < Math.max(content.length, contentViewportRows); index += 1) {
|
|
205
|
+
const base = content[index] ?? "";
|
|
206
|
+
const stripeIndex = index < content.length
|
|
207
|
+
? this.contentStripeIndices[index] ?? index
|
|
208
|
+
: 0;
|
|
209
|
+
const popup = selectorRows[index];
|
|
210
|
+
if (!popup) {
|
|
211
|
+
lines.push(this.stripedRow(base, innerWidth, stripeIndex));
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const transparentPrefix = popup.match(/^ +/)?.[0].length ?? 0;
|
|
215
|
+
const visiblePopup = popup.slice(transparentPrefix);
|
|
216
|
+
const popupWidth = visibleWidth(visiblePopup);
|
|
217
|
+
const popupAnchor = selectorAnchor + transparentPrefix;
|
|
218
|
+
const baseWidth = Math.max(0, innerWidth - popupWidth - popupAnchor);
|
|
219
|
+
const leadingBase = this.stripeBackground(
|
|
220
|
+
this.fit(this.takeVisiblePrefix(base, popupAnchor), popupAnchor),
|
|
221
|
+
stripeIndex,
|
|
222
|
+
);
|
|
223
|
+
const preservedBase = this.stripeBackground(
|
|
224
|
+
this.fit(
|
|
225
|
+
this.dropVisiblePrefix(base, popupWidth + popupAnchor),
|
|
226
|
+
baseWidth,
|
|
227
|
+
),
|
|
228
|
+
stripeIndex,
|
|
229
|
+
);
|
|
230
|
+
lines.push(this.row(`${leadingBase}${visiblePopup}${preservedBase}`, innerWidth, true));
|
|
231
|
+
}
|
|
232
|
+
lines.push(this.border("├", "", "┤", innerWidth));
|
|
233
|
+
lines.push(this.row(this.renderKeyHints(), innerWidth));
|
|
234
|
+
lines.push(this.border("╰", "", "╯", innerWidth));
|
|
235
|
+
return lines;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
invalidate(): void {}
|
|
239
|
+
|
|
240
|
+
dispose(): void {
|
|
241
|
+
clearInterval(this.refreshTimer);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private runs(): Inspector.ActorInspectorRunItem[] {
|
|
245
|
+
return Inspector.readActorInspectorRuns(this.stateRoot, this.ownerId);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private ensureSelectedRun(runs: Inspector.ActorInspectorRunItem[]): void {
|
|
249
|
+
if (!this.selectedRun) this.selectedRun = runs[0]?.run;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private contentViewportRows(): number {
|
|
253
|
+
const overlayRows = Math.floor(this.tui.terminal.rows * 0.94);
|
|
254
|
+
return Math.max(4, Math.min(16, overlayRows - 7));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private selectRun(run: string, index: number): void {
|
|
258
|
+
this.selectedRun = run;
|
|
259
|
+
this.runIndex = index;
|
|
260
|
+
this.rowIndex = 0;
|
|
261
|
+
this.subagentIndex = 0;
|
|
262
|
+
this.communicationFrom = "all";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private cycleRun(
|
|
266
|
+
runs: Inspector.ActorInspectorRunItem[],
|
|
267
|
+
direction: -1 | 1,
|
|
268
|
+
): void {
|
|
269
|
+
if (runs.length === 0) return;
|
|
270
|
+
const currentIndex = Math.max(
|
|
271
|
+
0,
|
|
272
|
+
runs.findIndex((run) => run.run === this.selectedRun),
|
|
273
|
+
);
|
|
274
|
+
const nextIndex = (currentIndex + direction + runs.length) % runs.length;
|
|
275
|
+
const next = runs[nextIndex];
|
|
276
|
+
if (next) this.selectRun(next.run, nextIndex);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private listItemCount(): number {
|
|
280
|
+
return this.tab === "communications"
|
|
281
|
+
? this.communicationPreviews().length
|
|
282
|
+
: this.turnItems().length;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private selectorAnchor(): number {
|
|
286
|
+
if (this.menuLevel === "run") {
|
|
287
|
+
const runControl = stripVTControlCharacters(this.renderRunControl());
|
|
288
|
+
return Math.max(0, runControl.indexOf("Run:") - 2);
|
|
289
|
+
}
|
|
290
|
+
const label = this.tab === "communications" ? "Messages" : "Turns";
|
|
291
|
+
const tabs = stripVTControlCharacters(this.renderTabs());
|
|
292
|
+
return Math.max(0, tabs.indexOf(label) - 2);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private renderKeyHints(): string {
|
|
296
|
+
const hint = (keys: string, description: string) =>
|
|
297
|
+
`${this.theme.fg("accent", keys)}${this.theme.fg("dim", ` ${description}`)}`;
|
|
298
|
+
if (this.focus === "select")
|
|
299
|
+
return this.menuLevel === "value"
|
|
300
|
+
? ` ${hint("↑↓", "option")} ${hint("enter", "apply")} ${hint("←/esc", "back")}`
|
|
301
|
+
: ` ${hint("↑↓", "option")} ${hint("→/enter", "open")} ${hint("←/esc", "back")}`;
|
|
302
|
+
if (this.focus === "detail")
|
|
303
|
+
return this.tab === "turns" && this.detailView === "evidence"
|
|
304
|
+
? ` ${hint("↑↓", "scroll")} ${hint("→/enter", "readable")} ${hint("←/esc", "back")}`
|
|
305
|
+
: ` ${hint("↑↓", "scroll")} ${hint("←/esc", this.detailView === "transcript" ? "evidence" : "back")}`;
|
|
306
|
+
if (this.focus === "list")
|
|
307
|
+
return ` ${hint("↑↓", "row")} ${hint("→/enter", "open")} ${hint("esc", "close")}`;
|
|
308
|
+
if (this.focus === "runs")
|
|
309
|
+
return ` ${hint("←→", "run")} ${hint("↓", "tabs")} ${hint("enter", "list")} ${hint("esc", "close")}`;
|
|
310
|
+
return ` ${hint("←→", "navigate")} ${hint("↑↓", "change row")} ${hint("enter", "select")} ${hint("esc", "close")}`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private renderRunControl(): string {
|
|
314
|
+
const runs = this.runs();
|
|
315
|
+
const runIndex = runs.findIndex((item) => item.run === this.selectedRun);
|
|
316
|
+
const run = runs[runIndex];
|
|
317
|
+
const focused = this.focus === "runs";
|
|
318
|
+
const active = focused || (this.focus === "select" && this.menuLevel === "run");
|
|
319
|
+
const prefix = active ? this.theme.fg("accent", " ← ") : " ";
|
|
320
|
+
const suffix = active ? this.theme.fg("accent", " → ") : " ";
|
|
321
|
+
if (!run) {
|
|
322
|
+
const empty = this.theme.fg("muted", `${prefix}Run: none${suffix}`);
|
|
323
|
+
return focused ? this.theme.bg("selectedBg", empty) : empty;
|
|
324
|
+
}
|
|
325
|
+
const statusColor = run.status === "failed"
|
|
326
|
+
? "error"
|
|
327
|
+
: run.status === "running"
|
|
328
|
+
? "warning"
|
|
329
|
+
: run.status === "done"
|
|
330
|
+
? "success"
|
|
331
|
+
: "muted";
|
|
332
|
+
const sequence = runs.length - runIndex;
|
|
333
|
+
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}`;
|
|
334
|
+
return focused ? this.theme.bg("selectedBg", value) : value;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private activeMessageFilters(): string[] {
|
|
338
|
+
return [
|
|
339
|
+
...(this.communicationChannel === "all" ? [] : [this.communicationChannel]),
|
|
340
|
+
...(this.communicationUnread ? ["unread"] : []),
|
|
341
|
+
...(this.communicationFrom === "all" ? [] : [this.communicationFrom]),
|
|
342
|
+
];
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private renderTabs(): string {
|
|
346
|
+
const tab = (id: ActorInspectorOverlayTab, label: string) => {
|
|
347
|
+
const selected = this.tab === id;
|
|
348
|
+
const focused = selected && this.focus === "tabs";
|
|
349
|
+
const runActive =
|
|
350
|
+
this.focus === "runs" ||
|
|
351
|
+
(this.focus === "select" && this.menuLevel === "run");
|
|
352
|
+
const display = selected && !runActive ? `[ ${label} ]` : ` ${label} `;
|
|
353
|
+
const value = ` ${display} `;
|
|
354
|
+
if (!selected) return this.theme.fg("muted", value);
|
|
355
|
+
const colored = this.theme.fg("accent", value);
|
|
356
|
+
return focused ? this.theme.bg("selectedBg", colored) : colored;
|
|
357
|
+
};
|
|
358
|
+
const messageFilters = this.activeMessageFilters();
|
|
359
|
+
const subagent = this.subagents()[this.subagentIndex] ?? "all";
|
|
360
|
+
const messagesLabel = `Messages${messageFilters.length > 0 ? ` (${messageFilters.join(", ")})` : ""}`;
|
|
361
|
+
const turnsLabel = `Turns${subagent === "all" ? "" : ` (${subagent})`}`;
|
|
362
|
+
return `${tab("communications", messagesLabel)} ${tab("turns", turnsLabel)}`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private renderRunSelector(width: number): string[] {
|
|
366
|
+
const runs = this.runs();
|
|
367
|
+
if (runs.length === 0) return [this.theme.fg("muted", " No owned actor runs")];
|
|
368
|
+
this.runIndex = Math.min(this.runIndex, runs.length - 1);
|
|
369
|
+
return runs.slice(0, this.contentViewportRows()).map((run, index) => {
|
|
370
|
+
const selected = index === this.runIndex;
|
|
371
|
+
const prefix = selected ? this.theme.fg("accent", " ▶ ") : " ";
|
|
372
|
+
const label = `${prefix}#${runs.length - index} run:${run.run}`;
|
|
373
|
+
const status = ` ${run.status}`;
|
|
374
|
+
return selected
|
|
375
|
+
? this.theme.fg("accent", `${label}${status}`)
|
|
376
|
+
: `${label}${this.theme.fg("muted", status)}`;
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private renderTimeline(width: number): string[] {
|
|
381
|
+
if (!this.selectedRun) return [];
|
|
382
|
+
const run = this.runs().find((item) => item.run === this.selectedRun);
|
|
383
|
+
if (!run) {
|
|
384
|
+
this.selectedRun = undefined;
|
|
385
|
+
return [this.theme.fg("warning", " Selected run is no longer owned by this session")];
|
|
386
|
+
}
|
|
387
|
+
const rows =
|
|
388
|
+
this.tab === "communications"
|
|
389
|
+
? this.communicationRows()
|
|
390
|
+
: this.turnRows();
|
|
391
|
+
if (rows.length === 0) {
|
|
392
|
+
this.contentStripeIndices = [0];
|
|
393
|
+
const filtered = this.tab === "communications"
|
|
394
|
+
? this.activeMessageFilters().length > 0
|
|
395
|
+
: (this.subagents()[this.subagentIndex] ?? "all") !== "all";
|
|
396
|
+
return [
|
|
397
|
+
this.theme.fg(
|
|
398
|
+
"muted",
|
|
399
|
+
filtered
|
|
400
|
+
? " No results for the active filters. Enter on the tab to adjust."
|
|
401
|
+
: this.tab === "communications"
|
|
402
|
+
? " No messages in this run"
|
|
403
|
+
: " No persisted turns in this run",
|
|
404
|
+
),
|
|
405
|
+
];
|
|
406
|
+
}
|
|
407
|
+
this.rowIndex = Math.min(this.rowIndex, rows.length - 1);
|
|
408
|
+
const viewportRows = this.contentViewportRows();
|
|
409
|
+
const start = Math.max(
|
|
410
|
+
0,
|
|
411
|
+
Math.min(
|
|
412
|
+
this.rowIndex - Math.floor(viewportRows / 2),
|
|
413
|
+
Math.max(0, rows.length - viewportRows),
|
|
414
|
+
),
|
|
415
|
+
);
|
|
416
|
+
const stripeIndices = this.tab === "communications"
|
|
417
|
+
? this.communicationPreviews().map((preview, index) =>
|
|
418
|
+
Math.max(0, (preview.sequence ?? index + 1) - 1),
|
|
419
|
+
)
|
|
420
|
+
: this.turnItems().map((turn) => Math.max(0, turn.index - 1));
|
|
421
|
+
const visibleRows = rows.slice(start, start + viewportRows);
|
|
422
|
+
this.contentStripeIndices = visibleRows.map(
|
|
423
|
+
(_, offset) => stripeIndices[start + offset] ?? start + offset,
|
|
424
|
+
);
|
|
425
|
+
return visibleRows.map((row, offset) => {
|
|
426
|
+
const index = start + offset;
|
|
427
|
+
const selected = this.focus === "list" && index === this.rowIndex;
|
|
428
|
+
return selected
|
|
429
|
+
? this.theme.fg("accent", ` ▶ ${row}`)
|
|
430
|
+
: ` ${row}`;
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
private communicationPreviews(): Inspector.ActorInspectorPreview[] {
|
|
435
|
+
if (!this.selectedRun) return [];
|
|
436
|
+
const channels =
|
|
437
|
+
this.communicationChannel === "all"
|
|
438
|
+
? undefined
|
|
439
|
+
: [this.communicationChannel];
|
|
440
|
+
return Inspector.readActorInspectorPreviews(this.stateRoot, 100, {
|
|
441
|
+
channels,
|
|
442
|
+
currentRunOnly: false,
|
|
443
|
+
ownerId: this.ownerId,
|
|
444
|
+
readKeys: this.readKeys,
|
|
445
|
+
run: this.selectedRun,
|
|
446
|
+
unreadOnly: this.communicationUnread,
|
|
447
|
+
})
|
|
448
|
+
.filter(
|
|
449
|
+
(preview) =>
|
|
450
|
+
this.communicationFrom === "all" ||
|
|
451
|
+
this.communicationActorLabel(preview.from_display ?? preview.from) ===
|
|
452
|
+
this.communicationFrom,
|
|
453
|
+
)
|
|
454
|
+
.reverse();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private communicationActorLabel(actor?: string): string {
|
|
458
|
+
const value = actor ?? "unknown";
|
|
459
|
+
if (value.startsWith("run:")) return value.slice("run:".length);
|
|
460
|
+
return value.replace(/^branch:[^/]+\//, "");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private communicationFromOptions(): string[] {
|
|
464
|
+
if (!this.selectedRun) return ["all"];
|
|
465
|
+
const actors = Inspector.readActorInspectorRoster(
|
|
466
|
+
this.stateRoot,
|
|
467
|
+
this.selectedRun,
|
|
468
|
+
).map((member) =>
|
|
469
|
+
this.communicationActorLabel(member.display ?? member.address),
|
|
470
|
+
);
|
|
471
|
+
return ["all", ...new Set(actors)];
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
private communicationRows(): string[] {
|
|
475
|
+
return this.communicationPreviews().map((preview) => {
|
|
476
|
+
const actor = preview.from_display ?? preview.from ?? "unknown";
|
|
477
|
+
const target = preview.channel === "room" ? "#all" : preview.to;
|
|
478
|
+
const text = preview.summary ?? preview.body_preview ?? "(no body)";
|
|
479
|
+
return `#${preview.sequence ?? "?"} ${preview.needs_response ? "! " : ""}${actor} → ${target} ${preview.type} ${text}`;
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private renderDetail(width: number): string[] {
|
|
484
|
+
if (this.tab === "communications") {
|
|
485
|
+
const preview = this.detailCommunication;
|
|
486
|
+
if (!preview) return [this.theme.fg("warning", " Communication row is no longer available")];
|
|
487
|
+
const fields: Array<[string, unknown]> = [
|
|
488
|
+
["channel", preview.channel],
|
|
489
|
+
["run", preview.run],
|
|
490
|
+
["from", preview.from_display ?? preview.from],
|
|
491
|
+
["to", preview.to],
|
|
492
|
+
["type", preview.type],
|
|
493
|
+
["summary", preview.summary],
|
|
494
|
+
["body", preview.body_preview],
|
|
495
|
+
["attention", preview.needs_response],
|
|
496
|
+
["inbox", preview.inbox_status],
|
|
497
|
+
["timestamp", preview.timestamp],
|
|
498
|
+
];
|
|
499
|
+
return fields
|
|
500
|
+
.filter(([, value]) => value !== undefined)
|
|
501
|
+
.map(([label, value]) =>
|
|
502
|
+
truncateToWidth(
|
|
503
|
+
` ${label.padEnd(12, " ")} ${typeof value === "string" ? value : JSON.stringify(value)}`,
|
|
504
|
+
width,
|
|
505
|
+
"…",
|
|
506
|
+
),
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
const turn = this.detailTurn;
|
|
510
|
+
if (!turn) return [this.theme.fg("warning", " Turn row is no longer available")];
|
|
511
|
+
const logicalLines = this.detailView === "transcript"
|
|
512
|
+
? this.turnTranscriptLines(turn)
|
|
513
|
+
: this.turnEvidenceLines(turn);
|
|
514
|
+
const wrapped = this.wrapDetailLines(logicalLines, width);
|
|
515
|
+
const viewportRows = this.contentViewportRows();
|
|
516
|
+
const maxStart = Math.max(0, wrapped.lines.length - viewportRows);
|
|
517
|
+
this.detailScroll = Math.min(this.detailScroll, maxStart);
|
|
518
|
+
this.contentStripeIndices = wrapped.stripeIndices.slice(
|
|
519
|
+
this.detailScroll,
|
|
520
|
+
this.detailScroll + viewportRows,
|
|
521
|
+
);
|
|
522
|
+
return wrapped.lines.slice(
|
|
523
|
+
this.detailScroll,
|
|
524
|
+
this.detailScroll + viewportRows,
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private wrapDetailLines(
|
|
529
|
+
lines: DetailLogicalLine[],
|
|
530
|
+
width: number,
|
|
531
|
+
): { lines: string[]; stripeIndices: number[] } {
|
|
532
|
+
const wrappedLines: string[] = [];
|
|
533
|
+
const stripeIndices: number[] = [];
|
|
534
|
+
let sectionIndex = 0;
|
|
535
|
+
for (const line of lines) {
|
|
536
|
+
if (typeof line !== "string") {
|
|
537
|
+
sectionIndex += 1;
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
const physicalLines = line.replaceAll("\t", " ").split(/\r?\n/);
|
|
541
|
+
for (const physicalLine of physicalLines) {
|
|
542
|
+
if (!physicalLine.trim()) continue;
|
|
543
|
+
const leading = physicalLine.match(/^ */)?.[0] ?? "";
|
|
544
|
+
const content = physicalLine.slice(leading.length);
|
|
545
|
+
const contentWidth = Math.max(1, width - visibleWidth(leading));
|
|
546
|
+
const wrapped = visibleWidth(physicalLine) <= width
|
|
547
|
+
? [physicalLine]
|
|
548
|
+
: wrapTextWithAnsi(content, contentWidth).map(
|
|
549
|
+
(visualLine) => `${leading}${visualLine}`,
|
|
550
|
+
);
|
|
551
|
+
for (const visualLine of wrapped) {
|
|
552
|
+
if (!visualLine.trim()) continue;
|
|
553
|
+
wrappedLines.push(visualLine);
|
|
554
|
+
stripeIndices.push(sectionIndex);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return { lines: wrappedLines, stripeIndices };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private detailSection(
|
|
562
|
+
title: string,
|
|
563
|
+
content: string[],
|
|
564
|
+
): DetailLogicalLine[] {
|
|
565
|
+
if (content.length === 0) return [];
|
|
566
|
+
return [
|
|
567
|
+
` ${title}`,
|
|
568
|
+
...content.flatMap((line) =>
|
|
569
|
+
line
|
|
570
|
+
.split(/\r?\n/)
|
|
571
|
+
.filter((physicalLine) => physicalLine.trim().length > 0)
|
|
572
|
+
.map((physicalLine) => ` ${physicalLine}`),
|
|
573
|
+
),
|
|
574
|
+
DETAIL_SECTION_BREAK,
|
|
575
|
+
];
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
private readableValueLines(
|
|
579
|
+
value: unknown,
|
|
580
|
+
indent = "",
|
|
581
|
+
depth = 0,
|
|
582
|
+
): string[] {
|
|
583
|
+
if (depth > 8) return [`${indent}[nested value]`];
|
|
584
|
+
if (typeof value === "string") {
|
|
585
|
+
const trimmed = value.trim();
|
|
586
|
+
if (/^[{[]/.test(trimmed)) {
|
|
587
|
+
try {
|
|
588
|
+
return this.readableValueLines(JSON.parse(trimmed), indent, depth + 1);
|
|
589
|
+
} catch {
|
|
590
|
+
// Keep non-JSON text as written.
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return value.split(/\r?\n/).map((line) => `${indent}${line}`);
|
|
594
|
+
}
|
|
595
|
+
if (value === null || value === undefined)
|
|
596
|
+
return [`${indent}${String(value)}`];
|
|
597
|
+
if (typeof value !== "object") return [`${indent}${String(value)}`];
|
|
598
|
+
if (Array.isArray(value)) {
|
|
599
|
+
if (value.length === 0) return [`${indent}(empty)`];
|
|
600
|
+
return value.flatMap((item) => {
|
|
601
|
+
if (
|
|
602
|
+
item &&
|
|
603
|
+
typeof item === "object" &&
|
|
604
|
+
(item as { type?: unknown }).type === "text" &&
|
|
605
|
+
typeof (item as { text?: unknown }).text === "string"
|
|
606
|
+
) {
|
|
607
|
+
return this.readableValueLines(
|
|
608
|
+
(item as { text: string }).text,
|
|
609
|
+
indent,
|
|
610
|
+
depth + 1,
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
if (!item || typeof item !== "object")
|
|
614
|
+
return [`${indent}- ${String(item)}`];
|
|
615
|
+
return [
|
|
616
|
+
`${indent}-`,
|
|
617
|
+
...this.readableValueLines(item, `${indent} `, depth + 1),
|
|
618
|
+
];
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
622
|
+
if (entries.length === 0) return [`${indent}(empty)`];
|
|
623
|
+
return entries.flatMap(([key, item]) => {
|
|
624
|
+
if (!item || typeof item !== "object") {
|
|
625
|
+
const scalar = typeof item === "string" ? item : String(item);
|
|
626
|
+
const [first = "", ...rest] = scalar.split(/\r?\n/);
|
|
627
|
+
return [
|
|
628
|
+
`${indent}${key}: ${first}`,
|
|
629
|
+
...rest.map((line) => `${indent} ${line}`),
|
|
630
|
+
];
|
|
631
|
+
}
|
|
632
|
+
return [
|
|
633
|
+
`${indent}${key}:`,
|
|
634
|
+
...this.readableValueLines(item, `${indent} `, depth + 1),
|
|
635
|
+
];
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private turnEvidenceLines(
|
|
640
|
+
turn: Inspector.ActorInspectorTurnItem,
|
|
641
|
+
): DetailLogicalLine[] {
|
|
642
|
+
const lines: DetailLogicalLine[] = [];
|
|
643
|
+
const commandNumber = /^command-(\d+)$/.exec(turn.commandId)?.[1];
|
|
644
|
+
const subagent = commandNumber
|
|
645
|
+
? String(Number.parseInt(commandNumber, 10))
|
|
646
|
+
: turn.commandId;
|
|
647
|
+
const stage =
|
|
648
|
+
turn.stage && turn.stage !== "subagent" && turn.stage !== "command"
|
|
649
|
+
? ` (${turn.stage})`
|
|
650
|
+
: "";
|
|
651
|
+
lines.push(` Subagent ${subagent}${stage}`, DETAIL_SECTION_BREAK);
|
|
652
|
+
if (turn.userText)
|
|
653
|
+
lines.push(...this.detailSection("User", [turn.userText]));
|
|
654
|
+
if (turn.thinking)
|
|
655
|
+
lines.push(...this.detailSection("Thinking", [turn.thinking]));
|
|
656
|
+
if (turn.assistantText)
|
|
657
|
+
lines.push(...this.detailSection("Assistant", [turn.assistantText]));
|
|
658
|
+
for (const [index, tool] of turn.toolCalls.entries()) {
|
|
659
|
+
const content = [
|
|
660
|
+
...(tool.arguments === undefined
|
|
661
|
+
? []
|
|
662
|
+
: ["Input", ...this.readableValueLines(tool.arguments, " ")]),
|
|
663
|
+
...(tool.result === undefined
|
|
664
|
+
? []
|
|
665
|
+
: ["Result", ...this.readableValueLines(tool.result, " ")]),
|
|
666
|
+
`call id: ${tool.id}`,
|
|
667
|
+
];
|
|
668
|
+
lines.push(
|
|
669
|
+
...this.detailSection(
|
|
670
|
+
`Tool ${index + 1} (${tool.name}${tool.resultError ? ", Error" : ""})`,
|
|
671
|
+
content,
|
|
672
|
+
),
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
const execution = [
|
|
676
|
+
...(!turn.thinking ? ["thinking: not persisted"] : []),
|
|
677
|
+
...(turn.model ? [`model: ${turn.model}`] : []),
|
|
678
|
+
...(turn.stopReason ? [`stop reason: ${turn.stopReason}`] : []),
|
|
679
|
+
...(turn.error ? [`error: ${turn.error}`] : []),
|
|
680
|
+
...(turn.usage === undefined
|
|
681
|
+
? []
|
|
682
|
+
: ["Usage", ...this.readableValueLines(turn.usage, " ")]),
|
|
683
|
+
...(turn.unmatchedToolResults > 0
|
|
684
|
+
? [`unmatched tool results: ${turn.unmatchedToolResults}`]
|
|
685
|
+
: []),
|
|
686
|
+
];
|
|
687
|
+
if (execution.length > 0)
|
|
688
|
+
lines.push(...this.detailSection("Execution", execution));
|
|
689
|
+
if (turn.diagnostics.length > 0)
|
|
690
|
+
lines.push(...this.detailSection("Diagnostics", turn.diagnostics));
|
|
691
|
+
const provenance = [
|
|
692
|
+
"Session",
|
|
693
|
+
` ${turn.sessionFile}`,
|
|
694
|
+
...(turn.sessionTruncated ? [" turn list truncated"] : []),
|
|
695
|
+
...(turn.promptFile
|
|
696
|
+
? [
|
|
697
|
+
"Prompt",
|
|
698
|
+
` ${turn.promptFile}${turn.promptBytes ? ` (${turn.promptBytes} bytes)` : ""}`,
|
|
699
|
+
]
|
|
700
|
+
: []),
|
|
701
|
+
...(turn.recipeContext === undefined
|
|
702
|
+
? []
|
|
703
|
+
: [
|
|
704
|
+
"Recipe Context",
|
|
705
|
+
...this.readableValueLines(turn.recipeContext, " "),
|
|
706
|
+
]),
|
|
707
|
+
];
|
|
708
|
+
lines.push(...this.detailSection("Provenance", provenance));
|
|
709
|
+
return lines.length > 0 ? lines : [" No persisted turn evidence"];
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
private readablePromptText(text: string): string {
|
|
713
|
+
const wrapped = /^\s*<file\b[^>]*>\s*([\s\S]*?)\s*<\/file>\s*$/i.exec(text);
|
|
714
|
+
return wrapped?.[1]?.trim() || text;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
private turnTranscriptLines(
|
|
718
|
+
turn: Inspector.ActorInspectorTurnItem,
|
|
719
|
+
): DetailLogicalLine[] {
|
|
720
|
+
const lines: DetailLogicalLine[] = [];
|
|
721
|
+
if (turn.userText)
|
|
722
|
+
lines.push(
|
|
723
|
+
...this.detailSection("User", [this.readablePromptText(turn.userText)]),
|
|
724
|
+
);
|
|
725
|
+
if (turn.thinking)
|
|
726
|
+
lines.push(...this.detailSection("Thinking", [turn.thinking]));
|
|
727
|
+
if (turn.assistantText)
|
|
728
|
+
lines.push(...this.detailSection("Assistant", [turn.assistantText]));
|
|
729
|
+
for (const tool of turn.toolCalls) {
|
|
730
|
+
const content = [
|
|
731
|
+
...(tool.arguments === undefined
|
|
732
|
+
? []
|
|
733
|
+
: ["Input", ...this.readableValueLines(tool.arguments, " ")]),
|
|
734
|
+
...(tool.result === undefined
|
|
735
|
+
? []
|
|
736
|
+
: ["Result", ...this.readableValueLines(tool.result, " ")]),
|
|
737
|
+
];
|
|
738
|
+
lines.push(
|
|
739
|
+
...this.detailSection(
|
|
740
|
+
`Tool (${tool.name}${tool.resultError ? ", Error" : ""})`,
|
|
741
|
+
content,
|
|
742
|
+
),
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
if (turn.error) lines.push(...this.detailSection("Error", [turn.error]));
|
|
746
|
+
return lines.length > 0 ? lines : [" No readable turn content"];
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
private turnItems(): Inspector.ActorInspectorTurnItem[] {
|
|
750
|
+
if (!this.selectedRun) return [];
|
|
751
|
+
const selectedSubagent = this.subagents()[this.subagentIndex];
|
|
752
|
+
return Inspector.readActorInspectorTurns(
|
|
753
|
+
path.join(this.stateRoot, this.selectedRun),
|
|
754
|
+
)
|
|
755
|
+
.filter(
|
|
756
|
+
(turn) =>
|
|
757
|
+
!selectedSubagent ||
|
|
758
|
+
selectedSubagent === "all" ||
|
|
759
|
+
turn.commandId === selectedSubagent,
|
|
760
|
+
)
|
|
761
|
+
.reverse();
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
private turnRows(): string[] {
|
|
765
|
+
return this.turnItems().map((turn) => {
|
|
766
|
+
const commandNumber = /^command-(\d+)$/.exec(turn.commandId)?.[1];
|
|
767
|
+
const subagent = commandNumber
|
|
768
|
+
? `Subagent ${Number.parseInt(commandNumber, 10)}`
|
|
769
|
+
: `Subagent ${turn.commandId}`;
|
|
770
|
+
const stage =
|
|
771
|
+
turn.stage && turn.stage !== "subagent" && turn.stage !== "command"
|
|
772
|
+
? ` (${turn.stage})`
|
|
773
|
+
: "";
|
|
774
|
+
const toolNames = [...new Set(turn.toolCalls.map((tool) => tool.name))];
|
|
775
|
+
const action = turn.toolCalls.length === 0
|
|
776
|
+
? ""
|
|
777
|
+
: turn.toolCalls.length === 1
|
|
778
|
+
? ` (${toolNames[0]})`
|
|
779
|
+
: toolNames.length === 1
|
|
780
|
+
? ` (${toolNames[0]}, ${turn.toolCalls.length} calls)`
|
|
781
|
+
: turn.toolCalls.length <= 2
|
|
782
|
+
? ` (${toolNames.join(", ")})`
|
|
783
|
+
: ` (${turn.toolCalls.length} tools)`;
|
|
784
|
+
const failed = turn.error || turn.toolCalls.some((tool) => tool.resultError)
|
|
785
|
+
? " (error)"
|
|
786
|
+
: "";
|
|
787
|
+
const preview = this.readablePromptText(
|
|
788
|
+
turn.assistantText ?? turn.userText ?? "(no text)",
|
|
789
|
+
).replaceAll(/\s+/g, " ").trim();
|
|
790
|
+
return `#${turn.index} ${subagent}${stage} ${turn.model ?? "unknown model"}${action}${failed} ${preview}`;
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
private subagents(): string[] {
|
|
795
|
+
if (!this.selectedRun || this.tab !== "turns") return ["all"];
|
|
796
|
+
const ids = Inspector.readActorInspectorTurns(
|
|
797
|
+
path.join(this.stateRoot, this.selectedRun),
|
|
798
|
+
).map((turn) => turn.commandId);
|
|
799
|
+
return ["all", ...new Set(ids)];
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
private valueOptions(): string[] {
|
|
803
|
+
if (this.tab === "turns") return this.subagents();
|
|
804
|
+
if (this.filterControlIndex === 0)
|
|
805
|
+
return ["all", "room", "direct", "broadcast"];
|
|
806
|
+
if (this.filterControlIndex === 1) return ["all", "unread"];
|
|
807
|
+
return this.communicationFromOptions();
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
private filterOptions(): string[] {
|
|
811
|
+
return this.tab === "communications"
|
|
812
|
+
? ["Channel", "State", "From"]
|
|
813
|
+
: ["Subagent"];
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
private menuOptions(): string[] {
|
|
817
|
+
if (this.menuLevel === "run") return this.runs().map((run) => run.run);
|
|
818
|
+
if (this.menuLevel === "filter") return this.filterOptions();
|
|
819
|
+
return this.valueOptions();
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
private backMenu(): void {
|
|
823
|
+
if (this.menuLevel === "value") {
|
|
824
|
+
this.menuLevel = "filter";
|
|
825
|
+
this.selectorIndex = this.filterControlIndex;
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
this.focus = this.menuLevel === "run" ? "runs" : "tabs";
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
private openRunMenu(): void {
|
|
832
|
+
const options = this.runs();
|
|
833
|
+
this.selectorIndex = Math.max(0, options.findIndex((run) => run.run === this.selectedRun));
|
|
834
|
+
this.menuLevel = "run";
|
|
835
|
+
this.focus = "select";
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
private openFilterMenu(): void {
|
|
839
|
+
this.selectorIndex = this.filterControlIndex;
|
|
840
|
+
this.menuLevel = "filter";
|
|
841
|
+
this.focus = "select";
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
private applyMenuOption(value?: string): void {
|
|
845
|
+
if (!value) return;
|
|
846
|
+
if (this.menuLevel === "run") {
|
|
847
|
+
this.selectRun(
|
|
848
|
+
value,
|
|
849
|
+
Math.max(0, this.runs().findIndex((run) => run.run === value)),
|
|
850
|
+
);
|
|
851
|
+
this.focus = "runs";
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
if (this.menuLevel === "filter") {
|
|
855
|
+
this.filterControlIndex = this.selectorIndex;
|
|
856
|
+
const options = this.valueOptions();
|
|
857
|
+
const current = this.currentFilterValue();
|
|
858
|
+
this.selectorIndex = Math.max(0, options.indexOf(current));
|
|
859
|
+
this.menuLevel = "value";
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
if (this.tab === "turns")
|
|
863
|
+
this.subagentIndex = Math.max(0, this.subagents().indexOf(value));
|
|
864
|
+
else if (this.filterControlIndex === 0)
|
|
865
|
+
this.communicationChannel = value as typeof this.communicationChannel;
|
|
866
|
+
else if (this.filterControlIndex === 1)
|
|
867
|
+
this.communicationUnread = value === "unread";
|
|
868
|
+
else this.communicationFrom = value;
|
|
869
|
+
this.rowIndex = 0;
|
|
870
|
+
this.menuLevel = "filter";
|
|
871
|
+
this.selectorIndex = this.filterControlIndex;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
private filterValue(index: number): string {
|
|
875
|
+
if (this.tab === "turns") return this.subagents()[this.subagentIndex] ?? "all";
|
|
876
|
+
if (index === 0) return this.communicationChannel;
|
|
877
|
+
if (index === 1) return this.communicationUnread ? "unread" : "all";
|
|
878
|
+
return this.communicationFrom;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
private currentFilterValue(): string {
|
|
882
|
+
return this.filterValue(this.filterControlIndex);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
private renderMenuBox(
|
|
886
|
+
options: string[],
|
|
887
|
+
focusedIndex: number,
|
|
888
|
+
current?: string,
|
|
889
|
+
parentIndex?: number,
|
|
890
|
+
omitLeftBorder = false,
|
|
891
|
+
omitRightBorder = false,
|
|
892
|
+
maxVisibleOptions = options.length,
|
|
893
|
+
): string[] {
|
|
894
|
+
const visibleLimit = Math.max(1, Math.min(maxVisibleOptions, options.length));
|
|
895
|
+
const anchorIndex = Math.max(
|
|
896
|
+
0,
|
|
897
|
+
Math.min(
|
|
898
|
+
options.length - 1,
|
|
899
|
+
focusedIndex >= 0 ? focusedIndex : (parentIndex ?? 0),
|
|
900
|
+
),
|
|
901
|
+
);
|
|
902
|
+
const maxStart = Math.max(0, options.length - visibleLimit);
|
|
903
|
+
const start = Math.max(
|
|
904
|
+
0,
|
|
905
|
+
Math.min(anchorIndex - Math.floor(visibleLimit / 2), maxStart),
|
|
906
|
+
);
|
|
907
|
+
const visibleOptions = options.slice(start, start + visibleLimit);
|
|
908
|
+
const hiddenAbove = start > 0;
|
|
909
|
+
const hiddenBelow = start + visibleOptions.length < options.length;
|
|
910
|
+
const contentWidth = Math.max(
|
|
911
|
+
4,
|
|
912
|
+
...options.map((option) => visibleWidth(option) + 4),
|
|
913
|
+
);
|
|
914
|
+
const border = (left: string, right: string, marker = "") =>
|
|
915
|
+
this.theme.fg(
|
|
916
|
+
"border",
|
|
917
|
+
`${omitLeftBorder ? "" : left}${marker}${"─".repeat(Math.max(0, contentWidth - visibleWidth(marker)))}${omitRightBorder ? "" : right}`,
|
|
918
|
+
);
|
|
919
|
+
return [
|
|
920
|
+
border("╭", "╮", hiddenAbove ? "↑" : ""),
|
|
921
|
+
...visibleOptions.map((option, localIndex) => {
|
|
922
|
+
const index = start + localIndex;
|
|
923
|
+
const focused = index === focusedIndex;
|
|
924
|
+
const row = this.fit(`${focused ? " ▶ " : " "}${option}`, contentWidth);
|
|
925
|
+
const colored = focused || option === current || index === parentIndex
|
|
926
|
+
? this.theme.fg("accent", row)
|
|
927
|
+
: row;
|
|
928
|
+
const styled = focused || index === parentIndex
|
|
929
|
+
? this.theme.bg("selectedBg", colored)
|
|
930
|
+
: colored;
|
|
931
|
+
return `${omitLeftBorder ? "" : this.theme.fg("border", "│")}${styled}${omitRightBorder ? "" : this.theme.fg("border", "│")}`;
|
|
932
|
+
}),
|
|
933
|
+
border("╰", "╯", hiddenBelow ? "↓" : ""),
|
|
934
|
+
];
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
private renderFilterMenus(_width: number): string[] {
|
|
938
|
+
const filters = this.filterOptions().map(
|
|
939
|
+
(filter, index) => `${filter}: ${this.filterValue(index)}`,
|
|
940
|
+
);
|
|
941
|
+
const maxVisibleOptions = Math.max(1, this.contentViewportRows() - 1);
|
|
942
|
+
const parentFocus = this.menuLevel === "filter" ? this.selectorIndex : -1;
|
|
943
|
+
const parent = this.renderMenuBox(
|
|
944
|
+
filters,
|
|
945
|
+
parentFocus,
|
|
946
|
+
undefined,
|
|
947
|
+
this.menuLevel === "value" ? this.filterControlIndex : undefined,
|
|
948
|
+
false,
|
|
949
|
+
false,
|
|
950
|
+
maxVisibleOptions,
|
|
951
|
+
);
|
|
952
|
+
if (this.menuLevel !== "value") return parent;
|
|
953
|
+
const parentShared = this.renderMenuBox(
|
|
954
|
+
filters,
|
|
955
|
+
parentFocus,
|
|
956
|
+
undefined,
|
|
957
|
+
this.filterControlIndex,
|
|
958
|
+
false,
|
|
959
|
+
true,
|
|
960
|
+
maxVisibleOptions,
|
|
961
|
+
);
|
|
962
|
+
const child = this.renderMenuBox(
|
|
963
|
+
this.valueOptions(),
|
|
964
|
+
this.selectorIndex,
|
|
965
|
+
this.currentFilterValue(),
|
|
966
|
+
undefined,
|
|
967
|
+
false,
|
|
968
|
+
false,
|
|
969
|
+
maxVisibleOptions,
|
|
970
|
+
);
|
|
971
|
+
return Array.from({ length: Math.max(parent.length, child.length) }, (_, index) =>
|
|
972
|
+
child[index]
|
|
973
|
+
? `${parentShared[index] ?? " ".repeat(visibleWidth(parentShared[0]))}${child[index]}`
|
|
974
|
+
: (parent[index] ?? ""),
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
private openDetail(): void {
|
|
979
|
+
if (this.tab === "communications") {
|
|
980
|
+
const preview = this.communicationPreviews()[this.rowIndex];
|
|
981
|
+
this.detailCommunication = preview;
|
|
982
|
+
if (preview) this.readKeys.add(Inspector.inspectorPreviewReadKey(preview));
|
|
983
|
+
} else this.detailTurn = this.turnItems()[this.rowIndex];
|
|
984
|
+
this.detailScroll = 0;
|
|
985
|
+
this.detailView = "evidence";
|
|
986
|
+
this.detailOpen = Boolean(this.detailCommunication || this.detailTurn);
|
|
987
|
+
if (this.detailOpen) this.focus = "detail";
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
private backDetail(): void {
|
|
991
|
+
if (this.detailView === "transcript") {
|
|
992
|
+
this.detailView = "evidence";
|
|
993
|
+
this.detailScroll = 0;
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
this.closeDetail();
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
private closeDetail(): void {
|
|
1000
|
+
this.detailOpen = false;
|
|
1001
|
+
this.detailCommunication = undefined;
|
|
1002
|
+
this.detailTurn = undefined;
|
|
1003
|
+
this.detailScroll = 0;
|
|
1004
|
+
this.detailView = "evidence";
|
|
1005
|
+
this.focus = "list";
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
private border(left: string, title: string, right: string, width: number): string {
|
|
1009
|
+
const titleText = truncateToWidth(title, width, "");
|
|
1010
|
+
const fill = "─".repeat(Math.max(0, width - visibleWidth(titleText)));
|
|
1011
|
+
return this.theme.fg("border", `${left}${titleText}${fill}${right}`);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
private stripeBackground(content: string, index: number): string {
|
|
1015
|
+
if (index % 2 === 0) return content;
|
|
1016
|
+
const segments = content.split("…");
|
|
1017
|
+
return segments
|
|
1018
|
+
.map((segment, segmentIndex) =>
|
|
1019
|
+
`${this.theme.bg("customMessageBg", segment)}${segmentIndex < segments.length - 1 ? this.theme.bg("customMessageBg", "…") : ""}`,
|
|
1020
|
+
)
|
|
1021
|
+
.join("");
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
private stripedRow(content: string, width: number, index: number): string {
|
|
1025
|
+
return this.row(
|
|
1026
|
+
this.stripeBackground(this.fit(content, width), index),
|
|
1027
|
+
width,
|
|
1028
|
+
true,
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
private row(content: string, width: number, fitted = false): string {
|
|
1033
|
+
const body = fitted ? content : this.fit(content, width);
|
|
1034
|
+
return `${this.theme.fg("border", "│")}${body}${this.theme.fg("border", "│")}`;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
private takeVisiblePrefix(content: string, width: number): string {
|
|
1038
|
+
const plain = stripVTControlCharacters(content);
|
|
1039
|
+
let consumed = 0;
|
|
1040
|
+
let result = "";
|
|
1041
|
+
for (const character of plain) {
|
|
1042
|
+
const characterWidth = visibleWidth(character);
|
|
1043
|
+
if (consumed + characterWidth > width) break;
|
|
1044
|
+
result += character;
|
|
1045
|
+
consumed += characterWidth;
|
|
1046
|
+
}
|
|
1047
|
+
return result;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
private dropVisiblePrefix(content: string, width: number): string {
|
|
1051
|
+
const plain = stripVTControlCharacters(content);
|
|
1052
|
+
let consumed = 0;
|
|
1053
|
+
let index = 0;
|
|
1054
|
+
for (const character of plain) {
|
|
1055
|
+
if (consumed >= width) break;
|
|
1056
|
+
consumed += visibleWidth(character);
|
|
1057
|
+
index += character.length;
|
|
1058
|
+
}
|
|
1059
|
+
return plain.slice(index);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
private fit(content: string, width: number): string {
|
|
1063
|
+
// A Component render entry must remain exactly one terminal row. Persisted
|
|
1064
|
+
// prompts, messages, and tool results may contain line breaks or tabs;
|
|
1065
|
+
// passing those through lets them escape the overlay compositor and corrupt
|
|
1066
|
+
// the underlying TUI during tab changes.
|
|
1067
|
+
const singleLine = content.replace(/[\r\n\t]/g, " ");
|
|
1068
|
+
const bounded = truncateToWidth(singleLine, width, "…");
|
|
1069
|
+
return `${bounded}${" ".repeat(Math.max(0, width - visibleWidth(bounded)))}`;
|
|
1070
|
+
}
|
|
1071
|
+
}
|