@ian-pascoe/pi-minimal-subagents 0.1.0 → 0.2.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.
@@ -3,6 +3,8 @@ import {
3
3
  getMarkdownTheme,
4
4
  keyHint,
5
5
  type AgentToolResult,
6
+ type MessageRenderer,
7
+ type MessageRenderOptions,
6
8
  type Theme,
7
9
  type ThemeColor,
8
10
  type ToolRenderResultOptions,
@@ -17,25 +19,55 @@ import {
17
19
  visibleWidth,
18
20
  type Component,
19
21
  } from "@earendil-works/pi-tui";
20
- export type CoordinatorToolName =
21
- | "subagent"
22
- | "agent_message"
23
- | "subagent_wait"
24
- | "subagent_status"
25
- | "subagent_cancel"
26
- | "subagent_delete";
27
-
28
- interface RenderableCoordinatorMessage {
29
- content: unknown;
30
- details?: unknown;
31
- }
32
-
33
- interface CoordinatorMessageRenderOptions {
34
- expanded: boolean;
35
- outputPad: number;
36
- }
37
-
38
- const SUBAGENT_STATUS_PRESENTATION: Record<string, { symbol: string; color: ThemeColor }> = {
22
+ import {
23
+ parseCoordinatorMessageDetails,
24
+ parseCoordinatorToolCall,
25
+ parseCoordinatorToolResult,
26
+ type CancelRenderDetails,
27
+ type CoordinatorMessageRenderDetails,
28
+ type CoordinatorToolCallInput,
29
+ type CoordinatorToolName,
30
+ type DeleteRenderDetails,
31
+ type ManagementCallArguments,
32
+ type MessageCallArguments,
33
+ type MessageRenderDetails,
34
+ type RenderStatusAgent,
35
+ type SpawnCallArguments,
36
+ type SpawnRenderDetails,
37
+ type StatusCallArguments,
38
+ type StatusRenderDetails,
39
+ type WaitCallArguments,
40
+ type WaitRenderDetails,
41
+ } from "./minimal-subagents-render-contract.js";
42
+ import { stripCoordinatorMessageEnvelope } from "./minimal-subagents-message-envelope.js";
43
+
44
+ export type { CoordinatorToolName } from "./minimal-subagents-render-contract.js";
45
+
46
+ /** Theme operations used by Minimal Subagents transcript renderers. */
47
+ export type MinimalSubagentsRenderTheme = Pick<Theme, "fg" | "bg" | "bold">;
48
+
49
+ /** Theme operation shared by transcript and widget status renderers. */
50
+ export type MinimalSubagentsStatusTheme = Pick<Theme, "fg">;
51
+
52
+ type RenderableCoordinatorMessage = Pick<Parameters<MessageRenderer>[0], "content" | "details">;
53
+
54
+ type SubagentPresentationStatus =
55
+ | "running"
56
+ | "waiting"
57
+ | "completed"
58
+ | "failed"
59
+ | "cancelled"
60
+ | "interrupted"
61
+ | "unavailable"
62
+ | "idle"
63
+ | "delivered"
64
+ | "delivered-via-wait"
65
+ | "queued"
66
+ | "message";
67
+
68
+ type SubagentStatusPresentation = { readonly symbol: string; readonly color: ThemeColor };
69
+
70
+ const SUBAGENT_STATUS_PRESENTATION = {
39
71
  running: { symbol: "◉", color: "accent" },
40
72
  waiting: { symbol: "◌", color: "accent" },
41
73
  completed: { symbol: "✓", color: "success" },
@@ -45,38 +77,19 @@ const SUBAGENT_STATUS_PRESENTATION: Record<string, { symbol: string; color: Them
45
77
  unavailable: { symbol: "!", color: "warning" },
46
78
  idle: { symbol: "○", color: "dim" },
47
79
  delivered: { symbol: "→", color: "accent" },
48
- };
49
-
50
- function asRecord(value: unknown): Record<string, unknown> | undefined {
51
- return typeof value === "object" && value !== null && !Array.isArray(value)
52
- ? (value as Record<string, unknown>)
53
- : undefined;
54
- }
55
-
56
- function asString(value: unknown): string | undefined {
57
- return typeof value === "string" ? value : undefined;
58
- }
59
-
60
- function asStringArray(value: unknown): string[] {
61
- return Array.isArray(value)
62
- ? value.filter((item): item is string => typeof item === "string")
63
- : [];
64
- }
65
-
66
- function asNumber(value: unknown): number | undefined {
67
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
68
- }
69
-
70
- function coordinatorMessageText(content: unknown): string {
71
- if (typeof content === "string") return content;
72
- if (!Array.isArray(content)) return "";
73
- return content
74
- .map((item) => {
75
- const block = asRecord(item);
76
- return block?.type === "text" ? (asString(block.text) ?? "") : "";
77
- })
78
- .filter(Boolean)
79
- .join("\n");
80
+ "delivered-via-wait": { symbol: "→", color: "accent" },
81
+ queued: { symbol: "↗", color: "accent" },
82
+ message: { symbol: "→", color: "accent" },
83
+ } satisfies { readonly [Status in SubagentPresentationStatus]: SubagentStatusPresentation };
84
+
85
+ function coordinatorMessageText(content: RenderableCoordinatorMessage["content"]): string {
86
+ if (!Array.isArray(content)) return stripCoordinatorMessageEnvelope(content);
87
+ return stripCoordinatorMessageEnvelope(
88
+ content
89
+ .map((item) => (item.type === "text" ? item.text : ""))
90
+ .filter(Boolean)
91
+ .join("\n"),
92
+ );
80
93
  }
81
94
 
82
95
  function toolResultText(result: AgentToolResult<unknown>): string {
@@ -84,24 +97,37 @@ function toolResultText(result: AgentToolResult<unknown>): string {
84
97
  return text?.type === "text" ? text.text : "";
85
98
  }
86
99
 
100
+ function subagentStatusPresentation(status: string): SubagentStatusPresentation {
101
+ for (const [knownStatus, presentation] of Object.entries(SUBAGENT_STATUS_PRESENTATION)) {
102
+ if (knownStatus === status) return presentation;
103
+ }
104
+ return SUBAGENT_STATUS_PRESENTATION.idle;
105
+ }
106
+
87
107
  /** Render the shared semantic symbol and color for one subagent status. */
88
- export function renderSubagentStatusSymbol(theme: Theme, status: string): string {
89
- const presentation = SUBAGENT_STATUS_PRESENTATION[status] ?? SUBAGENT_STATUS_PRESENTATION.idle!;
108
+ export function renderSubagentStatusSymbol(
109
+ theme: MinimalSubagentsStatusTheme,
110
+ status: string,
111
+ ): string {
112
+ const presentation = subagentStatusPresentation(status);
90
113
  return theme.fg(presentation.color, presentation.symbol);
91
114
  }
92
115
 
93
116
  /** Render a subagent status label with the same semantic color as its symbol. */
94
- export function renderSubagentStatusLabel(theme: Theme, status: string): string {
95
- const presentation = SUBAGENT_STATUS_PRESENTATION[status] ?? SUBAGENT_STATUS_PRESENTATION.idle!;
117
+ export function renderSubagentStatusLabel(
118
+ theme: MinimalSubagentsStatusTheme,
119
+ status: string,
120
+ ): string {
121
+ const presentation = subagentStatusPresentation(status);
96
122
  return theme.fg(presentation.color, status);
97
123
  }
98
124
 
99
- function renderSubagentSeparator(theme: Theme): string {
125
+ function renderSubagentSeparator(theme: MinimalSubagentsRenderTheme): string {
100
126
  return theme.fg("dim", " · ");
101
127
  }
102
128
 
103
129
  function renderSubagentSummary(
104
- theme: Theme,
130
+ theme: MinimalSubagentsRenderTheme,
105
131
  status: string,
106
132
  agentId: string,
107
133
  metrics: readonly string[] = [],
@@ -114,32 +140,49 @@ function renderSubagentSummary(
114
140
  ].join(renderSubagentSeparator(theme));
115
141
  }
116
142
 
117
- function renderLabelValue(theme: Theme, label: string, value: unknown): Text {
118
- const text = typeof value === "string" ? value : JSON.stringify(value, null, 2);
119
- return new Text(`${theme.fg("muted", `${label}:`)} ${text ?? ""}`, 0, 0);
143
+ function renderLabelValue(theme: MinimalSubagentsRenderTheme, label: string, value: string): Text {
144
+ return new Text(`${theme.fg("muted", `${label}:`)} ${value}`, 0, 0);
120
145
  }
121
146
 
122
- function appendSection(
147
+ function appendSectionHeading(
123
148
  container: Container,
124
- theme: Theme,
149
+ theme: MinimalSubagentsRenderTheme,
125
150
  label: string,
126
- content: string | Component,
127
151
  ): void {
128
152
  container.addChild(new Spacer(1));
129
153
  container.addChild(new Text(theme.fg("muted", theme.bold(label)), 0, 0));
130
- container.addChild(typeof content === "string" ? new Text(content, 0, 0) : content);
154
+ }
155
+
156
+ function appendTextSection(
157
+ container: Container,
158
+ theme: MinimalSubagentsRenderTheme,
159
+ label: string,
160
+ content: string,
161
+ ): void {
162
+ appendSectionHeading(container, theme, label);
163
+ container.addChild(new Text(content, 0, 0));
164
+ }
165
+
166
+ function appendComponentSection(
167
+ container: Container,
168
+ theme: MinimalSubagentsRenderTheme,
169
+ label: string,
170
+ content: Component,
171
+ ): void {
172
+ appendSectionHeading(container, theme, label);
173
+ container.addChild(content);
131
174
  }
132
175
 
133
176
  function renderFallbackToolResult(
134
177
  result: AgentToolResult<unknown>,
135
- theme: Theme,
178
+ theme: MinimalSubagentsRenderTheme,
136
179
  isError: boolean,
137
180
  ): Component {
138
181
  const content = toolResultText(result) || "(no output)";
139
182
  return new Text(isError ? theme.fg("error", content) : content, 0, 0);
140
183
  }
141
184
 
142
- function collapsedExpansionHint(theme: Theme): string {
185
+ function collapsedExpansionHint(theme: MinimalSubagentsRenderTheme): string {
143
186
  return theme.fg("dim", ` · ${keyHint("app.tools.expand", "to expand")}`);
144
187
  }
145
188
 
@@ -165,8 +208,8 @@ export function formatSubagentTokenCount(tokens: number | undefined): string | u
165
208
  }
166
209
 
167
210
  /** Collapse multiline task or message text into one terminal-friendly preview. */
168
- export function formatSubagentPreview(content: string, maxWidth = 72): string {
169
- const singleLine = content.replace(/\s+/g, " ").trim();
211
+ export function formatSubagentPreview(content: string | undefined, maxWidth = 72): string {
212
+ const singleLine = (content ?? "").replace(/\s+/g, " ").trim();
170
213
  const boundedWidth = Math.max(1, maxWidth);
171
214
  if (visibleWidth(singleLine) <= boundedWidth) return singleLine;
172
215
  if (boundedWidth === 1) return "…";
@@ -179,83 +222,106 @@ function currentSubagentPreviewWidth(reservedWidth: number): number {
179
222
 
180
223
  /** Format complete Pi usage metrics for expanded subagent output. */
181
224
  export function formatSubagentUsage(usage: Usage | undefined): string | undefined {
182
- const usageRecord = asRecord(usage);
183
- if (!usageRecord) return undefined;
225
+ if (usage === undefined) return undefined;
184
226
  const values = [
185
- `input ${formatSubagentTokenCount(asNumber(usageRecord.input)) ?? "0"}`,
186
- `output ${formatSubagentTokenCount(asNumber(usageRecord.output)) ?? "0"}`,
187
- `cache read ${formatSubagentTokenCount(asNumber(usageRecord.cacheRead)) ?? "0"}`,
188
- `cache write ${formatSubagentTokenCount(asNumber(usageRecord.cacheWrite)) ?? "0"}`,
189
- `total ${formatSubagentTokenCount(asNumber(usageRecord.totalTokens)) ?? "0"}`,
227
+ `input ${formatSubagentTokenCount(usage.input) ?? "0"}`,
228
+ `output ${formatSubagentTokenCount(usage.output) ?? "0"}`,
229
+ `cache read ${formatSubagentTokenCount(usage.cacheRead) ?? "0"}`,
230
+ `cache write ${formatSubagentTokenCount(usage.cacheWrite) ?? "0"}`,
231
+ `total ${formatSubagentTokenCount(usage.totalTokens) ?? "0"}`,
190
232
  ];
191
- const totalCost = asNumber(asRecord(usageRecord.cost)?.total);
192
- if (totalCost !== undefined && totalCost > 0) values.push(`cost $${totalCost.toFixed(4)}`);
233
+ if (usage.cost.total > 0) values.push(`cost $${usage.cost.total.toFixed(4)}`);
193
234
  return values.join(" · ");
194
235
  }
195
236
 
196
- type CoordinatorToolCallRenderer = (args: Record<string, unknown>, theme: Theme) => Component;
197
-
198
- function coordinatorToolCallTitle(theme: Theme, label: string): string {
237
+ function coordinatorToolCallTitle(theme: MinimalSubagentsRenderTheme, label: string): string {
199
238
  return theme.fg("toolTitle", theme.bold(label));
200
239
  }
201
240
 
202
- function coordinatorToolCallPreview(theme: Theme, value: unknown): string {
203
- return typeof value === "string" && value.length > 0
241
+ function coordinatorToolCallPreview(
242
+ theme: MinimalSubagentsRenderTheme,
243
+ value: string | undefined,
244
+ ): string {
245
+ return value && value.length > 0
204
246
  ? ` · ${theme.fg("dim", `“${formatSubagentPreview(value, currentSubagentPreviewWidth(36))}”`)}`
205
247
  : "";
206
248
  }
207
249
 
208
250
  function renderManagementToolCall(
209
251
  label: string,
210
- args: Record<string, unknown>,
211
- theme: Theme,
252
+ args: ManagementCallArguments,
253
+ theme: MinimalSubagentsRenderTheme,
212
254
  ): Component {
213
255
  return new Text(
214
- `${coordinatorToolCallTitle(theme, label)} ${theme.fg("accent", asString(args.agent_id) ?? "agent")} ${theme.fg("dim", args.recursive === false ? "· target only" : "· recursive")}`,
256
+ `${coordinatorToolCallTitle(theme, label)} ${theme.fg("accent", args.agent_id ?? "agent")} ${theme.fg("dim", args.recursive === false ? "· target only" : "· recursive")}`,
215
257
  0,
216
258
  0,
217
259
  );
218
260
  }
219
261
 
220
- const COORDINATOR_TOOL_CALL_RENDERERS: Record<CoordinatorToolName, CoordinatorToolCallRenderer> = {
262
+ type CoordinatorToolCallRenderers = {
263
+ readonly subagent: (args: SpawnCallArguments, theme: MinimalSubagentsRenderTheme) => Component;
264
+ readonly agent_message: (
265
+ args: MessageCallArguments,
266
+ theme: MinimalSubagentsRenderTheme,
267
+ ) => Component;
268
+ readonly subagent_wait: (
269
+ args: WaitCallArguments,
270
+ theme: MinimalSubagentsRenderTheme,
271
+ ) => Component;
272
+ readonly subagent_status: (
273
+ args: StatusCallArguments,
274
+ theme: MinimalSubagentsRenderTheme,
275
+ ) => Component;
276
+ readonly subagent_cancel: (
277
+ args: ManagementCallArguments,
278
+ theme: MinimalSubagentsRenderTheme,
279
+ ) => Component;
280
+ readonly subagent_delete: (
281
+ args: ManagementCallArguments,
282
+ theme: MinimalSubagentsRenderTheme,
283
+ ) => Component;
284
+ };
285
+
286
+ const COORDINATOR_TOOL_CALL_RENDERERS = {
221
287
  subagent: (args, theme) =>
222
288
  new Text(
223
- `${coordinatorToolCallTitle(theme, "Subagent")} ${theme.fg("accent", asString(args.agent_id) ?? "generated")}${coordinatorToolCallPreview(theme, args.task)}`,
289
+ `${coordinatorToolCallTitle(theme, "Subagent")} ${theme.fg("accent", args.agent_id ?? "generated")}${coordinatorToolCallPreview(theme, args.task)}`,
224
290
  0,
225
291
  0,
226
292
  ),
227
293
  agent_message: (args, theme) =>
228
294
  new Text(
229
- `${coordinatorToolCallTitle(theme, "Message")} ${theme.fg("accent", asString(args.agent_id) ?? "parent")}${coordinatorToolCallPreview(theme, args.message)}`,
295
+ `${coordinatorToolCallTitle(theme, "Message")} ${theme.fg("accent", args.agent_id ?? "parent")}${coordinatorToolCallPreview(theme, args.message)}`,
230
296
  0,
231
297
  0,
232
298
  ),
233
299
  subagent_wait: (args, theme) =>
234
300
  new Text(
235
- `${coordinatorToolCallTitle(theme, "Wait")} ${theme.fg("accent", asString(args.agent_id) ?? "agent")}`,
301
+ `${coordinatorToolCallTitle(theme, "Wait")} ${theme.fg("accent", args.agent_id ?? "agent")}`,
236
302
  0,
237
303
  0,
238
304
  ),
239
305
  subagent_status: (args, theme) =>
240
306
  new Text(
241
- `${coordinatorToolCallTitle(theme, "Status")} ${theme.fg("accent", asString(args.agent_id) ?? "children")}`,
307
+ `${coordinatorToolCallTitle(theme, "Status")} ${theme.fg("accent", args.agent_id ?? "children")}`,
242
308
  0,
243
309
  0,
244
310
  ),
245
311
  subagent_cancel: (args, theme) => renderManagementToolCall("Cancel", args, theme),
246
312
  subagent_delete: (args, theme) => renderManagementToolCall("Delete", args, theme),
247
- };
313
+ } satisfies CoordinatorToolCallRenderers;
248
314
 
249
315
  function renderSpawnResult(
250
- details: Record<string, unknown>,
316
+ details: SpawnRenderDetails,
251
317
  options: ToolRenderResultOptions,
252
- theme: Theme,
253
- args: Record<string, unknown>,
318
+ theme: MinimalSubagentsRenderTheme,
319
+ args: SpawnCallArguments,
254
320
  ): Component {
255
- const agentId = asString(details.agent_id) ?? "subagent";
256
- const status = asString(details.status) ?? "running";
257
- const agent = asRecord(details.agent);
258
- const launchContract = asRecord(agent?.launch_contract);
321
+ const agentId = details.agent_id;
322
+ const status = details.status;
323
+ const agent = details.agent;
324
+ const launchContract = agent?.launch_contract;
259
325
  if (!options.expanded) {
260
326
  return new Text(
261
327
  `${renderSubagentSummary(theme, status, agentId)}${collapsedExpansionHint(theme)}`,
@@ -265,64 +331,72 @@ function renderSpawnResult(
265
331
  }
266
332
  const container = new Container();
267
333
  container.addChild(new Text(renderSubagentSummary(theme, status, agentId), 0, 0));
268
- container.addChild(renderLabelValue(theme, "Turn", asString(details.turn_id) ?? "unknown"));
269
- appendSection(container, theme, "Task", asString(args.task) ?? "(task unavailable)");
270
- const resolvedModel = asString(launchContract?.model) ?? asString(args.model);
271
- const resolvedThinking =
272
- asString(launchContract?.thinking_level) ?? asString(args.thinking_level);
334
+ container.addChild(renderLabelValue(theme, "Turn", details.turn_id));
335
+ appendTextSection(container, theme, "Task", args.task ?? "(task unavailable)");
336
+ const resolvedModel = launchContract?.model ?? args.model;
337
+ const resolvedThinking = launchContract?.thinking_level ?? args.thinking_level;
273
338
  const launch = [
274
- `delegation ${asString(launchContract?.delegation) ?? asString(args.delegation) ?? "none"}`,
275
- `session context ${asString(launchContract?.session_context) ?? asString(args.session_context) ?? "inherit"}`,
276
- `project context ${asString(launchContract?.project_context) ?? asString(args.project_context) ?? "inherit"}`,
339
+ `delegation ${launchContract?.delegation ?? args.delegation ?? "none"}`,
340
+ `session context ${launchContract?.session_context ?? args.session_context ?? "inherit"}`,
341
+ `project context ${launchContract?.project_context ?? args.project_context ?? "inherit"}`,
277
342
  resolvedModel ? `model ${resolvedModel}` : undefined,
278
343
  resolvedThinking ? `thinking ${resolvedThinking}` : undefined,
279
- ].filter(Boolean);
280
- appendSection(container, theme, "Launch", launch.join(" · "));
281
- appendSection(
344
+ ].filter((value): value is string => value !== undefined);
345
+ appendTextSection(container, theme, "Launch", launch.join(" · "));
346
+ appendTextSection(
282
347
  container,
283
348
  theme,
284
349
  "Resolved tools",
285
- asStringArray(launchContract?.ordinary_tools ?? agent?.tools).join(", ") || "none",
350
+ (launchContract?.ordinary_tools ?? agent?.tools ?? []).join(", ") || "none",
286
351
  );
287
352
  return container;
288
353
  }
289
354
 
355
+ function messageDisposition(details: MessageRenderDetails): string {
356
+ return "disposition" in details
357
+ ? details.disposition
358
+ : details.delivered
359
+ ? "delivered"
360
+ : "failed";
361
+ }
362
+
290
363
  function renderMessageResult(
291
- details: Record<string, unknown>,
364
+ details: MessageRenderDetails,
292
365
  options: ToolRenderResultOptions,
293
- theme: Theme,
294
- args: Record<string, unknown>,
366
+ theme: MinimalSubagentsRenderTheme,
367
+ args: MessageCallArguments,
295
368
  ): Component {
296
- const agentId = asString(details.agent_id) ?? asString(args.agent_id) ?? "parent";
297
- const historicalBehavior = asString(details.behavior) ?? asString(args.behavior);
369
+ const agentId = details.agent_id ?? args.agent_id ?? "parent";
370
+ const historicalBehavior = details.behavior ?? args.behavior;
298
371
  const metrics = historicalBehavior ? [historicalBehavior] : [];
299
- const delivered = details.delivered === true;
300
- const summary = delivered
301
- ? renderSubagentSummary(theme, "delivered", agentId, metrics)
302
- : renderSubagentSummary(theme, "failed", agentId, metrics);
372
+ const disposition = messageDisposition(details);
373
+ const summary = renderSubagentSummary(theme, disposition, agentId, metrics);
303
374
  if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
304
375
  const container = new Container();
305
376
  container.addChild(new Text(summary, 0, 0));
306
- appendSection(container, theme, "Message", asString(args.message) ?? "(message unavailable)");
307
- appendSection(container, theme, "Recipient", agentId);
308
- const error = asString(details.error);
309
- if (error) appendSection(container, theme, "Error", error);
377
+ appendTextSection(container, theme, "Message", args.message ?? "(message unavailable)");
378
+ appendTextSection(container, theme, "Recipient", agentId);
379
+ appendTextSection(container, theme, "Disposition", disposition);
380
+ if (details.error) appendTextSection(container, theme, "Error", details.error);
310
381
  return container;
311
382
  }
312
383
 
313
384
  function renderWaitResult(
314
- details: Record<string, unknown>,
385
+ details: WaitRenderDetails,
315
386
  options: ToolRenderResultOptions,
316
- theme: Theme,
317
- args: Record<string, unknown>,
387
+ theme: MinimalSubagentsRenderTheme,
388
+ args: WaitCallArguments,
318
389
  ): Component {
319
- const agentId = asString(details.agent_id) ?? asString(args.agent_id) ?? "agent";
320
- const status = options.isPartial ? "waiting" : (asString(details.status) ?? "completed");
321
- const duration = formatSubagentDuration(asNumber(details.elapsed_ms));
322
- const usage = asRecord(details.usage) as Usage | undefined;
323
- const tokens = formatSubagentTokenCount(usage?.totalTokens);
390
+ const agentId = details.agent_id ?? args.agent_id ?? "agent";
391
+ const status = options.isPartial
392
+ ? "waiting"
393
+ : details.event === "message"
394
+ ? "message"
395
+ : details.status;
396
+ const duration = formatSubagentDuration(details.elapsed_ms);
397
+ const tokens = formatSubagentTokenCount(details.usage?.totalTokens);
324
398
  const metrics = [duration, tokens ? `${tokens} tokens` : undefined].filter(
325
- (metric): metric is string => Boolean(metric),
399
+ (metric): metric is string => metric !== undefined,
326
400
  );
327
401
  const summary = renderSubagentSummary(theme, status, agentId, metrics);
328
402
  if (options.isPartial || !options.expanded) {
@@ -330,310 +404,329 @@ function renderWaitResult(
330
404
  }
331
405
  const container = new Container();
332
406
  container.addChild(new Text(summary, 0, 0));
333
- container.addChild(renderLabelValue(theme, "Turn", asString(details.turn_id) ?? "unknown"));
334
- const output = asString(details.output) ?? "";
407
+ container.addChild(renderLabelValue(theme, "Turn", details.turn_id ?? "unknown"));
408
+ if (details.event === "message") {
409
+ appendTextSection(container, theme, "Message", details.message);
410
+ container.addChild(renderLabelValue(theme, "Message ID", details.message_id));
411
+ return container;
412
+ }
413
+ const output = details.output ?? "";
335
414
  if (status === "completed") {
336
- appendSection(
337
- container,
338
- theme,
339
- "Output",
340
- output.length > 0 ? new Markdown(output, 0, 0, getMarkdownTheme()) : "(no output)",
341
- );
415
+ if (output.length > 0) {
416
+ appendComponentSection(
417
+ container,
418
+ theme,
419
+ "Output",
420
+ new Markdown(output, 0, 0, getMarkdownTheme()),
421
+ );
422
+ } else {
423
+ appendTextSection(container, theme, "Output", "(no output)");
424
+ }
342
425
  } else {
343
- appendSection(
344
- container,
345
- theme,
346
- "Error",
347
- (asString(details.error) ?? output) || "(no error detail)",
348
- );
349
- appendSection(container, theme, "Diagnostics", JSON.stringify(details, null, 2));
426
+ appendTextSection(container, theme, "Error", details.error ?? (output || "(no error detail)"));
427
+ appendTextSection(container, theme, "Diagnostics", JSON.stringify(details, null, 2));
350
428
  }
351
- const usageText = formatSubagentUsage(usage);
352
- if (usageText) appendSection(container, theme, "Usage", usageText);
429
+ const usageText = formatSubagentUsage(details.usage);
430
+ if (usageText) appendTextSection(container, theme, "Usage", usageText);
353
431
  return container;
354
432
  }
355
433
 
356
- function countDirectStatusAgents(agents: unknown[]): { children: number; running: number } {
357
- let children = 0;
434
+ interface DirectStatusCounts {
435
+ children: number;
436
+ running: number;
437
+ }
438
+
439
+ function countDirectStatusAgents(agents: readonly RenderStatusAgent[]): DirectStatusCounts {
358
440
  let running = 0;
359
- for (const item of agents) {
360
- const agent = asRecord(item);
361
- if (!agent) continue;
362
- children++;
441
+ for (const agent of agents) {
363
442
  if (agent.state === "running") running++;
364
443
  }
365
- return { children, running };
366
- }
367
-
368
- function renderDirectStatusRows(agents: unknown[], theme: Theme): string[] {
369
- const rows: string[] = [];
370
- for (const item of agents) {
371
- const agent = asRecord(item);
372
- if (!agent) continue;
373
- const availability = asString(agent.availability) ?? "available";
374
- const latestTurn = asRecord(agent.latest_turn);
375
- const status =
376
- availability === "unavailable"
377
- ? "unavailable"
378
- : asString(agent.state) === "running"
379
- ? "running"
380
- : (asString(latestTurn?.status) ?? "idle");
381
- const duration = formatSubagentDuration(asNumber(agent.elapsed_ms));
382
- const childCount = asNumber(agent.child_count) ?? 0;
444
+ return { children: agents.length, running };
445
+ }
446
+
447
+ function statusAgentPresentation(agent: RenderStatusAgent): string {
448
+ if (agent.availability === "unavailable") return "unavailable";
449
+ if (agent.state === "running") return "running";
450
+ return agent.latest_turn?.status ?? "idle";
451
+ }
452
+
453
+ function renderDirectStatusRows(
454
+ agents: readonly RenderStatusAgent[],
455
+ theme: MinimalSubagentsRenderTheme,
456
+ ): string[] {
457
+ return agents.map((agent) => {
458
+ const duration = formatSubagentDuration(agent.elapsed_ms);
459
+ const childCount = agent.child_count ?? 0;
383
460
  const metrics = [duration, childCount > 0 ? `${childCount} children` : undefined].filter(
384
- (metric): metric is string => Boolean(metric),
461
+ (metric): metric is string => metric !== undefined,
385
462
  );
386
- rows.push(renderSubagentSummary(theme, status, asString(agent.agent_id) ?? "unknown", metrics));
387
- }
388
- return rows;
463
+ return renderSubagentSummary(
464
+ theme,
465
+ statusAgentPresentation(agent),
466
+ agent.agent_id ?? "unknown",
467
+ metrics,
468
+ );
469
+ });
389
470
  }
390
471
 
391
- function renderStatusResult(
392
- details: Record<string, unknown>,
472
+ type StatusLabelValue = { readonly label: string; readonly value: string | undefined };
473
+
474
+ function renderDetailedStatusAgent(
475
+ agent: RenderStatusAgent,
393
476
  options: ToolRenderResultOptions,
394
- theme: Theme,
477
+ theme: MinimalSubagentsRenderTheme,
395
478
  ): Component {
396
- const agents = Array.isArray(details.agents) ? details.agents : undefined;
397
- if (agents) {
398
- const counts = countDirectStatusAgents(agents);
399
- const summary = [
400
- theme.fg("muted", `${counts.children} children`),
401
- theme.fg(counts.running > 0 ? "accent" : "dim", `${counts.running} running`),
402
- ].join(renderSubagentSeparator(theme));
403
- if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
404
- return new Text(
405
- `${summary}\n${renderDirectStatusRows(agents, theme).join("\n") || theme.fg("dim", "(no agents)")}`,
406
- 0,
407
- 0,
408
- );
409
- }
410
- const agent = asRecord(details.agent);
411
- if (!agent) {
412
- return new Text(
413
- [theme.fg("muted", "0 children"), theme.fg("dim", "0 running")].join(
414
- renderSubagentSeparator(theme),
415
- ),
416
- 0,
417
- 0,
418
- );
419
- }
420
- const availability = asString(agent.availability) ?? "available";
421
- const latestTurn = asRecord(agent.latest_turn);
422
- const status =
423
- availability === "unavailable"
424
- ? "unavailable"
425
- : asString(agent.state) === "running"
426
- ? "running"
427
- : (asString(latestTurn?.status) ?? "idle");
428
- const id = asString(agent.agent_id) ?? "agent";
429
- const childCount = asNumber(agent.child_count) ?? 0;
430
- const duration = formatSubagentDuration(asNumber(agent.elapsed_ms));
431
- const summary = renderSubagentSummary(
432
- theme,
433
- status,
434
- id,
435
- [duration, `${childCount} children`].filter((metric): metric is string => Boolean(metric)),
479
+ const availability = agent.availability ?? "available";
480
+ const status = statusAgentPresentation(agent);
481
+ const id = agent.agent_id ?? "agent";
482
+ const childCount = agent.child_count ?? 0;
483
+ const duration = formatSubagentDuration(agent.elapsed_ms);
484
+ const metrics = [duration, `${childCount} children`].filter(
485
+ (metric): metric is string => metric !== undefined,
436
486
  );
487
+ const summary = renderSubagentSummary(theme, status, id, metrics);
437
488
  if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
438
489
  const container = new Container();
439
490
  container.addChild(new Text(summary, 0, 0));
440
- for (const [label, value] of [
441
- ["Parent", agent.parent_id],
442
- ["Availability", availability],
443
- ["Turn", agent.active_turn_id ?? asRecord(agent.latest_turn)?.turn_id],
444
- ["Duration", duration],
445
- ["Model", agent.model],
446
- ["Thinking", agent.thinking_level],
447
- ["Session", agent.session_file],
448
- ["Spawn entry", agent.spawn_entry_id],
449
- ] as const) {
491
+ const labels = [
492
+ { label: "Parent", value: agent.parent_id },
493
+ { label: "Availability", value: availability },
494
+ { label: "Turn", value: agent.active_turn_id ?? agent.latest_turn?.turn_id },
495
+ { label: "Duration", value: duration },
496
+ { label: "Model", value: agent.model },
497
+ { label: "Thinking", value: agent.thinking_level },
498
+ { label: "Session", value: agent.session_file },
499
+ { label: "Spawn entry", value: agent.spawn_entry_id },
500
+ ] satisfies readonly StatusLabelValue[];
501
+ for (const { label, value } of labels) {
450
502
  if (value !== undefined) container.addChild(renderLabelValue(theme, label, value));
451
503
  }
452
- if (asString(agent.task)) appendSection(container, theme, "Task", String(agent.task));
453
- const launchContract = asRecord(agent.launch_contract);
504
+ if (agent.task) appendTextSection(container, theme, "Task", agent.task);
505
+ const launchContract = agent.launch_contract;
454
506
  if (launchContract) {
455
507
  const launchValues = [
456
- `session context ${asString(launchContract.session_context) ?? "inherit"}`,
457
- `project context ${asString(launchContract.project_context) ?? "inherit"}`,
458
- `model ${asString(launchContract.model) ?? asString(agent.model) ?? "unknown"}`,
459
- `thinking ${asString(launchContract.thinking_level) ?? asString(agent.thinking_level) ?? "unknown"}`,
460
- `delegation ${asString(launchContract.delegation) ?? "none"}`,
508
+ `session context ${launchContract.session_context ?? "inherit"}`,
509
+ `project context ${launchContract.project_context ?? "inherit"}`,
510
+ `model ${launchContract.model ?? agent.model ?? "unknown"}`,
511
+ `thinking ${launchContract.thinking_level ?? agent.thinking_level ?? "unknown"}`,
512
+ `delegation ${launchContract.delegation ?? "none"}`,
461
513
  ];
462
- appendSection(container, theme, "Launch contract", launchValues.join(" · "));
514
+ appendTextSection(container, theme, "Launch contract", launchValues.join(" · "));
463
515
  }
464
- appendSection(container, theme, "Tools", asStringArray(agent.tools).join(", ") || "none");
465
- appendSection(
516
+ appendTextSection(container, theme, "Tools", (agent.tools ?? []).join(", ") || "none");
517
+ appendTextSection(
466
518
  container,
467
519
  theme,
468
520
  "Capability ceiling",
469
- asStringArray(agent.capability_ceiling).join(", ") || "none",
521
+ (agent.capability_ceiling ?? []).join(", ") || "none",
470
522
  );
471
- const missing = asStringArray(agent.missing_dependencies);
472
- if (missing.length > 0)
473
- appendSection(container, theme, "Missing dependencies", missing.join("\n"));
474
- if (asString(agent.unavailable_reason)) {
475
- appendSection(container, theme, "Unavailable reason", String(agent.unavailable_reason));
523
+ const missing = agent.missing_dependencies ?? [];
524
+ if (missing.length > 0) {
525
+ appendTextSection(container, theme, "Missing dependencies", missing.join("\n"));
526
+ }
527
+ if (agent.unavailable_reason) {
528
+ appendTextSection(container, theme, "Unavailable reason", agent.unavailable_reason);
476
529
  }
477
- const recentMessages = Array.isArray(agent.recent_messages)
478
- ? agent.recent_messages
479
- .map(asRecord)
480
- .filter((item): item is Record<string, unknown> => Boolean(item))
481
- : [];
530
+ const recentMessages = agent.recent_messages ?? [];
482
531
  if (recentMessages.length > 0) {
483
- appendSection(
532
+ appendTextSection(
484
533
  container,
485
534
  theme,
486
535
  "Recent messages",
487
536
  recentMessages
488
- .map(
489
- (message) =>
490
- `${asString(message.source_agent_id) ?? "unknown"}: ${asString(message.content) ?? ""}`,
491
- )
537
+ .map((message) => `${message.source_agent_id ?? "unknown"}: ${message.content ?? ""}`)
492
538
  .join("\n"),
493
539
  );
494
540
  }
495
- const latestResult = asRecord(agent.latest_result);
541
+ const latestResult = agent.latest_result;
496
542
  if (latestResult) {
497
- const output = asString(latestResult.output) ?? "";
498
- appendSection(
499
- container,
500
- theme,
501
- "Latest result",
502
- asString(latestResult.status) === "completed" && output
503
- ? new Markdown(output, 0, 0, getMarkdownTheme())
504
- : output || JSON.stringify(latestResult, null, 2),
505
- );
543
+ const output = latestResult.output ?? "";
544
+ if (latestResult.status === "completed" && output) {
545
+ appendComponentSection(
546
+ container,
547
+ theme,
548
+ "Latest result",
549
+ new Markdown(output, 0, 0, getMarkdownTheme()),
550
+ );
551
+ } else {
552
+ appendTextSection(
553
+ container,
554
+ theme,
555
+ "Latest result",
556
+ output || JSON.stringify(latestResult, null, 2),
557
+ );
558
+ }
506
559
  }
507
- const usageText = formatSubagentUsage(asRecord(agent.usage) as Usage | undefined);
508
- if (usageText) appendSection(container, theme, "Usage", usageText);
560
+ const usageText = formatSubagentUsage(agent.usage);
561
+ if (usageText) appendTextSection(container, theme, "Usage", usageText);
509
562
  return container;
510
563
  }
511
564
 
565
+ function renderStatusResult(
566
+ details: StatusRenderDetails,
567
+ options: ToolRenderResultOptions,
568
+ theme: MinimalSubagentsRenderTheme,
569
+ ): Component {
570
+ if ("agents" in details) {
571
+ const counts = countDirectStatusAgents(details.agents);
572
+ const summary = [
573
+ theme.fg("muted", `${counts.children} children`),
574
+ theme.fg(counts.running > 0 ? "accent" : "dim", `${counts.running} running`),
575
+ ].join(renderSubagentSeparator(theme));
576
+ if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
577
+ return new Text(
578
+ `${summary}\n${renderDirectStatusRows(details.agents, theme).join("\n") || theme.fg("dim", "(no agents)")}`,
579
+ 0,
580
+ 0,
581
+ );
582
+ }
583
+ return renderDetailedStatusAgent(details.agent, options, theme);
584
+ }
585
+
512
586
  function renderCancelResult(
513
- details: Record<string, unknown>,
587
+ details: CancelRenderDetails,
514
588
  options: ToolRenderResultOptions,
515
- theme: Theme,
589
+ theme: MinimalSubagentsRenderTheme,
516
590
  ): Component {
517
- const id = asString(details.agent_id) ?? "agent";
518
- const turns = asStringArray(details.cancelled_turn_ids);
591
+ const turns = details.cancelled_turn_ids;
519
592
  const summary =
520
593
  turns.length > 0
521
- ? renderSubagentSummary(theme, "cancelled", id, [`${turns.length} turns cancelled`])
522
- : renderSubagentSummary(theme, "completed", id, ["no active turns"]);
594
+ ? renderSubagentSummary(theme, "cancelled", details.agent_id, [
595
+ `${turns.length} turns cancelled`,
596
+ ])
597
+ : renderSubagentSummary(theme, "completed", details.agent_id, ["no active turns"]);
523
598
  if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
524
599
  const container = new Container();
525
600
  container.addChild(new Text(summary, 0, 0));
526
- container.addChild(renderLabelValue(theme, "Requested target", id));
601
+ container.addChild(renderLabelValue(theme, "Requested target", details.agent_id));
527
602
  container.addChild(
528
- renderLabelValue(theme, "Mode", details.recursive === false ? "target only" : "recursive"),
603
+ renderLabelValue(theme, "Mode", details.recursive ? "recursive" : "target only"),
529
604
  );
530
- appendSection(
605
+ appendTextSection(
531
606
  container,
532
607
  theme,
533
608
  "Affected agents",
534
- asStringArray(details.affected_agent_ids).join("\n") || "(none)",
609
+ details.affected_agent_ids.join("\n") || "(none)",
535
610
  );
536
- appendSection(container, theme, "Cancelled turns", turns.join("\n") || "(none)");
611
+ appendTextSection(container, theme, "Cancelled turns", turns.join("\n") || "(none)");
537
612
  return container;
538
613
  }
539
614
 
540
615
  function renderDeleteResult(
541
- details: Record<string, unknown>,
616
+ details: DeleteRenderDetails,
542
617
  options: ToolRenderResultOptions,
543
- theme: Theme,
618
+ theme: MinimalSubagentsRenderTheme,
544
619
  ): Component {
545
- const id = asString(details.agent_id) ?? "agent";
546
- const deleted = asStringArray(details.deleted_agent_ids);
547
- const tombstoned = asStringArray(details.tombstoned_agent_ids);
548
- const failures = Array.isArray(details.failures) ? details.failures : [];
549
- const status = failures.length > 0 ? "failed" : "completed";
550
- const summary = renderSubagentSummary(
551
- theme,
552
- status,
553
- id,
554
- [
555
- `${deleted.length} agents deleted`,
556
- `${tombstoned.length} tombstoned`,
557
- failures.length > 0 ? `${failures.length} failed` : undefined,
558
- ].filter((metric): metric is string => Boolean(metric)),
559
- );
620
+ const status = details.failures.length > 0 ? "failed" : "completed";
621
+ const metrics = [
622
+ `${details.deleted_agent_ids.length} agents deleted`,
623
+ `${details.tombstoned_agent_ids.length} tombstoned`,
624
+ details.failures.length > 0 ? `${details.failures.length} failed` : undefined,
625
+ ].filter((metric): metric is string => metric !== undefined);
626
+ const summary = renderSubagentSummary(theme, status, details.agent_id, metrics);
560
627
  if (!options.expanded) return new Text(`${summary}${collapsedExpansionHint(theme)}`, 0, 0);
561
628
  const container = new Container();
562
629
  container.addChild(new Text(summary, 0, 0));
563
- container.addChild(renderLabelValue(theme, "Requested target", id));
630
+ container.addChild(renderLabelValue(theme, "Requested target", details.agent_id));
564
631
  container.addChild(
565
- renderLabelValue(theme, "Mode", details.recursive === false ? "target only" : "recursive"),
632
+ renderLabelValue(theme, "Mode", details.recursive ? "recursive" : "target only"),
633
+ );
634
+ appendTextSection(
635
+ container,
636
+ theme,
637
+ "Deleted agents",
638
+ details.deleted_agent_ids.join("\n") || "(none)",
639
+ );
640
+ appendTextSection(
641
+ container,
642
+ theme,
643
+ "Tombstones",
644
+ details.tombstoned_agent_ids.join("\n") || "(none)",
566
645
  );
567
- appendSection(container, theme, "Deleted agents", deleted.join("\n") || "(none)");
568
- appendSection(container, theme, "Tombstones", tombstoned.join("\n") || "(none)");
569
- appendSection(
646
+ appendTextSection(
570
647
  container,
571
648
  theme,
572
649
  "Trashed sessions",
573
- asStringArray(details.trashed_session_files).join("\n") || "(none)",
650
+ details.trashed_session_files.join("\n") || "(none)",
574
651
  );
575
- if (failures.length > 0) {
576
- appendSection(
652
+ if (details.failures.length > 0) {
653
+ appendTextSection(
577
654
  container,
578
655
  theme,
579
656
  "Failures",
580
- theme.fg("error", JSON.stringify(failures, null, 2)),
657
+ theme.fg("error", JSON.stringify(details.failures, null, 2)),
581
658
  );
582
659
  }
583
660
  return container;
584
661
  }
585
662
 
586
- type CoordinatorToolResultRenderer = (
587
- details: Record<string, unknown>,
588
- options: ToolRenderResultOptions,
589
- theme: Theme,
590
- args: Record<string, unknown>,
591
- ) => Component;
592
-
593
- const COORDINATOR_TOOL_RESULT_RENDERERS: Record<
594
- CoordinatorToolName,
595
- CoordinatorToolResultRenderer
596
- > = {
663
+ type CoordinatorToolResultRenderers = {
664
+ readonly subagent: (
665
+ details: SpawnRenderDetails,
666
+ options: ToolRenderResultOptions,
667
+ theme: MinimalSubagentsRenderTheme,
668
+ args: SpawnCallArguments,
669
+ ) => Component;
670
+ readonly agent_message: (
671
+ details: MessageRenderDetails,
672
+ options: ToolRenderResultOptions,
673
+ theme: MinimalSubagentsRenderTheme,
674
+ args: MessageCallArguments,
675
+ ) => Component;
676
+ readonly subagent_wait: (
677
+ details: WaitRenderDetails,
678
+ options: ToolRenderResultOptions,
679
+ theme: MinimalSubagentsRenderTheme,
680
+ args: WaitCallArguments,
681
+ ) => Component;
682
+ readonly subagent_status: (
683
+ details: StatusRenderDetails,
684
+ options: ToolRenderResultOptions,
685
+ theme: MinimalSubagentsRenderTheme,
686
+ ) => Component;
687
+ readonly subagent_cancel: (
688
+ details: CancelRenderDetails,
689
+ options: ToolRenderResultOptions,
690
+ theme: MinimalSubagentsRenderTheme,
691
+ ) => Component;
692
+ readonly subagent_delete: (
693
+ details: DeleteRenderDetails,
694
+ options: ToolRenderResultOptions,
695
+ theme: MinimalSubagentsRenderTheme,
696
+ ) => Component;
697
+ };
698
+
699
+ const COORDINATOR_TOOL_RESULT_RENDERERS = {
597
700
  subagent: renderSpawnResult,
598
701
  agent_message: renderMessageResult,
599
702
  subagent_wait: renderWaitResult,
600
- subagent_status: (details, options, theme) => renderStatusResult(details, options, theme),
601
- subagent_cancel: (details, options, theme) => renderCancelResult(details, options, theme),
602
- subagent_delete: (details, options, theme) => renderDeleteResult(details, options, theme),
603
- };
604
-
605
- const COORDINATOR_DETAIL_VALIDATORS: Record<
606
- CoordinatorToolName,
607
- (details: Record<string, unknown>) => boolean
608
- > = {
609
- subagent: (details) =>
610
- asString(details.agent_id) !== undefined &&
611
- asString(details.turn_id) !== undefined &&
612
- asString(details.status) !== undefined,
613
- agent_message: (details) =>
614
- asString(details.agent_id) !== undefined && typeof details.delivered === "boolean",
615
- subagent_wait: (details) =>
616
- asString(details.agent_id) !== undefined && asString(details.status) !== undefined,
617
- subagent_status: (details) =>
618
- Array.isArray(details.agents) || asRecord(details.agent) !== undefined,
619
- subagent_cancel: (details) =>
620
- asString(details.agent_id) !== undefined &&
621
- Array.isArray(details.affected_agent_ids) &&
622
- Array.isArray(details.cancelled_turn_ids),
623
- subagent_delete: (details) =>
624
- asString(details.agent_id) !== undefined &&
625
- Array.isArray(details.deleted_agent_ids) &&
626
- Array.isArray(details.tombstoned_agent_ids) &&
627
- Array.isArray(details.failures),
628
- };
703
+ subagent_status: renderStatusResult,
704
+ subagent_cancel: renderCancelResult,
705
+ subagent_delete: renderDeleteResult,
706
+ } satisfies CoordinatorToolResultRenderers;
629
707
 
630
708
  /** Render one of the six coordinator tool calls with a shared native Pi grammar. */
631
709
  export function renderCoordinatorToolCall(
632
710
  toolName: CoordinatorToolName,
633
- args: Record<string, unknown>,
634
- theme: Theme,
711
+ args: CoordinatorToolCallInput,
712
+ theme: MinimalSubagentsRenderTheme,
635
713
  ): Component {
636
- return COORDINATOR_TOOL_CALL_RENDERERS[toolName](args, theme);
714
+ const parsed = parseCoordinatorToolCall(toolName, args);
715
+ if (parsed === undefined) return new Text(coordinatorToolCallTitle(theme, toolName), 0, 0);
716
+ switch (parsed.toolName) {
717
+ case "subagent":
718
+ return COORDINATOR_TOOL_CALL_RENDERERS.subagent(parsed.args, theme);
719
+ case "agent_message":
720
+ return COORDINATOR_TOOL_CALL_RENDERERS.agent_message(parsed.args, theme);
721
+ case "subagent_wait":
722
+ return COORDINATOR_TOOL_CALL_RENDERERS.subagent_wait(parsed.args, theme);
723
+ case "subagent_status":
724
+ return COORDINATOR_TOOL_CALL_RENDERERS.subagent_status(parsed.args, theme);
725
+ case "subagent_cancel":
726
+ return COORDINATOR_TOOL_CALL_RENDERERS.subagent_cancel(parsed.args, theme);
727
+ case "subagent_delete":
728
+ return COORDINATOR_TOOL_CALL_RENDERERS.subagent_delete(parsed.args, theme);
729
+ }
637
730
  }
638
731
 
639
732
  /** Render one coordinator tool result in native collapsed, expanded, or partial mode. */
@@ -641,22 +734,61 @@ export function renderCoordinatorToolResult(
641
734
  toolName: CoordinatorToolName,
642
735
  result: AgentToolResult<unknown>,
643
736
  options: ToolRenderResultOptions,
644
- theme: Theme,
645
- args: Record<string, unknown>,
737
+ theme: MinimalSubagentsRenderTheme,
738
+ args: CoordinatorToolCallInput,
646
739
  isError = false,
647
740
  ): Component {
648
- const details = asRecord(result.details);
649
- if (!details || !COORDINATOR_DETAIL_VALIDATORS[toolName](details)) {
650
- return renderFallbackToolResult(result, theme, isError);
741
+ const parsedResult = parseCoordinatorToolResult(toolName, result.details);
742
+ if (parsedResult === undefined) return renderFallbackToolResult(result, theme, isError);
743
+ const parsedCall = parseCoordinatorToolCall(toolName, args);
744
+ switch (parsedResult.toolName) {
745
+ case "subagent":
746
+ return COORDINATOR_TOOL_RESULT_RENDERERS.subagent(
747
+ parsedResult.details,
748
+ options,
749
+ theme,
750
+ parsedCall?.toolName === "subagent" ? parsedCall.args : {},
751
+ );
752
+ case "agent_message":
753
+ return COORDINATOR_TOOL_RESULT_RENDERERS.agent_message(
754
+ parsedResult.details,
755
+ options,
756
+ theme,
757
+ parsedCall?.toolName === "agent_message" ? parsedCall.args : {},
758
+ );
759
+ case "subagent_wait":
760
+ return COORDINATOR_TOOL_RESULT_RENDERERS.subagent_wait(
761
+ parsedResult.details,
762
+ options,
763
+ theme,
764
+ parsedCall?.toolName === "subagent_wait" ? parsedCall.args : {},
765
+ );
766
+ case "subagent_status":
767
+ return COORDINATOR_TOOL_RESULT_RENDERERS.subagent_status(
768
+ parsedResult.details,
769
+ options,
770
+ theme,
771
+ );
772
+ case "subagent_cancel":
773
+ return COORDINATOR_TOOL_RESULT_RENDERERS.subagent_cancel(
774
+ parsedResult.details,
775
+ options,
776
+ theme,
777
+ );
778
+ case "subagent_delete":
779
+ return COORDINATOR_TOOL_RESULT_RENDERERS.subagent_delete(
780
+ parsedResult.details,
781
+ options,
782
+ theme,
783
+ );
651
784
  }
652
- return COORDINATOR_TOOL_RESULT_RENDERERS[toolName](details, options, theme, args);
653
785
  }
654
786
 
655
787
  /** Render explicit agent messages with compact source/destination metadata. */
656
788
  export function renderMinimalSubagentsMessage(
657
789
  message: RenderableCoordinatorMessage,
658
- options: CoordinatorMessageRenderOptions,
659
- theme: Theme,
790
+ options: MessageRenderOptions,
791
+ theme: MinimalSubagentsRenderTheme,
660
792
  ): Component {
661
793
  return renderCoordinatorMessage("Agent message", "→", message, options, theme);
662
794
  }
@@ -664,31 +796,42 @@ export function renderMinimalSubagentsMessage(
664
796
  /** Render automatic successful agent results with expandable Markdown output. */
665
797
  export function renderMinimalSubagentsResult(
666
798
  message: RenderableCoordinatorMessage,
667
- options: CoordinatorMessageRenderOptions,
668
- theme: Theme,
799
+ options: MessageRenderOptions,
800
+ theme: MinimalSubagentsRenderTheme,
669
801
  ): Component {
670
802
  return renderCoordinatorMessage("Agent result", "✓", message, options, theme);
671
803
  }
672
804
 
805
+ function messageSource(details: CoordinatorMessageRenderDetails | undefined): string {
806
+ return details?.source_agent_id ?? details?.agent_id ?? "unknown";
807
+ }
808
+
809
+ function messageSourceTurn(
810
+ details: CoordinatorMessageRenderDetails | undefined,
811
+ ): string | undefined {
812
+ return details?.source_turn_id ?? details?.turn_id;
813
+ }
814
+
673
815
  function renderCoordinatorMessage(
674
816
  label: string,
675
817
  symbol: string,
676
818
  message: RenderableCoordinatorMessage,
677
- options: CoordinatorMessageRenderOptions,
678
- theme: Theme,
819
+ options: MessageRenderOptions,
820
+ theme: MinimalSubagentsRenderTheme,
679
821
  ): Component {
680
- const details = asRecord(message.details);
822
+ const details = parseCoordinatorMessageDetails(message.details);
681
823
  const content = coordinatorMessageText(message.content);
682
- const source = asString(details?.source_agent_id) ?? "unknown";
683
- const destination = asString(details?.destination_agent_id) ?? "recipient";
684
- const status = asString(details?.status);
824
+ const source = messageSource(details);
825
+ const destination = details?.destination_agent_id ?? "recipient";
826
+ const sourceTurn = messageSourceTurn(details);
685
827
  const route = `${theme.fg("accent", theme.bold(source))} ${theme.fg("dim", "→")} ${theme.fg("accent", theme.bold(destination))}`;
686
828
  const heading = [
687
829
  `${theme.fg(symbol === "✓" ? "success" : "accent", symbol)} ${theme.bold(label)}`,
688
830
  route,
689
- status ? renderSubagentStatusLabel(theme, status) : undefined,
831
+ sourceTurn ? theme.fg("dim", `turn ${sourceTurn}`) : undefined,
832
+ details?.status ? renderSubagentStatusLabel(theme, details.status) : undefined,
690
833
  ]
691
- .filter((part): part is string => Boolean(part))
834
+ .filter((part): part is string => part !== undefined)
692
835
  .join(renderSubagentSeparator(theme));
693
836
  const box = new Box(options.outputPad, 1, (text) => theme.bg("customMessageBg", text));
694
837
  if (!options.expanded) {
@@ -703,15 +846,13 @@ function renderCoordinatorMessage(
703
846
  }
704
847
  const container = new Container();
705
848
  container.addChild(new Text(heading, 0, 0));
706
- if (asString(details?.source_turn_id)) {
707
- container.addChild(renderLabelValue(theme, "Source turn", details?.source_turn_id));
708
- }
709
- const duration = formatSubagentDuration(asNumber(details?.elapsed_ms));
849
+ if (sourceTurn) container.addChild(renderLabelValue(theme, "Source turn", sourceTurn));
850
+ const duration = formatSubagentDuration(details?.elapsed_ms);
710
851
  if (duration) container.addChild(renderLabelValue(theme, "Duration", duration));
711
852
  container.addChild(new Spacer(1));
712
853
  container.addChild(new Markdown(content, 0, 0, getMarkdownTheme()));
713
- const usageText = formatSubagentUsage(asRecord(details?.usage) as Usage | undefined);
714
- if (usageText) appendSection(container, theme, "Usage", usageText);
854
+ const usageText = formatSubagentUsage(details?.usage);
855
+ if (usageText) appendTextSection(container, theme, "Usage", usageText);
715
856
  box.addChild(container);
716
857
  return box;
717
858
  }