@ian-pascoe/pi-minimal-subagents 0.16.0 → 0.17.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,16 +1,25 @@
1
1
  import type { Usage } from "@earendil-works/pi-ai";
2
2
  import {
3
3
  getMarkdownTheme,
4
- keyHint,
5
4
  type AgentToolResult,
6
5
  type MessageRenderer,
7
6
  type MessageRenderOptions,
8
7
  type Theme,
9
- type ThemeColor,
10
8
  type ToolRenderResultOptions,
11
9
  } from "@earendil-works/pi-coding-agent";
12
10
  import {
13
- Box,
11
+ COLLAPSED_LINES,
12
+ CollapsedPreview,
13
+ customMessageBox,
14
+ joinInline,
15
+ previewBody,
16
+ statusMark,
17
+ summaryExpandHint,
18
+ toolHeader,
19
+ treePrefix,
20
+ type StatusKind,
21
+ } from "@ian-pascoe/pi-utils/ui";
22
+ import {
14
23
  Container,
15
24
  Markdown,
16
25
  sliceByColumn,
@@ -56,40 +65,19 @@ export type MinimalSubagentsStatusTheme = Pick<Theme, "fg">;
56
65
 
57
66
  type RenderableCoordinatorMessage = Pick<Parameters<MessageRenderer>[0], "content" | "details">;
58
67
 
59
- type SubagentPresentationStatus =
60
- | "running"
61
- | "waiting"
62
- | "completed"
63
- | "failed"
64
- | "cancelled"
65
- | "interrupted"
66
- | "unavailable"
67
- | "idle"
68
- | "delivered"
69
- | "delivered-via-wait"
70
- | "queued"
71
- | "started-turn"
72
- | "message"
73
- | "timed out";
74
-
75
- type SubagentStatusPresentation = { readonly symbol: string; readonly color: ThemeColor };
76
-
77
- const SUBAGENT_STATUS_PRESENTATION = {
78
- running: { symbol: "◉", color: "accent" },
79
- waiting: { symbol: "◌", color: "accent" },
80
- completed: { symbol: "✓", color: "success" },
81
- failed: { symbol: "×", color: "error" },
82
- cancelled: { symbol: "■", color: "warning" },
83
- interrupted: { symbol: "!", color: "warning" },
84
- unavailable: { symbol: "!", color: "warning" },
85
- idle: { symbol: "○", color: "dim" },
86
- delivered: { symbol: "→", color: "accent" },
87
- "delivered-via-wait": { symbol: "→", color: "accent" },
88
- queued: { symbol: "↗", color: "accent" },
89
- "started-turn": { symbol: "◉", color: "accent" },
90
- message: { symbol: "→", color: "accent" },
91
- "timed out": { symbol: "!", color: "warning" },
92
- } satisfies { readonly [Status in SubagentPresentationStatus]: SubagentStatusPresentation };
68
+ /** Status Mark kind of each subagent status that has one; any other status reads as idle. */
69
+ const SUBAGENT_STATUS_KIND = new Map<string, StatusKind>([
70
+ ["running", "active"],
71
+ ["waiting", "active"],
72
+ ["started-turn", "active"],
73
+ ["completed", "done"],
74
+ ["failed", "failed"],
75
+ ["cancelled", "stopped"],
76
+ ["interrupted", "warning"],
77
+ ["unavailable", "warning"],
78
+ ["timed out", "warning"],
79
+ ["idle", "idle"],
80
+ ]);
93
81
 
94
82
  function coordinatorMessageText(content: RenderableCoordinatorMessage["content"]): string {
95
83
  if (!Array.isArray(content)) return stripCoordinatorMessageEnvelope(content);
@@ -134,46 +122,64 @@ export function subagentStatusLadder(agent: {
134
122
  return agent.latest_turn?.status ?? "idle";
135
123
  }
136
124
 
137
- function subagentStatusPresentation(status: string): SubagentStatusPresentation {
138
- // SAFETY: unknown statuses fall back to the idle presentation below.
139
- const known = status as keyof typeof SUBAGENT_STATUS_PRESENTATION;
140
- return SUBAGENT_STATUS_PRESENTATION[known] ?? SUBAGENT_STATUS_PRESENTATION.idle;
125
+ function subagentStatusKind(status: string): StatusKind {
126
+ return SUBAGENT_STATUS_KIND.get(status) ?? "idle";
141
127
  }
142
128
 
143
- /** Render the shared semantic symbol and color for one subagent status. */
144
- export function renderSubagentStatusSymbol(
129
+ /** Render the Status Mark of one subagent status, for widgets and overlay rows. */
130
+ export function renderSubagentStatusMark(
145
131
  theme: MinimalSubagentsStatusTheme,
146
132
  status: string,
147
133
  ): string {
148
- const presentation = subagentStatusPresentation(status);
149
- return theme.fg(presentation.color, presentation.symbol);
134
+ return statusMark(theme, subagentStatusKind(status));
150
135
  }
151
136
 
152
- /** Render a subagent status label with the same semantic color as its symbol. */
137
+ /** Render a subagent status word: `error` when failed, `warning` when it needs attention, else `muted`. */
153
138
  export function renderSubagentStatusLabel(
154
139
  theme: MinimalSubagentsStatusTheme,
155
140
  status: string,
156
141
  ): string {
157
- const presentation = subagentStatusPresentation(status);
158
- return theme.fg(presentation.color, status);
142
+ const kind = subagentStatusKind(status);
143
+ return theme.fg(kind === "failed" ? "error" : kind === "warning" ? "warning" : "muted", status);
159
144
  }
160
145
 
161
- function renderSubagentSeparator(theme: MinimalSubagentsRenderTheme): string {
162
- return theme.fg("dim", " · ");
146
+ /**
147
+ * Tree prefixes for rows listed in pre-order with their depth: nothing for a root row, then
148
+ * `├─ `/`└─ ` with `│ ` guides for the ancestors that still have later siblings.
149
+ */
150
+ export function treeRowPrefixes(depths: readonly number[]): string[] {
151
+ const isLastAt = (index: number): boolean => {
152
+ const depth = depths[index] ?? 0;
153
+ for (let next = index + 1; next < depths.length; next++) {
154
+ const nextDepth = depths[next] ?? 0;
155
+ if (nextDepth < depth) return true;
156
+ if (nextDepth === depth) return false;
157
+ }
158
+ return true;
159
+ };
160
+ return depths.map((depth, index) => {
161
+ if (depth === 0) return "";
162
+ const ancestorsLast: boolean[] = [];
163
+ let cursor = index - 1;
164
+ for (let level = depth - 1; level >= 1; level--) {
165
+ while (cursor >= 0 && depths[cursor] !== level) cursor--;
166
+ ancestorsLast.unshift(cursor < 0 || isLastAt(cursor));
167
+ }
168
+ return treePrefix(ancestorsLast, isLastAt(index));
169
+ });
163
170
  }
164
171
 
165
172
  function renderSubagentSummary(
166
173
  theme: MinimalSubagentsRenderTheme,
167
- status: string,
174
+ status: string | undefined,
168
175
  agentId: string,
169
176
  metrics: readonly string[] = [],
170
177
  ): string {
171
- const identity = `${renderSubagentStatusSymbol(theme, status)} ${theme.fg("accent", theme.bold(agentId))}`;
172
- return [
173
- identity,
174
- renderSubagentStatusLabel(theme, status),
175
- ...metrics.map((metric) => theme.fg("muted", metric)),
176
- ].join(renderSubagentSeparator(theme));
178
+ return joinInline(theme, [
179
+ theme.fg("accent", agentId),
180
+ status === undefined ? undefined : renderSubagentStatusLabel(theme, status),
181
+ ...metrics.filter(Boolean).map((metric) => theme.fg("muted", metric)),
182
+ ]);
177
183
  }
178
184
 
179
185
  function renderLabelValue(theme: MinimalSubagentsRenderTheme, label: string, value: string): Text {
@@ -213,58 +219,50 @@ function renderFallbackToolResult(
213
219
  result: AgentToolResult<unknown>,
214
220
  theme: MinimalSubagentsRenderTheme,
215
221
  isError: boolean,
222
+ expanded: boolean,
216
223
  ): Component {
217
224
  const content = toolResultText(result) || "(no output)";
218
- return new Text(isError ? theme.fg("error", content) : content, 0, 0);
219
- }
220
-
221
- function collapsedExpansionHint(theme: MinimalSubagentsRenderTheme): string {
222
- return theme.fg("dim", ` · ${keyHint("app.tools.expand", "to expand")}`);
223
- }
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;
225
+ return new Text(
226
+ previewBody(theme, content.split("\n"), {
227
+ limit: COLLAPSED_LINES.fallback,
228
+ expanded,
229
+ color: isError ? "error" : "toolOutput",
230
+ }).join("\n"),
231
+ 0,
232
+ 0,
233
+ );
230
234
  }
231
235
 
232
- function withoutTrailingBlankLines(lines: string[]): string[] {
233
- const end = lines.findLastIndex((line) => !isBlankRenderedLine(line));
234
- return lines.slice(0, end + 1);
236
+ /**
237
+ * A collapsed result: its one-line summary above a body preview, or, with no body, the summary
238
+ * alone ending in Pi's summary Expand Hint.
239
+ */
240
+ function collapsedWithBody(
241
+ theme: MinimalSubagentsRenderTheme,
242
+ summary: string,
243
+ body: Component | undefined,
244
+ ): Component {
245
+ if (!body) return new Text(`${summary}${summaryExpandHint(theme)}`, 0, 0);
246
+ const container = new Container();
247
+ container.addChild(new Text(summary, 0, 0));
248
+ container.addChild(body);
249
+ return container;
235
250
  }
236
251
 
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
- }
252
+ /** Pre-styled text in its Collapsed View: wrapped, clipped to `limit` lines, with Pi's Expand Hint. */
253
+ function collapsedText(
254
+ theme: MinimalSubagentsRenderTheme,
255
+ content: string,
256
+ limit: number = COLLAPSED_LINES.fallback,
257
+ ): Component {
258
+ return new CollapsedPreview(theme, new Text(content, 0, 0), { limit, expanded: false });
260
259
  }
261
260
 
262
261
  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);
262
+ return new CollapsedPreview(theme, new Markdown(content, 0, 0, getMarkdownTheme()), {
263
+ limit: COLLAPSED_LINES.fallback,
264
+ expanded: false,
265
+ });
268
266
  }
269
267
 
270
268
  const ToolArgumentsSchema = Type.Record(Type.String(), Type.Unknown());
@@ -307,13 +305,13 @@ function formatActivityLine(
307
305
  formatSubagentPreview(text, Math.max(1, width - visibleWidth(prefix)));
308
306
  if (activity.label.startsWith("tool call ")) {
309
307
  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)))}`;
308
+ const prefix = `${name} `;
309
+ return `${theme.fg("toolTitle", name)} ${theme.fg("dim", fit(prefix, toolArgumentSummary(activity.content)))}`;
312
310
  }
313
311
  if (activity.label.startsWith("tool result ")) {
314
312
  const failed = activity.label.endsWith(" (error)");
315
313
  const name = activity.label.slice("tool result ".length).replace(/ \(error\)$/, "");
316
- const prefix = `${failed ? "\u2717" : "\u2190"} ${name}: `;
314
+ const prefix = `${name}: `;
317
315
  return theme.fg(failed ? "error" : "dim", `${prefix}${fit(prefix, activity.content)}`);
318
316
  }
319
317
  if (activity.label === "reasoning") {
@@ -338,9 +336,11 @@ export function drawActivityRail(
338
336
  return items.flatMap((lines, index) => {
339
337
  const last = index === items.length - 1;
340
338
  const [first = "", ...rest] = lines;
339
+ // The guide for one ancestor is the leading segment of a nested row's tree prefix.
340
+ const guide = treePrefix([last], true).slice(0, ACTIVITY_RAIL_WIDTH);
341
341
  return [
342
- `${theme.fg("dim", last ? "└─ " : "├─ ")}${first}`,
343
- ...rest.map((line) => `${theme.fg("dim", last ? " " : "│ ")}${line}`),
342
+ `${theme.fg("dim", treePrefix([], last))}${first}`,
343
+ ...rest.map((line) => `${theme.fg("dim", guide)}${line}`),
344
344
  ];
345
345
  });
346
346
  }
@@ -411,8 +411,8 @@ export function formatSubagentPreview(content: string | undefined, maxWidth = 72
411
411
  const singleLine = (content ?? "").replace(/\s+/g, " ").trim();
412
412
  const boundedWidth = Math.max(1, maxWidth);
413
413
  if (visibleWidth(singleLine) <= boundedWidth) return singleLine;
414
- if (boundedWidth === 1) return "…";
415
- return `${sliceByColumn(singleLine, 0, boundedWidth - 1, true).trimEnd()}…`;
414
+ if (boundedWidth <= 3) return ".".repeat(boundedWidth);
415
+ return `${sliceByColumn(singleLine, 0, boundedWidth - 3, true).trimEnd()}...`;
416
416
  }
417
417
 
418
418
  /** Format complete Pi usage metrics for expanded subagent output. */
@@ -429,10 +429,6 @@ export function formatSubagentUsage(usage: Usage | undefined): string | undefine
429
429
  return values.join(" · ");
430
430
  }
431
431
 
432
- function coordinatorToolCallTitle(theme: MinimalSubagentsRenderTheme, label: string): string {
433
- return theme.fg("toolTitle", theme.bold(label));
434
- }
435
-
436
432
  /** Launch settings the caller chose explicitly; defaults are omitted to keep the header short. */
437
433
  function spawnCallLaunchSummary(args: SpawnCallArguments): string[] {
438
434
  const profile = args.model
@@ -464,19 +460,28 @@ function renderCallWithText(
464
460
  expanded: boolean,
465
461
  ): Component {
466
462
  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;
463
+ return new Text(
464
+ [
465
+ header,
466
+ ...previewBody(theme, text.split("\n"), { limit: COLLAPSED_LINES.fallback, expanded }),
467
+ ].join("\n"),
468
+ 0,
469
+ 0,
470
+ );
471
471
  }
472
472
 
473
473
  function renderManagementToolCall(
474
- label: string,
474
+ toolName: "subagent_cancel" | "subagent_delete",
475
475
  args: ManagementCallArguments,
476
476
  theme: MinimalSubagentsRenderTheme,
477
477
  ): Component {
478
478
  return new Text(
479
- `${coordinatorToolCallTitle(theme, label)} ${theme.fg("accent", args.agent_id ?? "agent")} ${theme.fg("dim", args.recursive === false ? "· target only" : "· recursive")}`,
479
+ toolHeader(
480
+ theme,
481
+ toolName,
482
+ args.agent_id ?? "agent",
483
+ args.recursive === false ? "target only" : "recursive",
484
+ ),
480
485
  0,
481
486
  0,
482
487
  );
@@ -493,11 +498,7 @@ function renderSpawnResult(
493
498
  const agent = details.agent;
494
499
  const launchContract = agent?.launch_contract;
495
500
  if (!options.expanded) {
496
- return new Text(
497
- `${renderSubagentSummary(theme, status, agentId)}${collapsedExpansionHint(theme)}`,
498
- 0,
499
- 0,
500
- );
501
+ return collapsedWithBody(theme, renderSubagentSummary(theme, status, agentId), undefined);
501
502
  }
502
503
  const container = new Container();
503
504
  container.addChild(new Text(renderSubagentSummary(theme, status, agentId), 0, 0));
@@ -541,7 +542,13 @@ function renderMessageResult(
541
542
  const metrics = historicalBehavior ? [historicalBehavior] : [];
542
543
  const disposition = messageDisposition(details);
543
544
  const summary = renderSubagentSummary(theme, disposition, agentId, metrics);
544
- if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
545
+ if (!options.expanded) {
546
+ return collapsedWithBody(
547
+ theme,
548
+ summary,
549
+ details.error ? collapsedText(theme, theme.fg("error", details.error)) : undefined,
550
+ );
551
+ }
545
552
  const container = new Container();
546
553
  container.addChild(new Text(summary, 0, 0));
547
554
  appendTextSection(container, theme, "Recipient", agentId);
@@ -576,7 +583,7 @@ function renderWaitProgress(
576
583
  if (!liveTurn) return new Text(summary, 0, 0);
577
584
  const container = new Container();
578
585
  container.addChild(
579
- new Text(options.expanded ? summary : `${summary}${collapsedExpansionHint(theme)}`, 0, 0),
586
+ new Text(options.expanded ? summary : `${summary}${summaryExpandHint(theme)}`, 0, 0),
580
587
  );
581
588
  container.addChild(liveTurn);
582
589
  return container;
@@ -601,24 +608,21 @@ function collapsedWaitBody(
601
608
  status: string,
602
609
  theme: MinimalSubagentsRenderTheme,
603
610
  ): Component | undefined {
604
- if (details.event === "message") return collapsedTextPreview(details.message, theme);
611
+ if (details.event === "message") return collapsedText(theme, details.message);
605
612
  if (details.event === "timeout") {
606
613
  const labels = timeoutActivityLabels(details);
607
614
  return labels.length > 0
608
- ? collapsedTextPreview(theme.fg("dim", labels.join(" \u00b7 ")), theme)
615
+ ? collapsedText(theme, theme.fg("dim", labels.join(" \u00b7 ")))
609
616
  : undefined;
610
617
  }
611
618
  if (details.already_delivered) {
612
- return collapsedTextPreview(theme.fg("muted", ALREADY_DELIVERED_TEXT), theme);
619
+ return collapsedText(theme, theme.fg("muted", ALREADY_DELIVERED_TEXT));
613
620
  }
614
621
  const output = details.output ?? "";
615
622
  if (status === "completed") {
616
623
  return output ? collapsedMarkdownPreview(output, theme) : undefined;
617
624
  }
618
- return collapsedTextPreview(
619
- theme.fg("error", details.error ?? (output || "(no error detail)")),
620
- theme,
621
- );
625
+ return collapsedText(theme, theme.fg("error", details.error ?? (output || "(no error detail)")));
622
626
  }
623
627
 
624
628
  function renderWaitResult(
@@ -655,13 +659,7 @@ function renderWaitResult(
655
659
  const summary = renderSubagentSummary(theme, status, agentId, metrics);
656
660
  if (options.isPartial) return new Text(summary, 0, 0);
657
661
  if (!options.expanded) {
658
- const body = collapsedWaitBody(details, status, theme);
659
- const heading = `${summary}${collapsedExpansionHint(theme)}`;
660
- if (!body) return new Text(heading, 0, 0);
661
- const container = new Container();
662
- container.addChild(new Text(heading, 0, 0));
663
- container.addChild(body);
664
- return container;
662
+ return collapsedWithBody(theme, summary, collapsedWaitBody(details, status, theme));
665
663
  }
666
664
  const container = new Container();
667
665
  container.addChild(new Text(summary, 0, 0));
@@ -769,12 +767,11 @@ function renderDetailedStatusAgent(
769
767
  const summary = renderSubagentSummary(theme, status, id, metrics);
770
768
  if (!options.expanded) {
771
769
  const activity = latestProgressActivity(agent.recent_activity ?? []);
772
- const heading = `${summary}${collapsedExpansionHint(theme)}`;
773
- if (activity.length === 0) return new Text(heading, 0, 0);
774
- const collapsed = new Container();
775
- collapsed.addChild(new Text(heading, 0, 0));
776
- collapsed.addChild(new ActivityLines(activity, theme));
777
- return collapsed;
770
+ return collapsedWithBody(
771
+ theme,
772
+ summary,
773
+ activity.length === 0 ? undefined : new ActivityLines(activity, theme),
774
+ );
778
775
  }
779
776
  const container = new Container();
780
777
  container.addChild(new Text(summary, 0, 0));
@@ -874,17 +871,20 @@ function renderStatusResult(
874
871
  ): Component {
875
872
  if ("agents" in details) {
876
873
  const counts = countDirectStatusAgents(details.agents);
877
- const summary = [
874
+ const summary = joinInline(theme, [
878
875
  theme.fg("muted", `${counts.children} children`),
879
876
  theme.fg(counts.running > 0 ? "accent" : "dim", `${counts.running} running`),
880
- ].join(renderSubagentSeparator(theme));
881
- const rows =
882
- renderDirectStatusRows(details.agents, theme).join("\n") || theme.fg("dim", "(no agents)");
877
+ ]);
878
+ const rows = renderDirectStatusRows(details.agents, theme);
883
879
  const container = new Container();
880
+ container.addChild(new Text(summary, 0, 0));
884
881
  container.addChild(
885
- new Text(options.expanded ? summary : `${summary}${collapsedExpansionHint(theme)}`, 0, 0),
882
+ new CollapsedPreview(
883
+ theme,
884
+ new Text(rows.length === 0 ? theme.fg("dim", "(no agents)") : rows.join("\n"), 0, 0),
885
+ { limit: COLLAPSED_LINES.list, expanded: options.expanded },
886
+ ),
886
887
  );
887
- container.addChild(options.expanded ? new Text(rows, 0, 0) : collapsedTextPreview(rows, theme));
888
888
  return container;
889
889
  }
890
890
  return renderDetailedStatusAgent(details.agent, options, theme);
@@ -898,12 +898,12 @@ function renderCancelResult(
898
898
  const turns = details.cancelled_turn_ids;
899
899
  const summary =
900
900
  turns.length > 0
901
- ? renderSubagentSummary(theme, "cancelled", details.agent_id, [
901
+ ? renderSubagentSummary(theme, undefined, details.agent_id, [
902
902
  `${turns.length} ${turns.length === 1 ? "turn" : "turns"} cancelled`,
903
903
  details.affected_agent_ids.join(", "),
904
904
  ])
905
- : renderSubagentSummary(theme, "completed", details.agent_id, ["no active turns"]);
906
- if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
905
+ : renderSubagentSummary(theme, undefined, details.agent_id, ["no active turns"]);
906
+ if (!options.expanded) return collapsedWithBody(theme, summary, undefined);
907
907
  const container = new Container();
908
908
  container.addChild(new Text(summary, 0, 0));
909
909
  container.addChild(renderLabelValue(theme, "Requested target", details.agent_id));
@@ -925,7 +925,7 @@ function renderDeleteResult(
925
925
  options: ToolRenderResultOptions,
926
926
  theme: MinimalSubagentsRenderTheme,
927
927
  ): Component {
928
- const status = details.failures.length > 0 ? "failed" : "completed";
928
+ const status = details.failures.length > 0 ? "failed" : undefined;
929
929
  const deletedCount = details.deleted_agent_ids.length;
930
930
  const metrics = [
931
931
  `${deletedCount} ${deletedCount === 1 ? "agent" : "agents"} deleted`,
@@ -933,7 +933,21 @@ function renderDeleteResult(
933
933
  details.failures.length > 0 ? `${details.failures.length} failed` : undefined,
934
934
  ].filter((metric): metric is string => metric !== undefined);
935
935
  const summary = renderSubagentSummary(theme, status, details.agent_id, metrics);
936
- if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
936
+ if (!options.expanded) {
937
+ return collapsedWithBody(
938
+ theme,
939
+ summary,
940
+ details.failures.length > 0
941
+ ? collapsedText(
942
+ theme,
943
+ theme.fg(
944
+ "error",
945
+ details.failures.map((failure) => `${failure.agent_id}: ${failure.error}`).join("\n"),
946
+ ),
947
+ )
948
+ : undefined,
949
+ );
950
+ }
937
951
  const container = new Container();
938
952
  container.addChild(new Text(summary, 0, 0));
939
953
  container.addChild(renderLabelValue(theme, "Requested target", details.agent_id));
@@ -971,38 +985,41 @@ export function renderCoordinatorToolCall(
971
985
  expanded = false,
972
986
  ): Component {
973
987
  const parsed = parseCoordinatorToolCall(toolName, args);
974
- if (parsed === undefined) return new Text(coordinatorToolCallTitle(theme, toolName), 0, 0);
988
+ if (parsed === undefined) return new Text(toolHeader(theme, toolName), 0, 0);
975
989
  switch (parsed.toolName) {
976
990
  case "subagent": {
977
- const header = [
978
- `${coordinatorToolCallTitle(theme, "Subagent")} ${theme.fg("accent", parsed.args.agent_id ?? "generated")}`,
979
- ...spawnCallLaunchSummary(parsed.args).map((part) => theme.fg("dim", part)),
980
- ].join(theme.fg("dim", " · "));
991
+ const header = toolHeader(
992
+ theme,
993
+ "subagent",
994
+ parsed.args.agent_id ?? "generated",
995
+ spawnCallLaunchSummary(parsed.args).join(" · ") || undefined,
996
+ );
981
997
  return renderCallWithText(header, parsed.args.task, theme, expanded);
982
998
  }
983
999
  case "agent_message":
984
1000
  return renderCallWithText(
985
- `${coordinatorToolCallTitle(theme, "Message")} ${theme.fg("accent", parsed.args.agent_id ?? "parent")}`,
1001
+ toolHeader(theme, "agent_message", parsed.args.agent_id ?? "parent"),
986
1002
  parsed.args.message,
987
1003
  theme,
988
1004
  expanded,
989
1005
  );
990
1006
  case "subagent_wait":
991
- return new Text(
992
- `${coordinatorToolCallTitle(theme, "Wait")} ${theme.fg("accent", parsed.args.agent_id ?? "agent")}`,
993
- 0,
994
- 0,
995
- );
1007
+ return new Text(toolHeader(theme, "subagent_wait", parsed.args.agent_id ?? "agent"), 0, 0);
996
1008
  case "subagent_status":
997
1009
  return new Text(
998
- `${coordinatorToolCallTitle(theme, "Status")} ${theme.fg("accent", parsed.args.agent_id ?? "children")}`,
1010
+ toolHeader(
1011
+ theme,
1012
+ "subagent_status",
1013
+ parsed.args.agent_id,
1014
+ parsed.args.agent_id === undefined ? "children" : undefined,
1015
+ ),
999
1016
  0,
1000
1017
  0,
1001
1018
  );
1002
1019
  case "subagent_cancel":
1003
- return renderManagementToolCall("Cancel", parsed.args, theme);
1020
+ return renderManagementToolCall("subagent_cancel", parsed.args, theme);
1004
1021
  case "subagent_delete":
1005
- return renderManagementToolCall("Delete", parsed.args, theme);
1022
+ return renderManagementToolCall("subagent_delete", parsed.args, theme);
1006
1023
  }
1007
1024
  }
1008
1025
 
@@ -1021,7 +1038,9 @@ export function renderCoordinatorToolResult(
1021
1038
  if (progress) return renderWaitProgress(progress, options, theme, renderLiveTurn);
1022
1039
  }
1023
1040
  const parsedResult = parseCoordinatorToolResult(toolName, result.details);
1024
- if (parsedResult === undefined) return renderFallbackToolResult(result, theme, isError);
1041
+ if (parsedResult === undefined) {
1042
+ return renderFallbackToolResult(result, theme, isError, options.expanded);
1043
+ }
1025
1044
  const parsedCall = parseCoordinatorToolCall(toolName, args);
1026
1045
  switch (parsedResult.toolName) {
1027
1046
  case "subagent":
@@ -1060,7 +1079,7 @@ export function renderMinimalSubagentsMessage(
1060
1079
  options: MessageRenderOptions,
1061
1080
  theme: MinimalSubagentsRenderTheme,
1062
1081
  ): Component {
1063
- return renderCoordinatorMessage("Agent message", "→", message, options, theme);
1082
+ return renderCoordinatorMessage("message", message, options, theme);
1064
1083
  }
1065
1084
 
1066
1085
  /** Render automatic successful agent results with expandable Markdown output. */
@@ -1069,7 +1088,7 @@ export function renderMinimalSubagentsResult(
1069
1088
  options: MessageRenderOptions,
1070
1089
  theme: MinimalSubagentsRenderTheme,
1071
1090
  ): Component {
1072
- return renderCoordinatorMessage("Agent result", "✓", message, options, theme);
1091
+ return renderCoordinatorMessage("result", message, options, theme);
1073
1092
  }
1074
1093
 
1075
1094
  function messageSource(details: CoordinatorMessageRenderDetails | undefined): string {
@@ -1082,9 +1101,9 @@ function messageSourceTurn(
1082
1101
  return details?.source_turn_id ?? details?.turn_id;
1083
1102
  }
1084
1103
 
1104
+ /** `[subagents] result · worker → root · completed · 3s` above the Markdown, like Pi's `[skill] name`. */
1085
1105
  function renderCoordinatorMessage(
1086
- label: string,
1087
- symbol: string,
1106
+ kind: "message" | "result",
1088
1107
  message: RenderableCoordinatorMessage,
1089
1108
  options: MessageRenderOptions,
1090
1109
  theme: MinimalSubagentsRenderTheme,
@@ -1094,29 +1113,31 @@ function renderCoordinatorMessage(
1094
1113
  const source = messageSource(details);
1095
1114
  const destination = details?.destination_agent_id ?? "recipient";
1096
1115
  const sourceTurn = messageSourceTurn(details);
1097
- const route = `${theme.fg("accent", theme.bold(source))} ${theme.fg("dim", "→")} ${theme.fg("accent", theme.bold(destination))}`;
1098
1116
  const metrics = [formatSubagentDuration(details?.elapsed_ms), formatSubagentCost(details?.usage)];
1099
- const heading = [
1100
- `${theme.fg(symbol === "✓" ? "success" : "accent", symbol)} ${theme.bold(label)}`,
1101
- route,
1117
+ const heading = joinInline(theme, [
1118
+ theme.fg("customMessageText", kind),
1119
+ theme.fg("customMessageText", `${source} \u2192 ${destination}`),
1102
1120
  details?.status ? renderSubagentStatusLabel(theme, details.status) : undefined,
1103
1121
  ...metrics.map((metric) => (metric ? theme.fg("muted", metric) : undefined)),
1104
- ]
1105
- .filter((part): part is string => part !== undefined)
1106
- .join(renderSubagentSeparator(theme));
1107
- const box = new Box(options.outputPad, 1, (text) => theme.bg("customMessageBg", text));
1122
+ ]);
1123
+ const markdown = new Markdown(content, 0, 0, getMarkdownTheme(), {
1124
+ color: (text) => theme.fg("customMessageText", text),
1125
+ });
1126
+ const box = (body: Component[]) =>
1127
+ customMessageBox(theme, { outputPad: options.outputPad, source: "subagents", heading }, body);
1108
1128
  if (!options.expanded) {
1109
- box.addChild(new Text(`${heading}${collapsedExpansionHint(theme)}`, 0, 0));
1110
- box.addChild(collapsedMarkdownPreview(content, theme));
1111
- return box;
1129
+ return box([
1130
+ new CollapsedPreview(theme, markdown, { limit: COLLAPSED_LINES.fallback, expanded: false }),
1131
+ ]);
1112
1132
  }
1113
- const container = new Container();
1114
- container.addChild(new Text(heading, 0, 0));
1115
- if (sourceTurn) container.addChild(renderLabelValue(theme, "Source turn", sourceTurn));
1116
- container.addChild(new Spacer(1));
1117
- container.addChild(new Markdown(content, 0, 0, getMarkdownTheme()));
1133
+ const body: Component[] = [];
1134
+ if (sourceTurn) body.push(renderLabelValue(theme, "Source turn", sourceTurn), new Spacer(1));
1135
+ body.push(markdown);
1118
1136
  const usageText = formatSubagentUsage(details?.usage);
1119
- if (usageText) appendTextSection(container, theme, "Usage", usageText);
1120
- box.addChild(container);
1121
- return box;
1137
+ if (usageText) {
1138
+ const usage = new Container();
1139
+ appendTextSection(usage, theme, "Usage", usageText);
1140
+ body.push(usage);
1141
+ }
1142
+ return box(body);
1122
1143
  }