@ian-pascoe/pi-minimal-subagents 0.12.0 → 0.14.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.
@@ -19,10 +19,13 @@ import {
19
19
  visibleWidth,
20
20
  type Component,
21
21
  } from "@earendil-works/pi-tui";
22
+ import { Type } from "typebox";
23
+ import { Value } from "typebox/value";
22
24
  import {
23
25
  parseCoordinatorMessageDetails,
24
26
  parseCoordinatorToolCall,
25
27
  parseCoordinatorToolResult,
28
+ parseWaitProgressDetails,
26
29
  type CancelRenderDetails,
27
30
  type CoordinatorMessageRenderDetails,
28
31
  type CoordinatorToolCallInput,
@@ -31,11 +34,13 @@ import {
31
34
  type ManagementCallArguments,
32
35
  type MessageCallArguments,
33
36
  type MessageRenderDetails,
37
+ type RenderRecentActivity,
34
38
  type RenderStatusAgent,
35
39
  type SpawnCallArguments,
36
40
  type SpawnRenderDetails,
37
41
  type StatusRenderDetails,
38
42
  type WaitCallArguments,
43
+ type WaitProgressRenderDetails,
39
44
  type WaitRenderDetails,
40
45
  } from "./minimal-subagents-render-contract.js";
41
46
  import { stripCoordinatorMessageEnvelope } from "./minimal-subagents-message-envelope.js";
@@ -63,6 +68,7 @@ type SubagentPresentationStatus =
63
68
  | "delivered"
64
69
  | "delivered-via-wait"
65
70
  | "queued"
71
+ | "started-turn"
66
72
  | "message"
67
73
  | "timed out";
68
74
 
@@ -80,6 +86,7 @@ const SUBAGENT_STATUS_PRESENTATION = {
80
86
  delivered: { symbol: "→", color: "accent" },
81
87
  "delivered-via-wait": { symbol: "→", color: "accent" },
82
88
  queued: { symbol: "↗", color: "accent" },
89
+ "started-turn": { symbol: "◉", color: "accent" },
83
90
  message: { symbol: "→", color: "accent" },
84
91
  "timed out": { symbol: "!", color: "warning" },
85
92
  } satisfies { readonly [Status in SubagentPresentationStatus]: SubagentStatusPresentation };
@@ -106,7 +113,7 @@ export function orderActiveAgentSubtrees(agents: readonly AgentSummary[]): Agent
106
113
  ): { agent: AgentSummary; active: boolean }[] =>
107
114
  siblings
108
115
  .map((agent) => {
109
- const children = orderSiblings(agent.children);
116
+ const children = orderSiblings(agent.children ?? []);
110
117
  return {
111
118
  agent: { ...agent, children: children.map((child) => child.agent) },
112
119
  active: agent.state === "running" || children.some((child) => child.active),
@@ -215,6 +222,169 @@ function collapsedExpansionHint(theme: MinimalSubagentsRenderTheme): string {
215
222
  return theme.fg("dim", ` · ${keyHint("app.tools.expand", "to expand")}`);
216
223
  }
217
224
 
225
+ /** Collapsed previews match Pi's built-in `read` output height. */
226
+ const COLLAPSED_PREVIEW_LINES = 10;
227
+
228
+ function isBlankRenderedLine(line: string): boolean {
229
+ return visibleWidth(line.trim()) === 0;
230
+ }
231
+
232
+ function withoutTrailingBlankLines(lines: string[]): string[] {
233
+ const end = lines.findLastIndex((line) => !isBlankRenderedLine(line));
234
+ return lines.slice(0, end + 1);
235
+ }
236
+
237
+ /** Shows the first rendered lines of `content`, noting how many lines expanding would reveal. */
238
+ class CollapsedPreview implements Component {
239
+ constructor(
240
+ private readonly content: Component,
241
+ private readonly theme: MinimalSubagentsRenderTheme,
242
+ ) {}
243
+
244
+ render(width: number): string[] {
245
+ const lines = withoutTrailingBlankLines(this.content.render(width));
246
+ if (lines.length <= COLLAPSED_PREVIEW_LINES) return lines;
247
+ // A cut at a paragraph break would leave a blank line above the hint.
248
+ const shown = withoutTrailingBlankLines(lines.slice(0, COLLAPSED_PREVIEW_LINES));
249
+ const hidden = lines.length - shown.length;
250
+ return [
251
+ ...shown,
252
+ // The result's first line already carries the expansion hint.
253
+ this.theme.fg("muted", `... (${hidden} more lines)`),
254
+ ];
255
+ }
256
+
257
+ invalidate(): void {
258
+ this.content.invalidate();
259
+ }
260
+ }
261
+
262
+ function collapsedMarkdownPreview(content: string, theme: MinimalSubagentsRenderTheme): Component {
263
+ return new CollapsedPreview(new Markdown(content, 0, 0, getMarkdownTheme()), theme);
264
+ }
265
+
266
+ function collapsedTextPreview(content: string, theme: MinimalSubagentsRenderTheme): Component {
267
+ return new CollapsedPreview(new Text(content, 0, 0), theme);
268
+ }
269
+
270
+ const ToolArgumentsSchema = Type.Record(Type.String(), Type.Unknown());
271
+ const ScalarArgumentSchema = Type.Union([Type.String(), Type.Number(), Type.Boolean()]);
272
+
273
+ /** Summarize tool-call arguments by their values, e.g. a path or command rather than its JSON. */
274
+ function toolArgumentSummary(content: string): string {
275
+ let parsed: unknown;
276
+ try {
277
+ parsed = JSON.parse(content);
278
+ } catch {
279
+ // Recent Activity keeps the tail of long arguments, which is no longer valid JSON.
280
+ return content;
281
+ }
282
+ if (!Value.Check(ToolArgumentsSchema, parsed)) return content;
283
+ return Object.values(parsed)
284
+ .filter((value) => value !== null && value !== undefined)
285
+ .map((value) =>
286
+ Value.Check(ScalarArgumentSchema, value) ? String(value) : JSON.stringify(value),
287
+ )
288
+ .join(" ");
289
+ }
290
+
291
+ function lastNonEmptyLine(content: string): string {
292
+ return (
293
+ content
294
+ .split("\n")
295
+ .map((line) => line.trim())
296
+ .findLast((line) => line.length > 0) ?? ""
297
+ );
298
+ }
299
+
300
+ /** One activity entry as a single width-bounded line, truncating plain text before styling it. */
301
+ function formatActivityLine(
302
+ activity: RenderRecentActivity,
303
+ theme: MinimalSubagentsRenderTheme,
304
+ width: number,
305
+ ): string {
306
+ const fit = (prefix: string, text: string) =>
307
+ formatSubagentPreview(text, Math.max(1, width - visibleWidth(prefix)));
308
+ if (activity.label.startsWith("tool call ")) {
309
+ const name = activity.label.slice("tool call ".length);
310
+ const prefix = `\u2192 ${name} `;
311
+ return `${theme.fg("accent", "\u2192")} ${theme.fg("toolTitle", name)} ${theme.fg("dim", fit(prefix, toolArgumentSummary(activity.content)))}`;
312
+ }
313
+ if (activity.label.startsWith("tool result ")) {
314
+ const failed = activity.label.endsWith(" (error)");
315
+ const name = activity.label.slice("tool result ".length).replace(/ \(error\)$/, "");
316
+ const prefix = `${failed ? "\u2717" : "\u2190"} ${name}: `;
317
+ return theme.fg(failed ? "error" : "dim", `${prefix}${fit(prefix, activity.content)}`);
318
+ }
319
+ if (activity.label === "reasoning") {
320
+ const prefix = "thinking: ";
321
+ return theme.fg("dim", `${prefix}${fit(prefix, lastNonEmptyLine(activity.content))}`);
322
+ }
323
+ if (activity.label === "assistant message") {
324
+ return theme.fg("muted", fit("", lastNonEmptyLine(activity.content)));
325
+ }
326
+ const prefix = `${activity.label}: `;
327
+ return theme.fg("dim", `${prefix}${fit(prefix, activity.content)}`);
328
+ }
329
+
330
+ /** Columns the activity rail takes before each item's own lines. */
331
+ export const ACTIVITY_RAIL_WIDTH = 3;
332
+
333
+ /** Hang rendered items off a tree rail: `├─` opens each item, `│` continues it, `└─` opens the last. */
334
+ export function drawActivityRail(
335
+ items: readonly (readonly string[])[],
336
+ theme: MinimalSubagentsRenderTheme,
337
+ ): string[] {
338
+ return items.flatMap((lines, index) => {
339
+ const last = index === items.length - 1;
340
+ const [first = "", ...rest] = lines;
341
+ return [
342
+ `${theme.fg("dim", last ? "└─ " : "├─ ")}${first}`,
343
+ ...rest.map((line) => `${theme.fg("dim", last ? " " : "│ ")}${line}`),
344
+ ];
345
+ });
346
+ }
347
+
348
+ /** Width-aware rendering of recorded activity, one rail item per entry. */
349
+ class ActivityLines implements Component {
350
+ constructor(
351
+ private readonly activity: readonly RenderRecentActivity[],
352
+ private readonly theme: MinimalSubagentsRenderTheme,
353
+ ) {}
354
+
355
+ render(width: number): string[] {
356
+ const itemWidth = Math.max(1, width - ACTIVITY_RAIL_WIDTH);
357
+ return drawActivityRail(
358
+ this.activity.map((entry) => [formatActivityLine(entry, this.theme, itemWidth)]),
359
+ this.theme,
360
+ );
361
+ }
362
+
363
+ invalidate(): void {}
364
+ }
365
+
366
+ const COLLAPSED_ACTIVITY_LINES = 3;
367
+
368
+ /** Latest progress-relevant activity: model output and tool calls, plus only failed tool results. */
369
+ function latestProgressActivity(activity: readonly RenderRecentActivity[]): RenderRecentActivity[] {
370
+ return activity
371
+ .filter(
372
+ (entry) =>
373
+ entry.label === "assistant message" ||
374
+ entry.label === "reasoning" ||
375
+ entry.label.startsWith("tool call ") ||
376
+ (entry.label.startsWith("tool result ") && entry.label.endsWith(" (error)")),
377
+ )
378
+ .slice(-COLLAPSED_ACTIVITY_LINES);
379
+ }
380
+
381
+ /** Format a turn's cost for summary rows; sub-cent costs keep enough precision to be non-zero. */
382
+ function formatSubagentCost(usage: Usage | undefined): string | undefined {
383
+ const total = usage?.cost.total;
384
+ if (total === undefined || !(total > 0)) return undefined;
385
+ return `$${total.toFixed(total < 0.01 ? 4 : 2)}`;
386
+ }
387
+
218
388
  /** Format milliseconds for compact subagent rows without losing sub-second durations. */
219
389
  export function formatSubagentDuration(elapsedMs: number | undefined): string | undefined {
220
390
  if (elapsedMs === undefined || !Number.isFinite(elapsedMs) || elapsedMs < 0) return undefined;
@@ -245,10 +415,6 @@ export function formatSubagentPreview(content: string | undefined, maxWidth = 72
245
415
  return `${sliceByColumn(singleLine, 0, boundedWidth - 1, true).trimEnd()}…`;
246
416
  }
247
417
 
248
- function currentSubagentPreviewWidth(reservedWidth: number): number {
249
- return Math.max(12, Math.min(72, (process.stdout.columns || 100) - reservedWidth));
250
- }
251
-
252
418
  /** Format complete Pi usage metrics for expanded subagent output. */
253
419
  export function formatSubagentUsage(usage: Usage | undefined): string | undefined {
254
420
  if (usage === undefined) return undefined;
@@ -267,13 +433,41 @@ function coordinatorToolCallTitle(theme: MinimalSubagentsRenderTheme, label: str
267
433
  return theme.fg("toolTitle", theme.bold(label));
268
434
  }
269
435
 
270
- function coordinatorToolCallPreview(
436
+ /** Launch settings the caller chose explicitly; defaults are omitted to keep the header short. */
437
+ function spawnCallLaunchSummary(args: SpawnCallArguments): string[] {
438
+ const profile = args.model
439
+ ? `${args.model}${args.thinking_level ? `:${args.thinking_level}` : ""}`
440
+ : args.thinking_level
441
+ ? `thinking ${args.thinking_level}`
442
+ : undefined;
443
+ const tools =
444
+ args.tools === undefined
445
+ ? undefined
446
+ : `tools ${Array.isArray(args.tools) ? args.tools.join(", ") || "none" : args.tools}`;
447
+ return [
448
+ args.role ? `role ${args.role}` : undefined,
449
+ profile,
450
+ tools,
451
+ args.session_context && args.session_context !== "omit"
452
+ ? `context ${args.session_context}`
453
+ : undefined,
454
+ args.project_context === "omit" ? "no project context" : undefined,
455
+ args.delegation && args.delegation !== "none" ? args.delegation : undefined,
456
+ ].filter((part): part is string => part !== undefined);
457
+ }
458
+
459
+ /** A call header followed by its long text argument, previewed when collapsed. */
460
+ function renderCallWithText(
461
+ header: string,
462
+ text: string | undefined,
271
463
  theme: MinimalSubagentsRenderTheme,
272
- value: string | undefined,
273
- ): string {
274
- return value && value.length > 0
275
- ? ` · ${theme.fg("dim", `“${formatSubagentPreview(value, currentSubagentPreviewWidth(36))}”`)}`
276
- : "";
464
+ expanded: boolean,
465
+ ): Component {
466
+ if (!text) return new Text(header, 0, 0);
467
+ const container = new Container();
468
+ container.addChild(new Text(header, 0, 0));
469
+ container.addChild(expanded ? new Text(text, 0, 0) : collapsedTextPreview(text, theme));
470
+ return container;
277
471
  }
278
472
 
279
473
  function renderManagementToolCall(
@@ -308,13 +502,13 @@ function renderSpawnResult(
308
502
  const container = new Container();
309
503
  container.addChild(new Text(renderSubagentSummary(theme, status, agentId), 0, 0));
310
504
  container.addChild(renderLabelValue(theme, "Turn", details.turn_id));
311
- appendTextSection(container, theme, "Task", args.task ?? "(task unavailable)");
312
505
  const resolvedModel = launchContract?.model ?? args.model;
313
506
  const resolvedThinking = launchContract?.thinking_level ?? args.thinking_level;
314
507
  const launch = [
315
508
  `delegation ${launchContract?.delegation ?? args.delegation ?? "none"}`,
316
- `session context ${launchContract?.session_context ?? args.session_context ?? "inherit"}`,
509
+ `session context ${launchContract?.session_context ?? args.session_context ?? "omit"}`,
317
510
  `project context ${launchContract?.project_context ?? args.project_context ?? "inherit"}`,
511
+ launchContract?.role ? `role ${launchContract.role}` : undefined,
318
512
  resolvedModel ? `model ${resolvedModel}` : undefined,
319
513
  resolvedThinking ? `thinking ${resolvedThinking}` : undefined,
320
514
  ].filter((value): value is string => value !== undefined);
@@ -350,13 +544,65 @@ function renderMessageResult(
350
544
  if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
351
545
  const container = new Container();
352
546
  container.addChild(new Text(summary, 0, 0));
353
- appendTextSection(container, theme, "Message", args.message ?? "(message unavailable)");
354
547
  appendTextSection(container, theme, "Recipient", agentId);
355
548
  appendTextSection(container, theme, "Disposition", disposition);
549
+ if ("turn_id" in details && details.turn_id) {
550
+ appendTextSection(container, theme, "Started turn", details.turn_id);
551
+ }
356
552
  if (details.error) appendTextSection(container, theme, "Error", details.error);
357
553
  return container;
358
554
  }
359
555
 
556
+ /** Renders a running child's turn for a waiting parent; returns nothing when the turn is not live. */
557
+ export type LiveTurnRenderer = (
558
+ agentId: string,
559
+ turnId: string | undefined,
560
+ expanded: boolean,
561
+ ) => Component | undefined;
562
+
563
+ function renderWaitProgress(
564
+ details: WaitProgressRenderDetails,
565
+ options: ToolRenderResultOptions,
566
+ theme: MinimalSubagentsRenderTheme,
567
+ renderLiveTurn: LiveTurnRenderer | undefined,
568
+ ): Component {
569
+ const toolCalls = details.tool_calls ?? 0;
570
+ const metrics = [
571
+ formatSubagentDuration(details.elapsed_ms),
572
+ toolCalls > 0 ? `${toolCalls} tool ${toolCalls === 1 ? "call" : "calls"}` : undefined,
573
+ ].filter((metric): metric is string => metric !== undefined);
574
+ const summary = renderSubagentSummary(theme, "waiting", details.agent_id, metrics);
575
+ const liveTurn = renderLiveTurn?.(details.agent_id, details.turn_id, options.expanded);
576
+ if (!liveTurn) return new Text(summary, 0, 0);
577
+ const container = new Container();
578
+ container.addChild(
579
+ new Text(options.expanded ? summary : `${summary}${collapsedExpansionHint(theme)}`, 0, 0),
580
+ );
581
+ container.addChild(liveTurn);
582
+ return container;
583
+ }
584
+
585
+ /** The collapsed body of a settled wait: what the child said, or why it stopped. */
586
+ function collapsedWaitBody(
587
+ details: WaitRenderDetails,
588
+ status: string,
589
+ theme: MinimalSubagentsRenderTheme,
590
+ ): Component | undefined {
591
+ if (details.event === "message") return collapsedTextPreview(details.message, theme);
592
+ if (details.event === "timeout") {
593
+ const activity = latestProgressActivity(details.agent.recent_activity ?? []);
594
+ return activity.length > 0 ? new ActivityLines(activity, theme) : undefined;
595
+ }
596
+ const output = details.output ?? "";
597
+ if (status === "completed") {
598
+ return output ? collapsedMarkdownPreview(output, theme) : undefined;
599
+ }
600
+ return collapsedTextPreview(
601
+ theme.fg("error", details.error ?? (output || "(no error detail)")),
602
+ theme,
603
+ );
604
+ }
605
+
360
606
  function renderWaitResult(
361
607
  details: WaitRenderDetails,
362
608
  options: ToolRenderResultOptions,
@@ -374,9 +620,8 @@ function renderWaitResult(
374
620
  const duration = formatSubagentDuration(
375
621
  details.event === "timeout" ? details.timeout_ms : details.elapsed_ms,
376
622
  );
377
- const tokens = formatSubagentTokenCount(
378
- details.event === "timeout" ? undefined : details.usage?.totalTokens,
379
- );
623
+ const usage = details.event === "timeout" ? undefined : details.usage;
624
+ const tokens = formatSubagentTokenCount(usage?.totalTokens);
380
625
  const drainedMessageCount =
381
626
  details.event === "message" || details.event === "timeout"
382
627
  ? 0
@@ -384,11 +629,19 @@ function renderWaitResult(
384
629
  const metrics = [
385
630
  duration,
386
631
  tokens ? `${tokens} tokens` : undefined,
632
+ formatSubagentCost(usage),
387
633
  drainedMessageCount > 0 ? `${drainedMessageCount} messages` : undefined,
388
634
  ].filter((metric): metric is string => metric !== undefined);
389
635
  const summary = renderSubagentSummary(theme, status, agentId, metrics);
390
- if (options.isPartial || !options.expanded) {
391
- return new Text(`${summary}${options.isPartial ? "" : collapsedExpansionHint(theme)}`, 0, 0);
636
+ if (options.isPartial) return new Text(summary, 0, 0);
637
+ if (!options.expanded) {
638
+ const body = collapsedWaitBody(details, status, theme);
639
+ const heading = `${summary}${collapsedExpansionHint(theme)}`;
640
+ if (!body) return new Text(heading, 0, 0);
641
+ const container = new Container();
642
+ container.addChild(new Text(heading, 0, 0));
643
+ container.addChild(body);
644
+ return container;
392
645
  }
393
646
  const container = new Container();
394
647
  container.addChild(new Text(summary, 0, 0));
@@ -460,9 +713,10 @@ function renderDirectStatusRows(
460
713
  return agents.map((agent) => {
461
714
  const duration = formatSubagentDuration(agent.elapsed_ms);
462
715
  const childCount = agent.child_count ?? 0;
463
- const metrics = [duration, childCount > 0 ? `${childCount} children` : undefined].filter(
464
- (metric): metric is string => metric !== undefined,
465
- );
716
+ const metrics = [
717
+ duration,
718
+ childCount > 0 ? `${childCount} ${childCount === 1 ? "child" : "children"}` : undefined,
719
+ ].filter((metric): metric is string => metric !== undefined);
466
720
  return renderSubagentSummary(
467
721
  theme,
468
722
  statusAgentPresentation(agent),
@@ -484,11 +738,19 @@ function renderDetailedStatusAgent(
484
738
  const id = agent.agent_id ?? "agent";
485
739
  const childCount = agent.child_count ?? 0;
486
740
  const duration = formatSubagentDuration(agent.elapsed_ms);
487
- const metrics = [duration, `${childCount} children`].filter(
741
+ const metrics = [duration, `${childCount} ${childCount === 1 ? "child" : "children"}`].filter(
488
742
  (metric): metric is string => metric !== undefined,
489
743
  );
490
744
  const summary = renderSubagentSummary(theme, status, id, metrics);
491
- if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
745
+ if (!options.expanded) {
746
+ const activity = latestProgressActivity(agent.recent_activity ?? []);
747
+ const heading = `${summary}${collapsedExpansionHint(theme)}`;
748
+ if (activity.length === 0) return new Text(heading, 0, 0);
749
+ const collapsed = new Container();
750
+ collapsed.addChild(new Text(heading, 0, 0));
751
+ collapsed.addChild(new ActivityLines(activity, theme));
752
+ return collapsed;
753
+ }
492
754
  const container = new Container();
493
755
  container.addChild(new Text(summary, 0, 0));
494
756
  const labels = [
@@ -510,6 +772,7 @@ function renderDetailedStatusAgent(
510
772
  const launchValues = [
511
773
  `session context ${launchContract.session_context ?? "inherit"}`,
512
774
  `project context ${launchContract.project_context ?? "inherit"}`,
775
+ ...(launchContract.role ? [`role ${launchContract.role}`] : []),
513
776
  `model ${launchContract.model ?? agent.model ?? "unknown"}`,
514
777
  `thinking ${launchContract.thinking_level ?? agent.thinking_level ?? "unknown"}`,
515
778
  `delegation ${launchContract.delegation ?? "none"}`,
@@ -590,12 +853,14 @@ function renderStatusResult(
590
853
  theme.fg("muted", `${counts.children} children`),
591
854
  theme.fg(counts.running > 0 ? "accent" : "dim", `${counts.running} running`),
592
855
  ].join(renderSubagentSeparator(theme));
593
- if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
594
- return new Text(
595
- `${summary}\n${renderDirectStatusRows(details.agents, theme).join("\n") || theme.fg("dim", "(no agents)")}`,
596
- 0,
597
- 0,
856
+ const rows =
857
+ renderDirectStatusRows(details.agents, theme).join("\n") || theme.fg("dim", "(no agents)");
858
+ const container = new Container();
859
+ container.addChild(
860
+ new Text(options.expanded ? summary : `${summary}${collapsedExpansionHint(theme)}`, 0, 0),
598
861
  );
862
+ container.addChild(options.expanded ? new Text(rows, 0, 0) : collapsedTextPreview(rows, theme));
863
+ return container;
599
864
  }
600
865
  return renderDetailedStatusAgent(details.agent, options, theme);
601
866
  }
@@ -609,7 +874,8 @@ function renderCancelResult(
609
874
  const summary =
610
875
  turns.length > 0
611
876
  ? renderSubagentSummary(theme, "cancelled", details.agent_id, [
612
- `${turns.length} turns cancelled`,
877
+ `${turns.length} ${turns.length === 1 ? "turn" : "turns"} cancelled`,
878
+ details.affected_agent_ids.join(", "),
613
879
  ])
614
880
  : renderSubagentSummary(theme, "completed", details.agent_id, ["no active turns"]);
615
881
  if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
@@ -635,9 +901,10 @@ function renderDeleteResult(
635
901
  theme: MinimalSubagentsRenderTheme,
636
902
  ): Component {
637
903
  const status = details.failures.length > 0 ? "failed" : "completed";
904
+ const deletedCount = details.deleted_agent_ids.length;
638
905
  const metrics = [
639
- `${details.deleted_agent_ids.length} agents deleted`,
640
- `${details.deleted_agent_ids.length} tombstones`,
906
+ `${deletedCount} ${deletedCount === 1 ? "agent" : "agents"} deleted`,
907
+ deletedCount > 0 ? details.deleted_agent_ids.join(", ") : undefined,
641
908
  details.failures.length > 0 ? `${details.failures.length} failed` : undefined,
642
909
  ].filter((metric): metric is string => metric !== undefined);
643
910
  const summary = renderSubagentSummary(theme, status, details.agent_id, metrics);
@@ -676,21 +943,24 @@ export function renderCoordinatorToolCall(
676
943
  toolName: CoordinatorToolName,
677
944
  args: CoordinatorToolCallInput,
678
945
  theme: MinimalSubagentsRenderTheme,
946
+ expanded = false,
679
947
  ): Component {
680
948
  const parsed = parseCoordinatorToolCall(toolName, args);
681
949
  if (parsed === undefined) return new Text(coordinatorToolCallTitle(theme, toolName), 0, 0);
682
950
  switch (parsed.toolName) {
683
- case "subagent":
684
- return new Text(
685
- `${coordinatorToolCallTitle(theme, "Subagent")} ${theme.fg("accent", parsed.args.agent_id ?? "generated")}${coordinatorToolCallPreview(theme, parsed.args.task)}`,
686
- 0,
687
- 0,
688
- );
951
+ case "subagent": {
952
+ const header = [
953
+ `${coordinatorToolCallTitle(theme, "Subagent")} ${theme.fg("accent", parsed.args.agent_id ?? "generated")}`,
954
+ ...spawnCallLaunchSummary(parsed.args).map((part) => theme.fg("dim", part)),
955
+ ].join(theme.fg("dim", " · "));
956
+ return renderCallWithText(header, parsed.args.task, theme, expanded);
957
+ }
689
958
  case "agent_message":
690
- return new Text(
691
- `${coordinatorToolCallTitle(theme, "Message")} ${theme.fg("accent", parsed.args.agent_id ?? "parent")}${coordinatorToolCallPreview(theme, parsed.args.message)}`,
692
- 0,
693
- 0,
959
+ return renderCallWithText(
960
+ `${coordinatorToolCallTitle(theme, "Message")} ${theme.fg("accent", parsed.args.agent_id ?? "parent")}`,
961
+ parsed.args.message,
962
+ theme,
963
+ expanded,
694
964
  );
695
965
  case "subagent_wait":
696
966
  return new Text(
@@ -719,7 +989,12 @@ export function renderCoordinatorToolResult(
719
989
  theme: MinimalSubagentsRenderTheme,
720
990
  args: CoordinatorToolCallInput,
721
991
  isError = false,
992
+ renderLiveTurn?: LiveTurnRenderer,
722
993
  ): Component {
994
+ if (toolName === "subagent_wait" && options.isPartial) {
995
+ const progress = parseWaitProgressDetails(result.details);
996
+ if (progress) return renderWaitProgress(progress, options, theme, renderLiveTurn);
997
+ }
723
998
  const parsedResult = parseCoordinatorToolResult(toolName, result.details);
724
999
  if (parsedResult === undefined) return renderFallbackToolResult(result, theme, isError);
725
1000
  const parsedCall = parseCoordinatorToolCall(toolName, args);
@@ -795,30 +1070,24 @@ function renderCoordinatorMessage(
795
1070
  const destination = details?.destination_agent_id ?? "recipient";
796
1071
  const sourceTurn = messageSourceTurn(details);
797
1072
  const route = `${theme.fg("accent", theme.bold(source))} ${theme.fg("dim", "→")} ${theme.fg("accent", theme.bold(destination))}`;
1073
+ const metrics = [formatSubagentDuration(details?.elapsed_ms), formatSubagentCost(details?.usage)];
798
1074
  const heading = [
799
1075
  `${theme.fg(symbol === "✓" ? "success" : "accent", symbol)} ${theme.bold(label)}`,
800
1076
  route,
801
- sourceTurn ? theme.fg("dim", `turn ${sourceTurn}`) : undefined,
802
1077
  details?.status ? renderSubagentStatusLabel(theme, details.status) : undefined,
1078
+ ...metrics.map((metric) => (metric ? theme.fg("muted", metric) : undefined)),
803
1079
  ]
804
1080
  .filter((part): part is string => part !== undefined)
805
1081
  .join(renderSubagentSeparator(theme));
806
1082
  const box = new Box(options.outputPad, 1, (text) => theme.bg("customMessageBg", text));
807
1083
  if (!options.expanded) {
808
- box.addChild(
809
- new Text(
810
- `${heading}\n${theme.fg("muted", formatSubagentPreview(content, currentSubagentPreviewWidth(24)))}`,
811
- 0,
812
- 0,
813
- ),
814
- );
1084
+ box.addChild(new Text(`${heading}${collapsedExpansionHint(theme)}`, 0, 0));
1085
+ box.addChild(collapsedMarkdownPreview(content, theme));
815
1086
  return box;
816
1087
  }
817
1088
  const container = new Container();
818
1089
  container.addChild(new Text(heading, 0, 0));
819
1090
  if (sourceTurn) container.addChild(renderLabelValue(theme, "Source turn", sourceTurn));
820
- const duration = formatSubagentDuration(details?.elapsed_ms);
821
- if (duration) container.addChild(renderLabelValue(theme, "Duration", duration));
822
1091
  container.addChild(new Spacer(1));
823
1092
  container.addChild(new Markdown(content, 0, 0, getMarkdownTheme()));
824
1093
  const usageText = formatSubagentUsage(details?.usage);
@@ -503,7 +503,9 @@ export function findDeliveryEvidence(
503
503
  ))
504
504
  );
505
505
  }
506
- if (waitToolResult && details.event === "message") return false;
506
+ // A message or timeout wait result never carried the turn's terminal result.
507
+ if (waitToolResult && (details.event === "message" || details.event === "timeout"))
508
+ return false;
507
509
  return details.source_agent_id === sourceAgentId && details.source_turn_id === sourceTurnId;
508
510
  });
509
511
  }