@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.
@@ -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 { Container, Text, truncateToWidth, type Component, type TUI } from "@earendil-works/pi-tui";
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 STATUS_PANEL_FIXED_LINE_COUNT = 7;
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
- ): string[] {
151
+ cache: TranscriptRenderCache,
152
+ ): TranscriptLayout {
89
153
  if (snapshot.messages.length === 0) {
90
- return snapshot.fallback
91
- ? new Text(snapshot.fallback, 3, 0).render(width)
92
- : new Text("No Recent Activity", 3, 0).render(width);
154
+ return {
155
+ lines: new Text(snapshot.fallback || "No conversation messages yet.", 3, 0).render(width),
156
+ messageStarts: [0],
157
+ };
93
158
  }
94
- const container = new Container();
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 tool = pendingTools.get(message.toolCallId);
145
- if (tool) {
146
- tool.updateResult(message);
147
- pendingTools.delete(message.toolCallId);
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
- return container.render(width);
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 readonly expandedAgentIds = new Set<string>();
202
- private readonly transcripts = new Map<string, ChildAgentTranscriptSnapshot>();
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.toggleSelectedAgent();
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.scrollOffset = Math.max(0, this.scrollOffset - this.viewportHeight());
247
- this.ensureSelectionVisible = false;
383
+ this.scroll(-this.viewportHeight());
248
384
  } else if (this.keybindings.matches(data, "tui.select.pageDown")) {
249
- this.scrollOffset += this.viewportHeight();
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 the fixed access header and scrollable Child Agent hierarchy. */
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 header = this.renderHeader(width);
261
- const rowStarts = new Map<string, number>();
262
- const body: string[] = [];
263
- for (const { agent, depth } of this.flattened) {
264
- rowStarts.set(agent.agent_id, body.length);
265
- body.push(this.renderAgentRow(agent, depth, width));
266
- if (!this.expandedAgentIds.has(agent.agent_id)) continue;
267
- const transcript = this.transcripts.get(agent.agent_id);
268
- if (transcript) {
269
- body.push(
270
- ...renderTranscriptSnapshot(
271
- transcript,
272
- this.tui,
273
- this.cwd,
274
- this.toolOutputExpanded,
275
- width,
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
- const viewportHeight = this.viewportHeight();
281
- const selectedLine = this.selectedAgentId ? rowStarts.get(this.selectedAgentId) : undefined;
282
- if (this.ensureSelectionVisible && selectedLine !== undefined) {
283
- if (selectedLine < this.scrollOffset) this.scrollOffset = selectedLine;
284
- if (selectedLine >= this.scrollOffset + viewportHeight) {
285
- this.scrollOffset = selectedLine - viewportHeight + 1;
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.ensureSelectionVisible = false;
289
- this.scrollOffset = Math.min(this.scrollOffset, Math.max(0, body.length - viewportHeight));
290
- const visibleBody = body.slice(this.scrollOffset, this.scrollOffset + viewportHeight);
291
- const help = truncateToWidth(
292
- this.theme.fg(
293
- "dim",
294
- "↑↓ select Enter Recent Activity configured tool key expands output PgUp/PgDn scroll Esc close",
295
- ),
296
- width,
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
- /** Invalidate no cached layout because each render derives the current snapshot. */
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
- for (const agentId of this.expandedAgentIds) {
321
- if (!liveIds.has(agentId)) {
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 = this.expandedAgentIds.has(agent.agent_id) ? "▾" : "▸";
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 toggleSelectedAgent(): void {
385
- const agentId = this.selectedAgentId;
386
- if (!agentId) return;
387
- if (this.expandedAgentIds.delete(agentId)) {
388
- this.transcripts.delete(agentId);
389
- return;
390
- }
391
- this.expandedAgentIds.add(agentId);
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.transcripts.set(agentId, this.coordinator.inspectTranscript(agentId));
586
+ this.transcript = this.coordinator.inspectTranscript(agentId);
398
587
  } catch (error) {
399
- this.transcripts.set(agentId, {
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 Math.max(
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) return 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>((tui, theme, keybindings, done) => {
453
- const panel = new MinimalSubagentsStatusPanelComponent(
454
- this.coordinator,
455
- this.getAccess,
456
- tui,
457
- theme,
458
- keybindings,
459
- this.context.cwd,
460
- () => done(undefined),
461
- this.startRefresh,
462
- );
463
- this.activePanel = panel;
464
- return panel;
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 a bounded, image-free process-local Child Agent transcript for trusted status UI. */
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 it remains in the bounded tail. */
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
- /** Best-known status or result text when no live Child Agent runtime exists. */
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
- /** Select the bounded process-local transcript and its real visible tool definitions. */
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 = flattened
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);