@ian-pascoe/pi-minimal-subagents 0.6.5 → 0.7.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.
- package/README.md +33 -9
- package/package.json +1 -1
- package/src/minimal-subagents-config.ts +18 -14
- package/src/minimal-subagents-context.ts +19 -86
- package/src/minimal-subagents-coordinator.ts +9 -1
- package/src/minimal-subagents-extension.ts +89 -0
- package/src/minimal-subagents-fork-lifecycle.ts +1 -1
- package/src/minimal-subagents-registry-wire.ts +0 -4
- package/src/minimal-subagents-registry.ts +2 -6
- package/src/minimal-subagents-render-contract.ts +5 -9
- package/src/minimal-subagents-rendering.ts +18 -0
- package/src/minimal-subagents-sessions.ts +83 -7
- package/src/minimal-subagents-settings-writer.ts +9 -8
- package/src/minimal-subagents-status-panel.ts +308 -103
- package/src/minimal-subagents-types.ts +6 -4
- package/src/minimal-subagents-ui.ts +2 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from "node:util";
|
|
2
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
1
3
|
import { contentText } from "@earendil-works/pi-ai";
|
|
2
4
|
import {
|
|
3
5
|
AssistantMessageComponent,
|
|
@@ -12,10 +14,20 @@ import {
|
|
|
12
14
|
type Theme,
|
|
13
15
|
type TruncationResult,
|
|
14
16
|
} from "@earendil-works/pi-coding-agent";
|
|
15
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
Container,
|
|
19
|
+
Text,
|
|
20
|
+
matchesKey,
|
|
21
|
+
truncateToWidth,
|
|
22
|
+
visibleWidth,
|
|
23
|
+
type Component,
|
|
24
|
+
type OverlayHandle,
|
|
25
|
+
type TUI,
|
|
26
|
+
} from "@earendil-works/pi-tui";
|
|
16
27
|
import type { MinimalSubagentsCoordinator } from "./minimal-subagents-coordinator.js";
|
|
17
28
|
import {
|
|
18
29
|
formatSubagentDuration,
|
|
30
|
+
orderActiveAgentSubtrees,
|
|
19
31
|
renderMinimalSubagentsMessage,
|
|
20
32
|
renderMinimalSubagentsResult,
|
|
21
33
|
subagentStatusLadder,
|
|
@@ -29,8 +41,7 @@ import type {
|
|
|
29
41
|
} from "./minimal-subagents-types.js";
|
|
30
42
|
|
|
31
43
|
const STATUS_PANEL_REFRESH_MS = 1_000;
|
|
32
|
-
const
|
|
33
|
-
const STATUS_PANEL_MIN_VIEWPORT_LINES = 4;
|
|
44
|
+
const STATUS_PANEL_MARGIN = 1;
|
|
34
45
|
const COORDINATOR_TOOL_COUNT = COORDINATOR_TOOL_NAMES.length;
|
|
35
46
|
|
|
36
47
|
type StartStatusPanelRefresh = (refresh: () => void) => () => void;
|
|
@@ -58,7 +69,7 @@ function flattenStatusAgents(status: HierarchyStatusResult): FlattenedStatusAgen
|
|
|
58
69
|
for (const child of agent.children) visit(child, depth + 1);
|
|
59
70
|
};
|
|
60
71
|
const roots = "agents" in status ? status.agents : [status.agent];
|
|
61
|
-
for (const agent of roots) visit(agent, 0);
|
|
72
|
+
for (const agent of orderActiveAgentSubtrees(roots)) visit(agent, 0);
|
|
62
73
|
return flattened;
|
|
63
74
|
}
|
|
64
75
|
|
|
@@ -79,25 +90,107 @@ function statusAccessSourceLabel(source: SubagentAccessSnapshot["source"]): stri
|
|
|
79
90
|
}
|
|
80
91
|
}
|
|
81
92
|
|
|
93
|
+
interface CachedTranscriptMessage {
|
|
94
|
+
container: Container;
|
|
95
|
+
tools: Map<string, ToolExecutionComponent>;
|
|
96
|
+
expanded: boolean;
|
|
97
|
+
streaming: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface TranscriptLayout {
|
|
101
|
+
lines: string[];
|
|
102
|
+
messageStarts: number[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function transcriptText(line: string): string {
|
|
106
|
+
return stripVTControlCharacters(line).replace(/\s/g, "");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function anchoredTranscriptOffset(
|
|
110
|
+
previous: TranscriptLayout,
|
|
111
|
+
next: TranscriptLayout,
|
|
112
|
+
offset: number,
|
|
113
|
+
): number {
|
|
114
|
+
const index = previous.messageStarts.findLastIndex((start) => start <= offset);
|
|
115
|
+
const previousStart = previous.messageStarts[index] ?? 0;
|
|
116
|
+
const nextStart = next.messageStarts[index] ?? 0;
|
|
117
|
+
const oldLines = previous.lines
|
|
118
|
+
.slice(previousStart, previous.messageStarts[index + 1])
|
|
119
|
+
.map(transcriptText);
|
|
120
|
+
const newLines = next.lines.slice(nextStart, next.messageStarts[index + 1]).map(transcriptText);
|
|
121
|
+
const row = offset - previousStart;
|
|
122
|
+
if (oldLines.slice(0, row + 1).every((line, lineIndex) => line === newLines[lineIndex])) {
|
|
123
|
+
return nextStart + row;
|
|
124
|
+
}
|
|
125
|
+
const text = oldLines[row];
|
|
126
|
+
if (text) {
|
|
127
|
+
const occurrence = oldLines.slice(0, row).filter((line) => line === text).length;
|
|
128
|
+
let seen = 0;
|
|
129
|
+
const match = newLines.findIndex((line) => line === text && seen++ === occurrence);
|
|
130
|
+
if (match >= 0) return nextStart + match;
|
|
131
|
+
}
|
|
132
|
+
let characters = oldLines.slice(0, row).reduce((total, line) => total + line.length, 0);
|
|
133
|
+
for (const [lineIndex, line] of newLines.entries()) {
|
|
134
|
+
if (characters < line.length) return nextStart + lineIndex;
|
|
135
|
+
characters -= line.length;
|
|
136
|
+
}
|
|
137
|
+
return nextStart + Math.max(0, newLines.length - 1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
interface TranscriptRenderCache {
|
|
141
|
+
messages: WeakMap<AgentMessage, CachedTranscriptMessage>;
|
|
142
|
+
results: WeakMap<ToolExecutionComponent, AgentMessage>;
|
|
143
|
+
}
|
|
144
|
+
|
|
82
145
|
function renderTranscriptSnapshot(
|
|
83
146
|
snapshot: ChildAgentTranscriptSnapshot,
|
|
84
147
|
tui: TUI,
|
|
85
148
|
cwd: string,
|
|
86
149
|
expanded: boolean,
|
|
87
150
|
width: number,
|
|
88
|
-
|
|
151
|
+
cache: TranscriptRenderCache,
|
|
152
|
+
): TranscriptLayout {
|
|
89
153
|
if (snapshot.messages.length === 0) {
|
|
90
|
-
return
|
|
91
|
-
|
|
92
|
-
:
|
|
154
|
+
return {
|
|
155
|
+
lines: new Text(snapshot.fallback || "No conversation messages yet.", 3, 0).render(width),
|
|
156
|
+
messageStarts: [0],
|
|
157
|
+
};
|
|
93
158
|
}
|
|
94
|
-
const
|
|
159
|
+
const blocks: Container[] = [];
|
|
95
160
|
const tools = new Map(
|
|
96
161
|
snapshot.toolDefinitions.map((definition) => [definition.name, definition]),
|
|
97
162
|
);
|
|
98
163
|
const pendingTools = new Map<string, ToolExecutionComponent>();
|
|
164
|
+
const currentMessages = new Set(snapshot.messages);
|
|
99
165
|
|
|
100
166
|
for (const [messageIndex, message] of snapshot.messages.entries()) {
|
|
167
|
+
if (message.role === "toolResult") {
|
|
168
|
+
const paired = pendingTools.get(message.toolCallId);
|
|
169
|
+
if (paired) {
|
|
170
|
+
if (cache.results.get(paired) !== message) {
|
|
171
|
+
paired.updateResult(message);
|
|
172
|
+
cache.results.set(paired, message);
|
|
173
|
+
}
|
|
174
|
+
pendingTools.delete(message.toolCallId);
|
|
175
|
+
blocks.push(new Container());
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const streaming = messageIndex === snapshot.streamingAssistantIndex;
|
|
180
|
+
const cached = cache.messages.get(message);
|
|
181
|
+
const staleResult =
|
|
182
|
+
cached &&
|
|
183
|
+
[...cached.tools.values()].some((tool) => {
|
|
184
|
+
const result = cache.results.get(tool);
|
|
185
|
+
return result !== undefined && !currentMessages.has(result);
|
|
186
|
+
});
|
|
187
|
+
if (cached && !staleResult && cached.expanded === expanded && cached.streaming === streaming) {
|
|
188
|
+
blocks.push(cached.container);
|
|
189
|
+
for (const [id, tool] of cached.tools) pendingTools.set(id, tool);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const container = new Container();
|
|
193
|
+
const messageTools = new Map<string, ToolExecutionComponent>();
|
|
101
194
|
switch (message.role) {
|
|
102
195
|
case "user": {
|
|
103
196
|
const text = contentText(message.content, "\n\n");
|
|
@@ -115,7 +208,7 @@ function renderTranscriptSnapshot(
|
|
|
115
208
|
content.id,
|
|
116
209
|
content.arguments,
|
|
117
210
|
{ showImages: false },
|
|
118
|
-
tools.get(content.name),
|
|
211
|
+
tools.get(content.name) ?? {},
|
|
119
212
|
tui,
|
|
120
213
|
cwd,
|
|
121
214
|
);
|
|
@@ -136,16 +229,24 @@ function renderTranscriptSnapshot(
|
|
|
136
229
|
});
|
|
137
230
|
} else {
|
|
138
231
|
pendingTools.set(content.id, tool);
|
|
232
|
+
messageTools.set(content.id, tool);
|
|
139
233
|
}
|
|
140
234
|
}
|
|
141
235
|
break;
|
|
142
236
|
}
|
|
143
237
|
case "toolResult": {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
238
|
+
const inherited = new ToolExecutionComponent(
|
|
239
|
+
message.toolName,
|
|
240
|
+
message.toolCallId,
|
|
241
|
+
{},
|
|
242
|
+
{ showImages: false },
|
|
243
|
+
{},
|
|
244
|
+
tui,
|
|
245
|
+
cwd,
|
|
246
|
+
);
|
|
247
|
+
inherited.setExpanded(expanded);
|
|
248
|
+
inherited.updateResult(message);
|
|
249
|
+
container.addChild(inherited);
|
|
149
250
|
break;
|
|
150
251
|
}
|
|
151
252
|
case "custom": {
|
|
@@ -188,8 +289,26 @@ function renderTranscriptSnapshot(
|
|
|
188
289
|
break;
|
|
189
290
|
}
|
|
190
291
|
}
|
|
292
|
+
cache.messages.set(message, { container, tools: messageTools, expanded, streaming });
|
|
293
|
+
blocks.push(container);
|
|
191
294
|
}
|
|
192
|
-
|
|
295
|
+
let length = 0;
|
|
296
|
+
const messageStarts: number[] = [];
|
|
297
|
+
const lines = blocks.flatMap((block) => {
|
|
298
|
+
messageStarts.push(length);
|
|
299
|
+
// Native user-message prompt zones belong to the main terminal, not an embedded overlay.
|
|
300
|
+
const rendered = block
|
|
301
|
+
.render(width)
|
|
302
|
+
.map((line) =>
|
|
303
|
+
line
|
|
304
|
+
.replaceAll("\x1b]133;A\x07", "")
|
|
305
|
+
.replaceAll("\x1b]133;B\x07", "")
|
|
306
|
+
.replaceAll("\x1b]133;C\x07", ""),
|
|
307
|
+
);
|
|
308
|
+
length += rendered.length;
|
|
309
|
+
return rendered;
|
|
310
|
+
});
|
|
311
|
+
return { lines, messageStarts };
|
|
193
312
|
}
|
|
194
313
|
|
|
195
314
|
/** Interactive, read-only Child Agent hierarchy and transcript status component. */
|
|
@@ -198,9 +317,18 @@ export class MinimalSubagentsStatusPanelComponent implements Component {
|
|
|
198
317
|
private access!: MinimalSubagentsStatusAccess;
|
|
199
318
|
private flattened: FlattenedStatusAgent[] = [];
|
|
200
319
|
private selectedAgentId?: string;
|
|
201
|
-
private
|
|
202
|
-
private
|
|
320
|
+
private view: "tree" | "transcript" = "tree";
|
|
321
|
+
private transcript?: ChildAgentTranscriptSnapshot;
|
|
322
|
+
private notice = "";
|
|
203
323
|
private scrollOffset = 0;
|
|
324
|
+
private following = true;
|
|
325
|
+
private transcriptLineCount = 0;
|
|
326
|
+
private transcriptLayout?: TranscriptLayout;
|
|
327
|
+
private readonly transcriptCache: TranscriptRenderCache = {
|
|
328
|
+
messages: new WeakMap(),
|
|
329
|
+
results: new WeakMap(),
|
|
330
|
+
};
|
|
331
|
+
private bodyHeight = 1;
|
|
204
332
|
private ensureSelectionVisible = true;
|
|
205
333
|
private toolOutputExpanded = false;
|
|
206
334
|
private disposed = false;
|
|
@@ -231,7 +359,14 @@ export class MinimalSubagentsStatusPanelComponent implements Component {
|
|
|
231
359
|
/** Handle read-only hierarchy navigation and close keys. */
|
|
232
360
|
handleInput(data: string): void {
|
|
233
361
|
if (this.keybindings.matches(data, "tui.select.cancel")) {
|
|
234
|
-
this.close();
|
|
362
|
+
if (this.view === "tree") this.close();
|
|
363
|
+
else {
|
|
364
|
+
this.view = "tree";
|
|
365
|
+
this.transcript = undefined;
|
|
366
|
+
this.scrollOffset = 0;
|
|
367
|
+
this.ensureSelectionVisible = true;
|
|
368
|
+
this.tui.requestRender();
|
|
369
|
+
}
|
|
235
370
|
return;
|
|
236
371
|
}
|
|
237
372
|
if (this.keybindings.matches(data, "tui.select.up")) {
|
|
@@ -239,68 +374,108 @@ export class MinimalSubagentsStatusPanelComponent implements Component {
|
|
|
239
374
|
} else if (this.keybindings.matches(data, "tui.select.down")) {
|
|
240
375
|
this.moveSelection(1);
|
|
241
376
|
} else if (this.keybindings.matches(data, "tui.select.confirm")) {
|
|
242
|
-
this.
|
|
377
|
+
this.openSelectedTranscript();
|
|
243
378
|
} else if (this.keybindings.matches(data, "app.tools.expand")) {
|
|
244
379
|
this.toolOutputExpanded = !this.toolOutputExpanded;
|
|
380
|
+
} else if (this.view === "transcript" && matchesKey(data, "end")) {
|
|
381
|
+
this.following = true;
|
|
245
382
|
} else if (this.keybindings.matches(data, "tui.select.pageUp")) {
|
|
246
|
-
this.
|
|
247
|
-
this.ensureSelectionVisible = false;
|
|
383
|
+
this.scroll(-this.viewportHeight());
|
|
248
384
|
} else if (this.keybindings.matches(data, "tui.select.pageDown")) {
|
|
249
|
-
this.
|
|
250
|
-
this.ensureSelectionVisible = false;
|
|
385
|
+
this.scroll(this.viewportHeight());
|
|
251
386
|
} else {
|
|
252
387
|
return;
|
|
253
388
|
}
|
|
254
389
|
this.tui.requestRender();
|
|
255
390
|
}
|
|
256
391
|
|
|
257
|
-
/** Render
|
|
392
|
+
/** Render one framed, terminal-bounded tree or Child Session Transcript. */
|
|
258
393
|
render(width: number): string[] {
|
|
259
394
|
if (width <= 0) return [];
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
),
|
|
395
|
+
const height = Math.max(
|
|
396
|
+
1,
|
|
397
|
+
Math.min(
|
|
398
|
+
Math.floor(this.tui.terminal.rows * 0.9),
|
|
399
|
+
this.tui.terminal.rows - 2 * STATUS_PANEL_MARGIN,
|
|
400
|
+
),
|
|
401
|
+
);
|
|
402
|
+
if (width < 6 || height < 5) {
|
|
403
|
+
return new Text("Esc back · Enlarge terminal", 0, 0).render(width).slice(0, height);
|
|
404
|
+
}
|
|
405
|
+
const innerWidth = width - 4;
|
|
406
|
+
const selected = this.flattened.find(({ agent }) => agent.agent_id === this.selectedAgentId);
|
|
407
|
+
const transcriptView = this.view === "transcript" && this.transcript;
|
|
408
|
+
const header = transcriptView
|
|
409
|
+
? [
|
|
410
|
+
this.theme.bold(`Transcript · ${this.selectedAgentId}`),
|
|
411
|
+
selected ? this.renderAgentRow(selected.agent, 0, innerWidth) : "",
|
|
412
|
+
]
|
|
413
|
+
: this.renderHeader(innerWidth);
|
|
414
|
+
const toolKey = this.keybindings.getKeys("app.tools.expand").join("/");
|
|
415
|
+
const helpText = transcriptView
|
|
416
|
+
? `Esc tree · End live · ${toolKey} tools · ↑↓/PgUp/PgDn scroll · ${this.following ? "Following" : "Paused"}`
|
|
417
|
+
: "Esc close · Enter transcript · ↑↓ select · PgUp/PgDn page";
|
|
418
|
+
const help = new Text(this.theme.fg("text", helpText), 0, 0)
|
|
419
|
+
.render(innerWidth)
|
|
420
|
+
.slice(0, Math.min(2, height - 4));
|
|
421
|
+
const visibleHeader = header.slice(0, Math.max(1, height - help.length - 3));
|
|
422
|
+
this.bodyHeight = Math.max(1, height - 2 - visibleHeader.length - help.length);
|
|
423
|
+
let body: string[];
|
|
424
|
+
if (transcriptView) {
|
|
425
|
+
const layout = renderTranscriptSnapshot(
|
|
426
|
+
transcriptView,
|
|
427
|
+
this.tui,
|
|
428
|
+
this.cwd,
|
|
429
|
+
this.toolOutputExpanded,
|
|
430
|
+
innerWidth,
|
|
431
|
+
this.transcriptCache,
|
|
432
|
+
);
|
|
433
|
+
if (!this.following && this.transcriptLayout) {
|
|
434
|
+
this.scrollOffset = anchoredTranscriptOffset(
|
|
435
|
+
this.transcriptLayout,
|
|
436
|
+
layout,
|
|
437
|
+
this.scrollOffset,
|
|
277
438
|
);
|
|
278
439
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
440
|
+
this.transcriptLayout = layout;
|
|
441
|
+
body = layout.lines;
|
|
442
|
+
this.transcriptLineCount = body.length;
|
|
443
|
+
const maximum = Math.max(0, body.length - this.bodyHeight);
|
|
444
|
+
this.scrollOffset = this.following ? maximum : Math.min(this.scrollOffset, maximum);
|
|
445
|
+
} else {
|
|
446
|
+
body = this.flattened.map(({ agent, depth }) =>
|
|
447
|
+
this.renderAgentRow(agent, depth, innerWidth),
|
|
448
|
+
);
|
|
449
|
+
if (body.length === 0) body.push("No Child Agents yet.");
|
|
450
|
+
const selectedLine = this.flattened.findIndex(
|
|
451
|
+
({ agent }) => agent.agent_id === this.selectedAgentId,
|
|
452
|
+
);
|
|
453
|
+
if (this.ensureSelectionVisible && selectedLine >= 0) {
|
|
454
|
+
if (selectedLine < this.scrollOffset) this.scrollOffset = selectedLine;
|
|
455
|
+
if (selectedLine >= this.scrollOffset + this.bodyHeight)
|
|
456
|
+
this.scrollOffset = selectedLine - this.bodyHeight + 1;
|
|
286
457
|
}
|
|
458
|
+
this.ensureSelectionVisible = false;
|
|
459
|
+
this.scrollOffset = Math.min(this.scrollOffset, Math.max(0, body.length - this.bodyHeight));
|
|
287
460
|
}
|
|
288
|
-
this.
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
"
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
);
|
|
299
|
-
return [...header, ...visibleBody, help];
|
|
461
|
+
const visibleBody = body.slice(this.scrollOffset, this.scrollOffset + this.bodyHeight);
|
|
462
|
+
while (visibleBody.length < this.bodyHeight) visibleBody.push("");
|
|
463
|
+
const border = (text: string) => this.theme.fg("border", text);
|
|
464
|
+
const rows = [...visibleHeader, ...visibleBody, ...help].map((line) => {
|
|
465
|
+
const content = truncateToWidth(line, innerWidth, "…");
|
|
466
|
+
return this.theme.bg(
|
|
467
|
+
"customMessageBg",
|
|
468
|
+
`${border("│")} ${content}${" ".repeat(innerWidth - visibleWidth(content))} ${border("│")}`,
|
|
469
|
+
);
|
|
470
|
+
});
|
|
471
|
+
return [border(`╭${"─".repeat(width - 2)}╮`), ...rows, border(`╰${"─".repeat(width - 2)}╯`)];
|
|
300
472
|
}
|
|
301
473
|
|
|
302
|
-
/**
|
|
303
|
-
invalidate(): void {
|
|
474
|
+
/** Rebuild native transcript components when their theme changes. */
|
|
475
|
+
invalidate(): void {
|
|
476
|
+
this.transcriptCache.messages = new WeakMap();
|
|
477
|
+
this.transcriptCache.results = new WeakMap();
|
|
478
|
+
}
|
|
304
479
|
|
|
305
480
|
/** Release the live refresh owner idempotently. */
|
|
306
481
|
dispose(): void {
|
|
@@ -315,15 +490,16 @@ export class MinimalSubagentsStatusPanelComponent implements Component {
|
|
|
315
490
|
this.flattened = flattenStatusAgents(this.status);
|
|
316
491
|
const liveIds = new Set(this.flattened.map(({ agent }) => agent.agent_id));
|
|
317
492
|
if (!this.selectedAgentId || !liveIds.has(this.selectedAgentId)) {
|
|
493
|
+
if (this.view === "transcript") {
|
|
494
|
+
this.notice = `${this.selectedAgentId} is no longer available.`;
|
|
495
|
+
this.view = "tree";
|
|
496
|
+
this.transcript = undefined;
|
|
497
|
+
}
|
|
498
|
+
this.ensureSelectionVisible = true;
|
|
318
499
|
this.selectedAgentId = this.flattened[0]?.agent.agent_id;
|
|
319
500
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
this.expandedAgentIds.delete(agentId);
|
|
323
|
-
this.transcripts.delete(agentId);
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
this.refreshAgentTranscript(agentId);
|
|
501
|
+
if (this.view === "transcript" && this.selectedAgentId) {
|
|
502
|
+
this.refreshAgentTranscript(this.selectedAgentId);
|
|
327
503
|
}
|
|
328
504
|
}
|
|
329
505
|
|
|
@@ -355,13 +531,13 @@ export class MinimalSubagentsStatusPanelComponent implements Component {
|
|
|
355
531
|
),
|
|
356
532
|
truncateToWidth(`Coordinator Tools: ${toolState}`, width, "…"),
|
|
357
533
|
truncateToWidth(`Direct Children: ${running} running · ${idle} idle`, width, "…"),
|
|
358
|
-
"",
|
|
534
|
+
this.theme.fg("warning", this.notice),
|
|
359
535
|
];
|
|
360
536
|
}
|
|
361
537
|
|
|
362
538
|
private renderAgentRow(agent: AgentSummary, depth: number, width: number): string {
|
|
363
539
|
const selected = agent.agent_id === this.selectedAgentId;
|
|
364
|
-
const disclosure =
|
|
540
|
+
const disclosure = "▸";
|
|
365
541
|
const status = subagentStatusLadder(agent);
|
|
366
542
|
const elapsed = formatSubagentDuration(agent.elapsed_ms);
|
|
367
543
|
const task = agent.task?.replace(/\s+/g, " ").trim();
|
|
@@ -371,7 +547,21 @@ export class MinimalSubagentsStatusPanelComponent implements Component {
|
|
|
371
547
|
return truncateToWidth(selected ? this.theme.fg("accent", line) : line, width, "…");
|
|
372
548
|
}
|
|
373
549
|
|
|
550
|
+
private scroll(delta: number): void {
|
|
551
|
+
this.scrollOffset = Math.max(0, this.scrollOffset + delta);
|
|
552
|
+
this.ensureSelectionVisible = false;
|
|
553
|
+
if (this.view === "transcript") {
|
|
554
|
+
const maximum = Math.max(0, this.transcriptLineCount - this.viewportHeight());
|
|
555
|
+
this.scrollOffset = Math.min(this.scrollOffset, maximum);
|
|
556
|
+
this.following = this.scrollOffset === maximum;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
374
560
|
private moveSelection(delta: number): void {
|
|
561
|
+
if (this.view === "transcript") {
|
|
562
|
+
this.scroll(delta);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
375
565
|
if (this.flattened.length === 0) return;
|
|
376
566
|
const current = this.flattened.findIndex(
|
|
377
567
|
({ agent }) => agent.agent_id === this.selectedAgentId,
|
|
@@ -381,34 +571,30 @@ export class MinimalSubagentsStatusPanelComponent implements Component {
|
|
|
381
571
|
this.ensureSelectionVisible = true;
|
|
382
572
|
}
|
|
383
573
|
|
|
384
|
-
private
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
this.
|
|
392
|
-
this.refreshAgentTranscript(agentId);
|
|
574
|
+
private openSelectedTranscript(): void {
|
|
575
|
+
if (this.view === "transcript" || !this.selectedAgentId) return;
|
|
576
|
+
this.view = "transcript";
|
|
577
|
+
this.notice = "";
|
|
578
|
+
this.following = true;
|
|
579
|
+
this.scrollOffset = 0;
|
|
580
|
+
this.toolOutputExpanded = false;
|
|
581
|
+
this.refreshAgentTranscript(this.selectedAgentId);
|
|
393
582
|
}
|
|
394
583
|
|
|
395
584
|
private refreshAgentTranscript(agentId: string): void {
|
|
396
585
|
try {
|
|
397
|
-
this.
|
|
586
|
+
this.transcript = this.coordinator.inspectTranscript(agentId);
|
|
398
587
|
} catch (error) {
|
|
399
|
-
this.
|
|
588
|
+
this.transcript = {
|
|
400
589
|
messages: [],
|
|
401
590
|
toolDefinitions: [],
|
|
402
591
|
fallback: error instanceof Error ? error.message : String(error),
|
|
403
|
-
}
|
|
592
|
+
};
|
|
404
593
|
}
|
|
405
594
|
}
|
|
406
595
|
|
|
407
596
|
private viewportHeight(): number {
|
|
408
|
-
return
|
|
409
|
-
STATUS_PANEL_MIN_VIEWPORT_LINES,
|
|
410
|
-
this.tui.terminal.rows - STATUS_PANEL_FIXED_LINE_COUNT,
|
|
411
|
-
);
|
|
597
|
+
return this.bodyHeight;
|
|
412
598
|
}
|
|
413
599
|
|
|
414
600
|
/** Settle the custom view and release its refresh timer exactly once. */
|
|
@@ -423,6 +609,7 @@ export class MinimalSubagentsStatusPanelComponent implements Component {
|
|
|
423
609
|
export class MinimalSubagentsStatusPanelController {
|
|
424
610
|
private activePanel?: MinimalSubagentsStatusPanelComponent;
|
|
425
611
|
private activePromise?: Promise<void>;
|
|
612
|
+
private overlayHandle?: OverlayHandle;
|
|
426
613
|
|
|
427
614
|
/** Bind the panel owner to one Root Agent session and refresh lifecycle. */
|
|
428
615
|
constructor(
|
|
@@ -434,7 +621,10 @@ export class MinimalSubagentsStatusPanelController {
|
|
|
434
621
|
|
|
435
622
|
/** Open or focus the single live view; RPC receives a notification and structured modes stay silent. */
|
|
436
623
|
open(): Promise<void> {
|
|
437
|
-
if (this.activePromise)
|
|
624
|
+
if (this.activePromise) {
|
|
625
|
+
this.overlayHandle?.focus();
|
|
626
|
+
return this.activePromise;
|
|
627
|
+
}
|
|
438
628
|
if (this.context.mode === "rpc") {
|
|
439
629
|
const status = this.coordinator.inspectStatus();
|
|
440
630
|
const direct = "agents" in status ? status.agents : [status.agent];
|
|
@@ -449,20 +639,34 @@ export class MinimalSubagentsStatusPanelController {
|
|
|
449
639
|
if (this.context.mode !== "tui") return Promise.resolve();
|
|
450
640
|
|
|
451
641
|
const promise = this.context.ui
|
|
452
|
-
.custom<void>(
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
642
|
+
.custom<void>(
|
|
643
|
+
(tui, theme, keybindings, done) => {
|
|
644
|
+
const panel = new MinimalSubagentsStatusPanelComponent(
|
|
645
|
+
this.coordinator,
|
|
646
|
+
this.getAccess,
|
|
647
|
+
tui,
|
|
648
|
+
theme,
|
|
649
|
+
keybindings,
|
|
650
|
+
this.context.cwd,
|
|
651
|
+
() => done(undefined),
|
|
652
|
+
this.startRefresh,
|
|
653
|
+
);
|
|
654
|
+
this.activePanel = panel;
|
|
655
|
+
return panel;
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
overlay: true,
|
|
659
|
+
overlayOptions: {
|
|
660
|
+
anchor: "center",
|
|
661
|
+
width: "90%",
|
|
662
|
+
maxHeight: "90%",
|
|
663
|
+
margin: STATUS_PANEL_MARGIN,
|
|
664
|
+
},
|
|
665
|
+
onHandle: (handle) => {
|
|
666
|
+
this.overlayHandle = handle;
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
)
|
|
466
670
|
.catch(() => {
|
|
467
671
|
this.context.ui.notify("Subagents status view failed.", "error");
|
|
468
672
|
})
|
|
@@ -470,6 +674,7 @@ export class MinimalSubagentsStatusPanelController {
|
|
|
470
674
|
this.activePanel?.dispose();
|
|
471
675
|
this.activePanel = undefined;
|
|
472
676
|
this.activePromise = undefined;
|
|
677
|
+
this.overlayHandle = undefined;
|
|
473
678
|
});
|
|
474
679
|
this.activePromise = promise;
|
|
475
680
|
return promise;
|
|
@@ -123,14 +123,14 @@ export interface RecentAgentActivity {
|
|
|
123
123
|
truncated: boolean;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
/** Holds
|
|
126
|
+
/** Holds the complete selected-branch Child Session Transcript for trusted status UI. */
|
|
127
127
|
export interface ChildAgentTranscriptSnapshot {
|
|
128
128
|
messages: AgentMessage[];
|
|
129
|
-
/** Index of the current streaming assistant message when
|
|
129
|
+
/** Index of the current streaming assistant message, when not yet committed. */
|
|
130
130
|
streamingAssistantIndex?: number;
|
|
131
131
|
/** Real Child Agent tool definitions referenced by visible tool calls. */
|
|
132
132
|
toolDefinitions: ToolDefinition[];
|
|
133
|
-
/**
|
|
133
|
+
/** Explanation when neither live nor verified saved history is available. */
|
|
134
134
|
fallback?: string;
|
|
135
135
|
}
|
|
136
136
|
|
|
@@ -241,7 +241,7 @@ export interface ChildAgentRuntime {
|
|
|
241
241
|
snapshotCommittedMessages(): AgentMessage[];
|
|
242
242
|
/** Clone child transcript messages including the current streaming assistant tail. */
|
|
243
243
|
snapshotActivityMessages(): AgentMessage[];
|
|
244
|
-
/**
|
|
244
|
+
/** Snapshot the full selected branch and streaming output with its real tool definitions. */
|
|
245
245
|
snapshotActivityTranscript?(): ChildAgentTranscriptSnapshot;
|
|
246
246
|
hasDeliveryEvidence(sourceAgentId: string, sourceTurnId: string, deliveryId?: string): boolean;
|
|
247
247
|
getUsage(): Usage | undefined;
|
|
@@ -259,6 +259,8 @@ export interface AgentSessionFactory {
|
|
|
259
259
|
createIdentity(agent: PersistedAgent, importedMessages: AgentMessage[]): PersistedSessionIdentity;
|
|
260
260
|
/** Open one verified persisted Child Agent runtime for launch or restoration. */
|
|
261
261
|
openRuntime(agent: PersistedAgent): Promise<ChildAgentRuntime>;
|
|
262
|
+
/** Read verified saved history independently of runtime restoration dependencies. */
|
|
263
|
+
readTranscript?(agent: PersistedAgent): ChildAgentTranscriptSnapshot;
|
|
262
264
|
resolveLaunchMissingDependencies(agent: PersistedAgent): Promise<string[]>;
|
|
263
265
|
resolveRestorationMissingDependencies(agent: PersistedAgent): Promise<string[]>;
|
|
264
266
|
resolveThinkingLevel(modelId: string, requested: ThinkingLevel): ThinkingLevel;
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import type { MinimalSubagentsCoordinator } from "./minimal-subagents-coordinator.js";
|
|
14
14
|
import {
|
|
15
15
|
formatSubagentDuration,
|
|
16
|
+
orderActiveAgentSubtrees,
|
|
16
17
|
renderSubagentStatusLabel,
|
|
17
18
|
renderSubagentStatusSymbol,
|
|
18
19
|
subagentStatusLadder,
|
|
@@ -128,7 +129,7 @@ export function buildMinimalSubagentsWidgetView(
|
|
|
128
129
|
if (chosenIds.size + additions.length > MINIMAL_SUBAGENTS_WIDGET_ROW_LIMIT) continue;
|
|
129
130
|
for (const item of additions) chosenIds.add(item.agent.agent_id);
|
|
130
131
|
}
|
|
131
|
-
const rows =
|
|
132
|
+
const rows = flattenAgentHierarchy(orderActiveAgentSubtrees(agents))
|
|
132
133
|
.filter((item) => chosenIds.has(item.agent.agent_id))
|
|
133
134
|
.map((item): MinimalSubagentsWidgetRow => {
|
|
134
135
|
const structural = !meaningfulIds.has(item.agent.agent_id);
|