@ian-pascoe/pi-minimal-subagents 0.1.1 → 0.2.1

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