@ian-pascoe/pi-minimal-subagents 0.12.0 → 0.13.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.
@@ -90,6 +90,7 @@ const SpawnCallArgumentsSchema = Type.Object({
90
90
  model: Type.Optional(Type.String()),
91
91
  thinking_level: Type.Optional(Type.String()),
92
92
  delegation: Type.Optional(Type.String()),
93
+ tools: Type.Optional(Type.Union([Type.String(), Type.Array(Type.String())])),
93
94
  });
94
95
  const MessageCallArgumentsSchema = Type.Object({
95
96
  agent_id: Type.Optional(Type.String()),
@@ -113,6 +114,7 @@ const CurrentMessageDetailsSchema = Type.Object({
113
114
  agent_id: Type.String(),
114
115
  message_id: Type.Optional(Type.String()),
115
116
  disposition: Type.String(),
117
+ turn_id: Type.Optional(Type.String()),
116
118
  behavior: Type.Optional(Type.String()),
117
119
  error: Type.Optional(Type.String()),
118
120
  });
@@ -209,7 +211,7 @@ const CoordinatorAgentSummaryOutputSchema = Type.Object({
209
211
  latest_activity_at: Type.Optional(Type.String()),
210
212
  task: Type.Optional(Type.String()),
211
213
  child_count: Type.Number(),
212
- children: Type.Array(Type.Unknown()),
214
+ children: Type.Optional(Type.Array(Type.Unknown())),
213
215
  model: Type.String(),
214
216
  thinking_level: Type.String(),
215
217
  });
@@ -266,7 +268,11 @@ export const CoordinatorToolOutputSchemas = {
266
268
  agent_id: Type.String(),
267
269
  turn_id: Type.String(),
268
270
  status: Type.Literal("running"),
269
- agent: Type.Optional(CoordinatorAgentDetailOutputSchema),
271
+ model: Type.String(),
272
+ thinking_level: Type.String(),
273
+ tools: Type.Array(Type.String()),
274
+ delegation: Type.Union([Type.Literal("none"), Type.Literal("fanout")]),
275
+ warnings: Type.Optional(Type.Array(Type.String())),
270
276
  }),
271
277
  agent_message: Type.Object({
272
278
  agent_id: Type.String(),
@@ -274,8 +280,10 @@ export const CoordinatorToolOutputSchemas = {
274
280
  disposition: Type.Union([
275
281
  Type.Literal("delivered-via-wait"),
276
282
  Type.Literal("queued"),
283
+ Type.Literal("started-turn"),
277
284
  Type.Literal("failed"),
278
285
  ]),
286
+ turn_id: Type.Optional(Type.String()),
279
287
  error: Type.Optional(Type.String()),
280
288
  }),
281
289
  subagent_wait: Type.Union([
@@ -319,6 +327,25 @@ const WaitRenderDetailsSchema = Type.Union([
319
327
  WaitTimeoutDetailsSchema,
320
328
  ]);
321
329
 
330
+ /** Partial `subagent_wait` updates: elapsed wait time and, while the child runs, its progress. */
331
+ const WaitProgressRenderDetailsSchema = Type.Object({
332
+ agent_id: Type.String(),
333
+ status: Type.Literal("waiting"),
334
+ elapsed_ms: Type.Number(),
335
+ turn_id: Type.Optional(Type.String()),
336
+ tool_calls: Type.Optional(Type.Number()),
337
+ });
338
+
339
+ export type WaitProgressRenderDetails = Static<typeof WaitProgressRenderDetailsSchema>;
340
+
341
+ /** Parse one partial `subagent_wait` progress update. */
342
+ export function parseWaitProgressDetails(
343
+ // oxlint-disable-next-line anti-slop/no-unknown-parameters -- Partial tool details are unparsed until the progress schema checks them below.
344
+ details: unknown,
345
+ ): WaitProgressRenderDetails | undefined {
346
+ return Value.Check(WaitProgressRenderDetailsSchema, details) ? details : undefined;
347
+ }
348
+
322
349
  export type SpawnCallArguments = Static<typeof SpawnCallArgumentsSchema>;
323
350
  export type MessageCallArguments = Static<typeof MessageCallArgumentsSchema>;
324
351
  export type WaitCallArguments = Static<typeof WaitCallArgumentsSchema>;
@@ -331,6 +358,7 @@ export type StatusRenderDetails = Static<typeof StatusDetailsSchema>;
331
358
  export type CancelRenderDetails = Static<typeof CancelDetailsSchema>;
332
359
  export type DeleteRenderDetails = Static<typeof DeleteDetailsSchema>;
333
360
  export type RenderStatusAgent = Static<typeof RenderStatusAgentSchema>;
361
+ export type RenderRecentActivity = Static<typeof RenderRecentActivitySchema>;
334
362
 
335
363
  /** Raw tool arguments supplied by Pi's tool-rendering interface. */
336
364
  export type CoordinatorToolCallInput = Parameters<NonNullable<ToolDefinition["renderCall"]>>[0];
@@ -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,40 @@ 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
+ profile,
449
+ tools,
450
+ args.session_context && args.session_context !== "omit"
451
+ ? `context ${args.session_context}`
452
+ : undefined,
453
+ args.project_context === "omit" ? "no project context" : undefined,
454
+ args.delegation && args.delegation !== "none" ? args.delegation : undefined,
455
+ ].filter((part): part is string => part !== undefined);
456
+ }
457
+
458
+ /** A call header followed by its long text argument, previewed when collapsed. */
459
+ function renderCallWithText(
460
+ header: string,
461
+ text: string | undefined,
271
462
  theme: MinimalSubagentsRenderTheme,
272
- value: string | undefined,
273
- ): string {
274
- return value && value.length > 0
275
- ? ` · ${theme.fg("dim", `“${formatSubagentPreview(value, currentSubagentPreviewWidth(36))}”`)}`
276
- : "";
463
+ expanded: boolean,
464
+ ): Component {
465
+ if (!text) return new Text(header, 0, 0);
466
+ const container = new Container();
467
+ container.addChild(new Text(header, 0, 0));
468
+ container.addChild(expanded ? new Text(text, 0, 0) : collapsedTextPreview(text, theme));
469
+ return container;
277
470
  }
278
471
 
279
472
  function renderManagementToolCall(
@@ -308,12 +501,11 @@ function renderSpawnResult(
308
501
  const container = new Container();
309
502
  container.addChild(new Text(renderSubagentSummary(theme, status, agentId), 0, 0));
310
503
  container.addChild(renderLabelValue(theme, "Turn", details.turn_id));
311
- appendTextSection(container, theme, "Task", args.task ?? "(task unavailable)");
312
504
  const resolvedModel = launchContract?.model ?? args.model;
313
505
  const resolvedThinking = launchContract?.thinking_level ?? args.thinking_level;
314
506
  const launch = [
315
507
  `delegation ${launchContract?.delegation ?? args.delegation ?? "none"}`,
316
- `session context ${launchContract?.session_context ?? args.session_context ?? "inherit"}`,
508
+ `session context ${launchContract?.session_context ?? args.session_context ?? "omit"}`,
317
509
  `project context ${launchContract?.project_context ?? args.project_context ?? "inherit"}`,
318
510
  resolvedModel ? `model ${resolvedModel}` : undefined,
319
511
  resolvedThinking ? `thinking ${resolvedThinking}` : undefined,
@@ -350,13 +542,65 @@ function renderMessageResult(
350
542
  if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
351
543
  const container = new Container();
352
544
  container.addChild(new Text(summary, 0, 0));
353
- appendTextSection(container, theme, "Message", args.message ?? "(message unavailable)");
354
545
  appendTextSection(container, theme, "Recipient", agentId);
355
546
  appendTextSection(container, theme, "Disposition", disposition);
547
+ if ("turn_id" in details && details.turn_id) {
548
+ appendTextSection(container, theme, "Started turn", details.turn_id);
549
+ }
356
550
  if (details.error) appendTextSection(container, theme, "Error", details.error);
357
551
  return container;
358
552
  }
359
553
 
554
+ /** Renders a running child's turn for a waiting parent; returns nothing when the turn is not live. */
555
+ export type LiveTurnRenderer = (
556
+ agentId: string,
557
+ turnId: string | undefined,
558
+ expanded: boolean,
559
+ ) => Component | undefined;
560
+
561
+ function renderWaitProgress(
562
+ details: WaitProgressRenderDetails,
563
+ options: ToolRenderResultOptions,
564
+ theme: MinimalSubagentsRenderTheme,
565
+ renderLiveTurn: LiveTurnRenderer | undefined,
566
+ ): Component {
567
+ const toolCalls = details.tool_calls ?? 0;
568
+ const metrics = [
569
+ formatSubagentDuration(details.elapsed_ms),
570
+ toolCalls > 0 ? `${toolCalls} tool ${toolCalls === 1 ? "call" : "calls"}` : undefined,
571
+ ].filter((metric): metric is string => metric !== undefined);
572
+ const summary = renderSubagentSummary(theme, "waiting", details.agent_id, metrics);
573
+ const liveTurn = renderLiveTurn?.(details.agent_id, details.turn_id, options.expanded);
574
+ if (!liveTurn) return new Text(summary, 0, 0);
575
+ const container = new Container();
576
+ container.addChild(
577
+ new Text(options.expanded ? summary : `${summary}${collapsedExpansionHint(theme)}`, 0, 0),
578
+ );
579
+ container.addChild(liveTurn);
580
+ return container;
581
+ }
582
+
583
+ /** The collapsed body of a settled wait: what the child said, or why it stopped. */
584
+ function collapsedWaitBody(
585
+ details: WaitRenderDetails,
586
+ status: string,
587
+ theme: MinimalSubagentsRenderTheme,
588
+ ): Component | undefined {
589
+ if (details.event === "message") return collapsedTextPreview(details.message, theme);
590
+ if (details.event === "timeout") {
591
+ const activity = latestProgressActivity(details.agent.recent_activity ?? []);
592
+ return activity.length > 0 ? new ActivityLines(activity, theme) : undefined;
593
+ }
594
+ const output = details.output ?? "";
595
+ if (status === "completed") {
596
+ return output ? collapsedMarkdownPreview(output, theme) : undefined;
597
+ }
598
+ return collapsedTextPreview(
599
+ theme.fg("error", details.error ?? (output || "(no error detail)")),
600
+ theme,
601
+ );
602
+ }
603
+
360
604
  function renderWaitResult(
361
605
  details: WaitRenderDetails,
362
606
  options: ToolRenderResultOptions,
@@ -374,9 +618,8 @@ function renderWaitResult(
374
618
  const duration = formatSubagentDuration(
375
619
  details.event === "timeout" ? details.timeout_ms : details.elapsed_ms,
376
620
  );
377
- const tokens = formatSubagentTokenCount(
378
- details.event === "timeout" ? undefined : details.usage?.totalTokens,
379
- );
621
+ const usage = details.event === "timeout" ? undefined : details.usage;
622
+ const tokens = formatSubagentTokenCount(usage?.totalTokens);
380
623
  const drainedMessageCount =
381
624
  details.event === "message" || details.event === "timeout"
382
625
  ? 0
@@ -384,11 +627,19 @@ function renderWaitResult(
384
627
  const metrics = [
385
628
  duration,
386
629
  tokens ? `${tokens} tokens` : undefined,
630
+ formatSubagentCost(usage),
387
631
  drainedMessageCount > 0 ? `${drainedMessageCount} messages` : undefined,
388
632
  ].filter((metric): metric is string => metric !== undefined);
389
633
  const summary = renderSubagentSummary(theme, status, agentId, metrics);
390
- if (options.isPartial || !options.expanded) {
391
- return new Text(`${summary}${options.isPartial ? "" : collapsedExpansionHint(theme)}`, 0, 0);
634
+ if (options.isPartial) return new Text(summary, 0, 0);
635
+ if (!options.expanded) {
636
+ const body = collapsedWaitBody(details, status, theme);
637
+ const heading = `${summary}${collapsedExpansionHint(theme)}`;
638
+ if (!body) return new Text(heading, 0, 0);
639
+ const container = new Container();
640
+ container.addChild(new Text(heading, 0, 0));
641
+ container.addChild(body);
642
+ return container;
392
643
  }
393
644
  const container = new Container();
394
645
  container.addChild(new Text(summary, 0, 0));
@@ -460,9 +711,10 @@ function renderDirectStatusRows(
460
711
  return agents.map((agent) => {
461
712
  const duration = formatSubagentDuration(agent.elapsed_ms);
462
713
  const childCount = agent.child_count ?? 0;
463
- const metrics = [duration, childCount > 0 ? `${childCount} children` : undefined].filter(
464
- (metric): metric is string => metric !== undefined,
465
- );
714
+ const metrics = [
715
+ duration,
716
+ childCount > 0 ? `${childCount} ${childCount === 1 ? "child" : "children"}` : undefined,
717
+ ].filter((metric): metric is string => metric !== undefined);
466
718
  return renderSubagentSummary(
467
719
  theme,
468
720
  statusAgentPresentation(agent),
@@ -484,11 +736,19 @@ function renderDetailedStatusAgent(
484
736
  const id = agent.agent_id ?? "agent";
485
737
  const childCount = agent.child_count ?? 0;
486
738
  const duration = formatSubagentDuration(agent.elapsed_ms);
487
- const metrics = [duration, `${childCount} children`].filter(
739
+ const metrics = [duration, `${childCount} ${childCount === 1 ? "child" : "children"}`].filter(
488
740
  (metric): metric is string => metric !== undefined,
489
741
  );
490
742
  const summary = renderSubagentSummary(theme, status, id, metrics);
491
- if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
743
+ if (!options.expanded) {
744
+ const activity = latestProgressActivity(agent.recent_activity ?? []);
745
+ const heading = `${summary}${collapsedExpansionHint(theme)}`;
746
+ if (activity.length === 0) return new Text(heading, 0, 0);
747
+ const collapsed = new Container();
748
+ collapsed.addChild(new Text(heading, 0, 0));
749
+ collapsed.addChild(new ActivityLines(activity, theme));
750
+ return collapsed;
751
+ }
492
752
  const container = new Container();
493
753
  container.addChild(new Text(summary, 0, 0));
494
754
  const labels = [
@@ -590,12 +850,14 @@ function renderStatusResult(
590
850
  theme.fg("muted", `${counts.children} children`),
591
851
  theme.fg(counts.running > 0 ? "accent" : "dim", `${counts.running} running`),
592
852
  ].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,
853
+ const rows =
854
+ renderDirectStatusRows(details.agents, theme).join("\n") || theme.fg("dim", "(no agents)");
855
+ const container = new Container();
856
+ container.addChild(
857
+ new Text(options.expanded ? summary : `${summary}${collapsedExpansionHint(theme)}`, 0, 0),
598
858
  );
859
+ container.addChild(options.expanded ? new Text(rows, 0, 0) : collapsedTextPreview(rows, theme));
860
+ return container;
599
861
  }
600
862
  return renderDetailedStatusAgent(details.agent, options, theme);
601
863
  }
@@ -609,7 +871,8 @@ function renderCancelResult(
609
871
  const summary =
610
872
  turns.length > 0
611
873
  ? renderSubagentSummary(theme, "cancelled", details.agent_id, [
612
- `${turns.length} turns cancelled`,
874
+ `${turns.length} ${turns.length === 1 ? "turn" : "turns"} cancelled`,
875
+ details.affected_agent_ids.join(", "),
613
876
  ])
614
877
  : renderSubagentSummary(theme, "completed", details.agent_id, ["no active turns"]);
615
878
  if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
@@ -635,9 +898,10 @@ function renderDeleteResult(
635
898
  theme: MinimalSubagentsRenderTheme,
636
899
  ): Component {
637
900
  const status = details.failures.length > 0 ? "failed" : "completed";
901
+ const deletedCount = details.deleted_agent_ids.length;
638
902
  const metrics = [
639
- `${details.deleted_agent_ids.length} agents deleted`,
640
- `${details.deleted_agent_ids.length} tombstones`,
903
+ `${deletedCount} ${deletedCount === 1 ? "agent" : "agents"} deleted`,
904
+ deletedCount > 0 ? details.deleted_agent_ids.join(", ") : undefined,
641
905
  details.failures.length > 0 ? `${details.failures.length} failed` : undefined,
642
906
  ].filter((metric): metric is string => metric !== undefined);
643
907
  const summary = renderSubagentSummary(theme, status, details.agent_id, metrics);
@@ -676,21 +940,24 @@ export function renderCoordinatorToolCall(
676
940
  toolName: CoordinatorToolName,
677
941
  args: CoordinatorToolCallInput,
678
942
  theme: MinimalSubagentsRenderTheme,
943
+ expanded = false,
679
944
  ): Component {
680
945
  const parsed = parseCoordinatorToolCall(toolName, args);
681
946
  if (parsed === undefined) return new Text(coordinatorToolCallTitle(theme, toolName), 0, 0);
682
947
  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
- );
948
+ case "subagent": {
949
+ const header = [
950
+ `${coordinatorToolCallTitle(theme, "Subagent")} ${theme.fg("accent", parsed.args.agent_id ?? "generated")}`,
951
+ ...spawnCallLaunchSummary(parsed.args).map((part) => theme.fg("dim", part)),
952
+ ].join(theme.fg("dim", " · "));
953
+ return renderCallWithText(header, parsed.args.task, theme, expanded);
954
+ }
689
955
  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,
956
+ return renderCallWithText(
957
+ `${coordinatorToolCallTitle(theme, "Message")} ${theme.fg("accent", parsed.args.agent_id ?? "parent")}`,
958
+ parsed.args.message,
959
+ theme,
960
+ expanded,
694
961
  );
695
962
  case "subagent_wait":
696
963
  return new Text(
@@ -719,7 +986,12 @@ export function renderCoordinatorToolResult(
719
986
  theme: MinimalSubagentsRenderTheme,
720
987
  args: CoordinatorToolCallInput,
721
988
  isError = false,
989
+ renderLiveTurn?: LiveTurnRenderer,
722
990
  ): Component {
991
+ if (toolName === "subagent_wait" && options.isPartial) {
992
+ const progress = parseWaitProgressDetails(result.details);
993
+ if (progress) return renderWaitProgress(progress, options, theme, renderLiveTurn);
994
+ }
723
995
  const parsedResult = parseCoordinatorToolResult(toolName, result.details);
724
996
  if (parsedResult === undefined) return renderFallbackToolResult(result, theme, isError);
725
997
  const parsedCall = parseCoordinatorToolCall(toolName, args);
@@ -795,30 +1067,24 @@ function renderCoordinatorMessage(
795
1067
  const destination = details?.destination_agent_id ?? "recipient";
796
1068
  const sourceTurn = messageSourceTurn(details);
797
1069
  const route = `${theme.fg("accent", theme.bold(source))} ${theme.fg("dim", "→")} ${theme.fg("accent", theme.bold(destination))}`;
1070
+ const metrics = [formatSubagentDuration(details?.elapsed_ms), formatSubagentCost(details?.usage)];
798
1071
  const heading = [
799
1072
  `${theme.fg(symbol === "✓" ? "success" : "accent", symbol)} ${theme.bold(label)}`,
800
1073
  route,
801
- sourceTurn ? theme.fg("dim", `turn ${sourceTurn}`) : undefined,
802
1074
  details?.status ? renderSubagentStatusLabel(theme, details.status) : undefined,
1075
+ ...metrics.map((metric) => (metric ? theme.fg("muted", metric) : undefined)),
803
1076
  ]
804
1077
  .filter((part): part is string => part !== undefined)
805
1078
  .join(renderSubagentSeparator(theme));
806
1079
  const box = new Box(options.outputPad, 1, (text) => theme.bg("customMessageBg", text));
807
1080
  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
- );
1081
+ box.addChild(new Text(`${heading}${collapsedExpansionHint(theme)}`, 0, 0));
1082
+ box.addChild(collapsedMarkdownPreview(content, theme));
815
1083
  return box;
816
1084
  }
817
1085
  const container = new Container();
818
1086
  container.addChild(new Text(heading, 0, 0));
819
1087
  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
1088
  container.addChild(new Spacer(1));
823
1089
  container.addChild(new Markdown(content, 0, 0, getMarkdownTheme()));
824
1090
  const usageText = formatSubagentUsage(details?.usage);