@narumitw/pi-subagents 0.15.1 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/render.ts CHANGED
@@ -12,6 +12,7 @@ import type { AgentScope, SubagentThinkingLevel } from "./agents.js";
12
12
  import type { SubagentParams } from "./params.js";
13
13
  import {
14
14
  getResultFinalOutput,
15
+ isResultError,
15
16
  type SingleResult,
16
17
  type SubagentDetails,
17
18
  } from "./runner.js";
@@ -37,6 +38,8 @@ export function formatUsageStats(
37
38
  },
38
39
  model?: string,
39
40
  thinkingLevel?: SubagentThinkingLevel,
41
+ actualProvider?: string,
42
+ actualModel?: string,
40
43
  ): string {
41
44
  const parts: string[] = [];
42
45
  if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
@@ -45,14 +48,27 @@ export function formatUsageStats(
45
48
  if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
46
49
  if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
47
50
  if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
48
- if (usage.contextTokens && usage.contextTokens > 0) {
51
+ if (usage.contextTokens && usage.contextTokens > 0)
49
52
  parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
50
- }
51
- if (model) parts.push(model);
52
- if (thinkingLevel) parts.push(`thinking:${thinkingLevel}`);
53
+ const actual =
54
+ actualProvider && actualModel
55
+ ? `${actualProvider}/${actualModel}`
56
+ : (actualModel ?? actualProvider);
57
+ if (actual ?? model) parts.push(actual ?? model ?? "");
58
+ if (thinkingLevel) parts.push(`requested-thinking:${thinkingLevel}`);
53
59
  return parts.join(" ");
54
60
  }
55
61
 
62
+ function formatResultUsageStats(result: SingleResult): string {
63
+ return formatUsageStats(
64
+ result.usage,
65
+ result.model,
66
+ result.thinkingLevel,
67
+ result.actualProvider,
68
+ result.actualModel,
69
+ );
70
+ }
71
+
56
72
  function formatToolCall(
57
73
  toolName: string,
58
74
  args: Record<string, unknown>,
@@ -102,7 +118,11 @@ function formatToolCall(
102
118
  case "find": {
103
119
  const pattern = (args.pattern || "*") as string;
104
120
  const rawPath = (args.path || ".") as string;
105
- return themeFg("muted", "find ") + themeFg("accent", pattern) + themeFg("dim", ` in ${shortenPath(rawPath)}`);
121
+ return (
122
+ themeFg("muted", "find ") +
123
+ themeFg("accent", pattern) +
124
+ themeFg("dim", ` in ${shortenPath(rawPath)}`)
125
+ );
106
126
  }
107
127
  case "grep": {
108
128
  const pattern = (args.pattern || "") as string;
@@ -130,392 +150,496 @@ function getDisplayItems(messages: Message[]): DisplayItem[] {
130
150
  for (const msg of messages) {
131
151
  if (msg.role === "assistant") {
132
152
  for (const part of msg.content) {
133
- if (part.type === "text") items.push({ type: "text", text: part.text });
134
- else if (part.type === "toolCall") items.push({ type: "toolCall", name: part.name, args: part.arguments });
153
+ if (part.type === "text") {
154
+ const text = part.text.trim();
155
+ if (text) items.push({ type: "text", text });
156
+ } else if (part.type === "toolCall") {
157
+ items.push({ type: "toolCall", name: part.name, args: part.arguments });
158
+ }
135
159
  }
136
160
  }
137
161
  }
138
162
  return items;
139
163
  }
164
+ function getCollapsedDisplayItems(result: SingleResult): { items: DisplayItem[]; total: number } {
165
+ if (result.recentActivity && result.recentActivity.length > 0) {
166
+ return {
167
+ items: result.recentActivity,
168
+ total: Math.max(result.recentActivity.length, result.recentActivityTotal ?? 0),
169
+ };
170
+ }
171
+ const items = getDisplayItems(result.messages);
172
+ return { items, total: items.length };
173
+ }
140
174
 
141
175
  export function renderSubagentCall(args: SubagentParams, theme: Theme) {
142
- const scope: AgentScope = args.agentScope ?? "user";
143
- if (args.chain && args.chain.length > 0) {
144
- let text =
145
- theme.fg("toolTitle", theme.bold("subagent ")) +
146
- theme.fg("accent", `chain (${args.chain.length} steps)`) +
147
- theme.fg("muted", ` [${scope}]`);
148
- for (let i = 0; i < Math.min(args.chain.length, 3); i++) {
149
- const step = args.chain[i];
150
- // Clean up {previous} placeholder for display
151
- const cleanTask = step.task.replace(/\{previous\}/g, "").trim();
152
- const preview = cleanTask.length > 40 ? `${cleanTask.slice(0, 40)}...` : cleanTask;
153
- text +=
154
- "\n " +
155
- theme.fg("muted", `${i + 1}.`) +
156
- " " +
157
- theme.fg("accent", step.agent) +
158
- theme.fg("dim", ` ${preview}`);
159
- }
160
- if (args.chain.length > 3) text += `\n ${theme.fg("muted", `... +${args.chain.length - 3} more`)}`;
161
- return new Text(text, 0, 0);
162
- }
163
- if (args.tasks && args.tasks.length > 0) {
164
- let text =
165
- theme.fg("toolTitle", theme.bold("subagent ")) +
166
- theme.fg("accent", `parallel (${args.tasks.length} tasks)`) +
167
- theme.fg("muted", ` [${scope}]`);
168
- for (const t of args.tasks.slice(0, 3)) {
169
- const preview = t.task.length > 40 ? `${t.task.slice(0, 40)}...` : t.task;
170
- text += `\n ${theme.fg("accent", t.agent)}${theme.fg("dim", ` ${preview}`)}`;
171
- }
172
- if (args.tasks.length > 3) text += `\n ${theme.fg("muted", `... +${args.tasks.length - 3} more`)}`;
173
- if (args.aggregator) {
174
- const preview =
175
- args.aggregator.task.length > 40 ? `${args.aggregator.task.slice(0, 40)}...` : args.aggregator.task;
176
- text += `\n ${theme.fg("muted", "fan-in → ")}${theme.fg("accent", args.aggregator.agent)}${theme.fg(
177
- "dim",
178
- ` ${preview}`,
179
- )}`;
180
- }
181
- return new Text(text, 0, 0);
182
- }
183
- const agentName = args.agent || "...";
184
- const preview = args.task ? (args.task.length > 60 ? `${args.task.slice(0, 60)}...` : args.task) : "...";
185
- let text =
186
- theme.fg("toolTitle", theme.bold("subagent ")) +
187
- theme.fg("accent", agentName) +
188
- theme.fg("muted", ` [${scope}]`);
189
- text += `\n ${theme.fg("dim", preview)}`;
190
- return new Text(text, 0, 0);
176
+ const scope: AgentScope = args.agentScope ?? "user";
177
+ if (args.chain && args.chain.length > 0) {
178
+ let text =
179
+ theme.fg("toolTitle", theme.bold("subagent ")) +
180
+ theme.fg("accent", `chain (${args.chain.length} steps)`) +
181
+ theme.fg("muted", ` [${scope}]`);
182
+ for (let i = 0; i < Math.min(args.chain.length, 3); i++) {
183
+ const step = args.chain[i];
184
+ // Clean up {previous} placeholder for display
185
+ const cleanTask = step.task.replace(/\{previous\}/g, "").trim();
186
+ const preview = cleanTask.length > 40 ? `${cleanTask.slice(0, 40)}...` : cleanTask;
187
+ text +=
188
+ "\n " +
189
+ theme.fg("muted", `${i + 1}.`) +
190
+ " " +
191
+ theme.fg("accent", step.agent) +
192
+ theme.fg("dim", ` ${preview}`);
193
+ }
194
+ if (args.chain.length > 3)
195
+ text += `\n ${theme.fg("muted", `... +${args.chain.length - 3} more`)}`;
196
+ return new Text(text, 0, 0);
197
+ }
198
+ if (args.tasks && args.tasks.length > 0) {
199
+ let text =
200
+ theme.fg("toolTitle", theme.bold("subagent ")) +
201
+ theme.fg("accent", `parallel (${args.tasks.length} tasks)`) +
202
+ theme.fg("muted", ` [${scope}]`);
203
+ for (const t of args.tasks.slice(0, 3)) {
204
+ const preview = t.task.length > 40 ? `${t.task.slice(0, 40)}...` : t.task;
205
+ text += `\n ${theme.fg("accent", t.agent)}${theme.fg("dim", ` ${preview}`)}`;
206
+ }
207
+ if (args.tasks.length > 3)
208
+ text += `\n ${theme.fg("muted", `... +${args.tasks.length - 3} more`)}`;
209
+ if (args.aggregator) {
210
+ const preview =
211
+ args.aggregator.task.length > 40
212
+ ? `${args.aggregator.task.slice(0, 40)}...`
213
+ : args.aggregator.task;
214
+ text += `\n ${theme.fg("muted", "fan-in → ")}${theme.fg("accent", args.aggregator.agent)}${theme.fg(
215
+ "dim",
216
+ ` ${preview}`,
217
+ )}`;
218
+ }
219
+ return new Text(text, 0, 0);
220
+ }
221
+ const agentName = args.agent || "...";
222
+ const preview = args.task
223
+ ? args.task.length > 60
224
+ ? `${args.task.slice(0, 60)}...`
225
+ : args.task
226
+ : "...";
227
+ let text =
228
+ theme.fg("toolTitle", theme.bold("subagent ")) +
229
+ theme.fg("accent", agentName) +
230
+ theme.fg("muted", ` [${scope}]`);
231
+ text += `\n ${theme.fg("dim", preview)}`;
232
+ return new Text(text, 0, 0);
191
233
  }
192
234
 
193
235
  export function renderSubagentResult(
194
236
  result: AgentToolResult<SubagentDetails>,
195
- { expanded }: ToolRenderResultOptions,
237
+ { expanded, isPartial }: ToolRenderResultOptions,
196
238
  theme: Theme,
197
239
  ) {
198
- const details = result.details as SubagentDetails | undefined;
199
- if (!details || details.results.length === 0) {
200
- const text = result.content[0];
201
- return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
240
+ const details = result.details as SubagentDetails | undefined;
241
+ if (!details || details.results.length === 0) {
242
+ const text = result.content[0];
243
+ return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
244
+ }
245
+
246
+ const mdTheme = getMarkdownTheme();
247
+
248
+ const renderDisplayItems = (items: DisplayItem[], limit?: number, total = items.length) => {
249
+ const toShow = limit ? items.slice(-limit) : items;
250
+ const skipped = Math.max(0, total - toShow.length);
251
+ let text = "";
252
+ if (skipped > 0) text += theme.fg("muted", `... ${skipped} earlier items\n`);
253
+ for (const item of toShow) {
254
+ if (item.type === "text") {
255
+ const preview = expanded ? item.text : item.text.split("\n").slice(0, 3).join("\n");
256
+ text += `${theme.fg("toolOutput", preview)}\n`;
257
+ } else {
258
+ text += `${theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme))}\n`;
202
259
  }
260
+ }
261
+ return text.trimEnd();
262
+ };
203
263
 
204
- const mdTheme = getMarkdownTheme();
205
-
206
- const renderDisplayItems = (items: DisplayItem[], limit?: number) => {
207
- const toShow = limit ? items.slice(-limit) : items;
208
- const skipped = limit && items.length > limit ? items.length - limit : 0;
209
- let text = "";
210
- if (skipped > 0) text += theme.fg("muted", `... ${skipped} earlier items\n`);
211
- for (const item of toShow) {
212
- if (item.type === "text") {
213
- const preview = expanded ? item.text : item.text.split("\n").slice(0, 3).join("\n");
214
- text += `${theme.fg("toolOutput", preview)}\n`;
215
- } else {
216
- text += `${theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme))}\n`;
217
- }
264
+ if (details.mode === "single" && details.results.length === 1) {
265
+ const r = details.results[0];
266
+ const isError = isResultError(r);
267
+ const icon = isError
268
+ ? theme.fg("error", "✗")
269
+ : isPartial
270
+ ? theme.fg("warning", "⏳")
271
+ : theme.fg("success", "✓");
272
+ const displayItems = getDisplayItems(r.messages);
273
+ const finalOutput = getResultFinalOutput(r);
274
+
275
+ if (expanded) {
276
+ const container = new Container();
277
+ let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
278
+ if (isError && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
279
+ container.addChild(new Text(header, 0, 0));
280
+ if (isError && r.errorMessage)
281
+ container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
282
+ container.addChild(new Spacer(1));
283
+ container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
284
+ container.addChild(new Text(theme.fg("dim", r.task), 0, 0));
285
+ container.addChild(new Spacer(1));
286
+ container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0));
287
+ if (displayItems.length === 0 && !finalOutput) {
288
+ container.addChild(new Text(theme.fg("muted", "(no output)"), 0, 0));
289
+ } else {
290
+ for (const item of displayItems) {
291
+ if (item.type === "toolCall")
292
+ container.addChild(
293
+ new Text(
294
+ theme.fg("muted", "→ ") +
295
+ formatToolCall(item.name, item.args, theme.fg.bind(theme)),
296
+ 0,
297
+ 0,
298
+ ),
299
+ );
300
+ }
301
+ if (finalOutput) {
302
+ container.addChild(new Spacer(1));
303
+ container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
218
304
  }
219
- return text.trimEnd();
220
- };
305
+ }
306
+ const usageStr = formatResultUsageStats(r);
307
+ if (usageStr) {
308
+ container.addChild(new Spacer(1));
309
+ container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
310
+ }
311
+ return container;
312
+ }
221
313
 
222
- if (details.mode === "single" && details.results.length === 1) {
223
- const r = details.results[0];
224
- const isError = r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted";
225
- const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
314
+ const collapsed = getCollapsedDisplayItems(r);
315
+ let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
316
+ if (isError && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
317
+ if (isError && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
318
+ if (collapsed.items.length > 0) {
319
+ text += `\n${renderDisplayItems(collapsed.items, COLLAPSED_ITEM_COUNT, collapsed.total)}`;
320
+ if (collapsed.total > COLLAPSED_ITEM_COUNT)
321
+ text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
322
+ } else if (finalOutput.trim()) {
323
+ text += `\n${theme.fg("toolOutput", finalOutput.trim().split("\n").slice(0, 3).join("\n"))}`;
324
+ } else if (!isError || !r.errorMessage) {
325
+ text += `\n${theme.fg("muted", isPartial && !isError ? "(running...)" : "(no output)")}`;
326
+ }
327
+ const usageStr = formatResultUsageStats(r);
328
+ if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
329
+ return new Text(text, 0, 0);
330
+ }
331
+
332
+ const aggregateUsage = (results: SingleResult[]) => {
333
+ const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
334
+ for (const r of results) {
335
+ total.input += r.usage.input;
336
+ total.output += r.usage.output;
337
+ total.cacheRead += r.usage.cacheRead;
338
+ total.cacheWrite += r.usage.cacheWrite;
339
+ total.cost += r.usage.cost;
340
+ total.turns += r.usage.turns;
341
+ }
342
+ return total;
343
+ };
344
+
345
+ if (details.mode === "chain") {
346
+ const currentResult = details.results.at(-1);
347
+ const currentIsRunning =
348
+ isPartial && currentResult !== undefined && !isResultError(currentResult);
349
+ const successCount = details.results.filter(
350
+ (result) => !isResultError(result) && (!currentIsRunning || result !== currentResult),
351
+ ).length;
352
+ const icon = currentIsRunning
353
+ ? theme.fg("warning", "⏳")
354
+ : successCount === details.results.length
355
+ ? theme.fg("success", "✓")
356
+ : theme.fg("error", "✗");
357
+
358
+ if (expanded) {
359
+ const container = new Container();
360
+ container.addChild(
361
+ new Text(
362
+ icon +
363
+ " " +
364
+ theme.fg("toolTitle", theme.bold("chain ")) +
365
+ theme.fg("accent", `${successCount}/${details.results.length} steps`),
366
+ 0,
367
+ 0,
368
+ ),
369
+ );
370
+
371
+ for (const r of details.results) {
372
+ const rFailed = isResultError(r);
373
+ const rIcon = rFailed
374
+ ? theme.fg("error", "✗")
375
+ : currentIsRunning && r === currentResult
376
+ ? theme.fg("warning", "⏳")
377
+ : theme.fg("success", "✓");
226
378
  const displayItems = getDisplayItems(r.messages);
227
379
  const finalOutput = getResultFinalOutput(r);
228
380
 
229
- if (expanded) {
230
- const container = new Container();
231
- let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
232
- if (isError && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
233
- container.addChild(new Text(header, 0, 0));
234
- if (isError && r.errorMessage)
235
- container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
236
- container.addChild(new Spacer(1));
237
- container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
238
- container.addChild(new Text(theme.fg("dim", r.task), 0, 0));
239
- container.addChild(new Spacer(1));
240
- container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0));
241
- if (displayItems.length === 0 && !finalOutput) {
242
- container.addChild(new Text(theme.fg("muted", "(no output)"), 0, 0));
243
- } else {
244
- for (const item of displayItems) {
245
- if (item.type === "toolCall")
246
- container.addChild(
247
- new Text(
248
- theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
249
- 0,
250
- 0,
251
- ),
252
- );
253
- }
254
- if (finalOutput) {
255
- container.addChild(new Spacer(1));
256
- container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
257
- }
258
- }
259
- const usageStr = formatUsageStats(r.usage, r.model, r.thinkingLevel);
260
- if (usageStr) {
261
- container.addChild(new Spacer(1));
262
- container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
381
+ container.addChild(new Spacer(1));
382
+ container.addChild(
383
+ new Text(
384
+ `${theme.fg("muted", `─── Step ${r.step}: `) + theme.fg("accent", r.agent)} ${rIcon}`,
385
+ 0,
386
+ 0,
387
+ ),
388
+ );
389
+ container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
390
+ if (rFailed && r.errorMessage)
391
+ container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
392
+
393
+ // Show tool calls
394
+ for (const item of displayItems) {
395
+ if (item.type === "toolCall") {
396
+ container.addChild(
397
+ new Text(
398
+ theme.fg("muted", "→ ") +
399
+ formatToolCall(item.name, item.args, theme.fg.bind(theme)),
400
+ 0,
401
+ 0,
402
+ ),
403
+ );
263
404
  }
264
- return container;
265
405
  }
266
406
 
267
- let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
268
- if (isError && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
269
- if (isError && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
270
- else if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
271
- else {
272
- text += `\n${renderDisplayItems(displayItems, COLLAPSED_ITEM_COUNT)}`;
273
- if (displayItems.length > COLLAPSED_ITEM_COUNT) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
407
+ // Show final output as markdown
408
+ if (finalOutput) {
409
+ container.addChild(new Spacer(1));
410
+ container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
274
411
  }
275
- const usageStr = formatUsageStats(r.usage, r.model, r.thinkingLevel);
276
- if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
277
- return new Text(text, 0, 0);
412
+
413
+ const stepUsage = formatResultUsageStats(r);
414
+ if (stepUsage) container.addChild(new Text(theme.fg("dim", stepUsage), 0, 0));
278
415
  }
279
416
 
280
- const aggregateUsage = (results: SingleResult[]) => {
281
- const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
282
- for (const r of results) {
283
- total.input += r.usage.input;
284
- total.output += r.usage.output;
285
- total.cacheRead += r.usage.cacheRead;
286
- total.cacheWrite += r.usage.cacheWrite;
287
- total.cost += r.usage.cost;
288
- total.turns += r.usage.turns;
289
- }
290
- return total;
291
- };
417
+ const usageStr = formatUsageStats(aggregateUsage(details.results));
418
+ if (usageStr) {
419
+ container.addChild(new Spacer(1));
420
+ container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
421
+ }
422
+ return container;
423
+ }
292
424
 
293
- if (details.mode === "chain") {
294
- const successCount = details.results.filter((r) => r.exitCode === 0).length;
295
- const icon = successCount === details.results.length ? theme.fg("success", "✓") : theme.fg("error", "✗");
425
+ // Collapsed view
426
+ let text =
427
+ icon +
428
+ " " +
429
+ theme.fg("toolTitle", theme.bold("chain ")) +
430
+ theme.fg("accent", `${successCount}/${details.results.length} steps`);
431
+ for (const r of details.results) {
432
+ const rFailed = isResultError(r);
433
+ const rIcon = rFailed
434
+ ? theme.fg("error", "✗")
435
+ : currentIsRunning && r === currentResult
436
+ ? theme.fg("warning", "⏳")
437
+ : theme.fg("success", "✓");
438
+ const collapsed = getCollapsedDisplayItems(r);
439
+ const finalOutput = getResultFinalOutput(r).trim();
440
+ text += `\n\n${theme.fg("muted", `─── Step ${r.step}: `)}${theme.fg("accent", r.agent)} ${rIcon}`;
441
+ if (rFailed && r.errorMessage)
442
+ text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
443
+ if (collapsed.items.length > 0)
444
+ text += `\n${renderDisplayItems(collapsed.items, 5, collapsed.total)}`;
445
+ else if (currentIsRunning && r === currentResult)
446
+ text += `\n${theme.fg("muted", "(running...)")}`;
447
+ else if (finalOutput)
448
+ text += `\n${theme.fg("toolOutput", finalOutput.split("\n").slice(0, 3).join("\n"))}`;
449
+ else if (!rFailed || !r.errorMessage) text += `\n${theme.fg("muted", "(no output)")}`;
450
+ }
451
+ const usageStr = formatUsageStats(aggregateUsage(details.results));
452
+ if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
453
+ text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
454
+ return new Text(text, 0, 0);
455
+ }
296
456
 
297
- if (expanded) {
298
- const container = new Container();
299
- container.addChild(
300
- new Text(
301
- icon +
302
- " " +
303
- theme.fg("toolTitle", theme.bold("chain ")) +
304
- theme.fg("accent", `${successCount}/${details.results.length} steps`),
305
- 0,
306
- 0,
307
- ),
308
- );
457
+ if (details.mode === "parallel") {
458
+ const resultIsRunning = (result: SingleResult) =>
459
+ result.exitCode === -1 && !isResultError(result);
460
+ const running = details.results.filter(resultIsRunning).length;
461
+ const successCount = details.results.filter(
462
+ (result) => result.exitCode !== -1 && !isResultError(result),
463
+ ).length;
464
+ const failCount = details.results.filter(isResultError).length;
465
+ const aggregator = details.aggregator;
466
+ const aggregatorFailed = aggregator ? isResultError(aggregator) : false;
467
+ const aggregatorRunning = aggregator
468
+ ? !aggregatorFailed && (isPartial || aggregator.exitCode === -1)
469
+ : false;
470
+ const pendingSuccessfulSettlement =
471
+ isPartial && !aggregator && running === 0 && failCount === 0;
472
+ const isRunning = running > 0 || aggregatorRunning || pendingSuccessfulSettlement;
473
+ const icon = isRunning
474
+ ? theme.fg("warning", "⏳")
475
+ : failCount > 0 || aggregatorFailed
476
+ ? theme.fg("warning", "◐")
477
+ : theme.fg("success", "✓");
478
+ const status = isRunning
479
+ ? aggregatorRunning
480
+ ? `${successCount + failCount}/${details.results.length} done, fan-in running`
481
+ : running > 0
482
+ ? `${successCount + failCount}/${details.results.length} done, ${running} running`
483
+ : `${successCount + failCount}/${details.results.length} done, running`
484
+ : aggregator
485
+ ? `${successCount}/${details.results.length} tasks + fan-in`
486
+ : `${successCount}/${details.results.length} tasks`;
487
+
488
+ if (expanded && !isRunning) {
489
+ const container = new Container();
490
+ container.addChild(
491
+ new Text(
492
+ `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`,
493
+ 0,
494
+ 0,
495
+ ),
496
+ );
309
497
 
310
- for (const r of details.results) {
311
- const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
312
- const displayItems = getDisplayItems(r.messages);
313
- const finalOutput = getResultFinalOutput(r);
498
+ for (const r of details.results) {
499
+ const rFailed = isResultError(r);
500
+ const rIcon = rFailed
501
+ ? theme.fg("error", "✗")
502
+ : resultIsRunning(r)
503
+ ? theme.fg("warning", "⏳")
504
+ : theme.fg("success", "✓");
505
+ const displayItems = getDisplayItems(r.messages);
506
+ const finalOutput = getResultFinalOutput(r);
314
507
 
315
- container.addChild(new Spacer(1));
508
+ container.addChild(new Spacer(1));
509
+ container.addChild(
510
+ new Text(`${theme.fg("muted", "─── ") + theme.fg("accent", r.agent)} ${rIcon}`, 0, 0),
511
+ );
512
+ container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
513
+ if (rFailed && r.errorMessage)
514
+ container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
515
+
516
+ // Show tool calls
517
+ for (const item of displayItems) {
518
+ if (item.type === "toolCall") {
316
519
  container.addChild(
317
520
  new Text(
318
- `${theme.fg("muted", `─── Step ${r.step}: `) + theme.fg("accent", r.agent)} ${rIcon}`,
521
+ theme.fg("muted", "→ ") +
522
+ formatToolCall(item.name, item.args, theme.fg.bind(theme)),
319
523
  0,
320
524
  0,
321
525
  ),
322
526
  );
323
- container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
324
-
325
- // Show tool calls
326
- for (const item of displayItems) {
327
- if (item.type === "toolCall") {
328
- container.addChild(
329
- new Text(
330
- theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
331
- 0,
332
- 0,
333
- ),
334
- );
335
- }
336
- }
337
-
338
- // Show final output as markdown
339
- if (finalOutput) {
340
- container.addChild(new Spacer(1));
341
- container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
342
- }
343
-
344
- const stepUsage = formatUsageStats(r.usage, r.model, r.thinkingLevel);
345
- if (stepUsage) container.addChild(new Text(theme.fg("dim", stepUsage), 0, 0));
346
- }
347
-
348
- const usageStr = formatUsageStats(aggregateUsage(details.results));
349
- if (usageStr) {
350
- container.addChild(new Spacer(1));
351
- container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
352
527
  }
353
- return container;
354
528
  }
355
529
 
356
- // Collapsed view
357
- let text =
358
- icon +
359
- " " +
360
- theme.fg("toolTitle", theme.bold("chain ")) +
361
- theme.fg("accent", `${successCount}/${details.results.length} steps`);
362
- for (const r of details.results) {
363
- const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
364
- const displayItems = getDisplayItems(r.messages);
365
- text += `\n\n${theme.fg("muted", `─── Step ${r.step}: `)}${theme.fg("accent", r.agent)} ${rIcon}`;
366
- if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
367
- else text += `\n${renderDisplayItems(displayItems, 5)}`;
530
+ // Show final output as markdown
531
+ if (finalOutput) {
532
+ container.addChild(new Spacer(1));
533
+ container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
368
534
  }
369
- const usageStr = formatUsageStats(aggregateUsage(details.results));
370
- if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
371
- text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
372
- return new Text(text, 0, 0);
535
+
536
+ const taskUsage = formatResultUsageStats(r);
537
+ if (taskUsage) container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0));
373
538
  }
374
539
 
375
- if (details.mode === "parallel") {
376
- const running = details.results.filter((r) => r.exitCode === -1).length;
377
- const successCount = details.results.filter((r) => r.exitCode === 0).length;
378
- const failCount = details.results.filter((r) => r.exitCode > 0).length;
379
- const aggregator = details.aggregator;
380
- const aggregatorRunning = aggregator?.exitCode === -1;
381
- const aggregatorFailed = aggregator ? aggregator.exitCode > 0 || aggregator.stopReason === "error" : false;
382
- const isRunning = running > 0 || aggregatorRunning;
383
- const icon = isRunning
384
- ? theme.fg("warning", "⏳")
385
- : failCount > 0 || aggregatorFailed
386
- ? theme.fg("warning", "◐")
540
+ if (aggregator) {
541
+ const rIcon = aggregatorFailed
542
+ ? theme.fg("error", "✗")
543
+ : aggregatorRunning
544
+ ? theme.fg("warning", "⏳")
387
545
  : theme.fg("success", "✓");
388
- const status = isRunning
389
- ? aggregatorRunning
390
- ? `${successCount + failCount}/${details.results.length} done, fan-in running`
391
- : `${successCount + failCount}/${details.results.length} done, ${running} running`
392
- : aggregator
393
- ? `${successCount}/${details.results.length} tasks + fan-in`
394
- : `${successCount}/${details.results.length} tasks`;
395
-
396
- if (expanded && !isRunning) {
397
- const container = new Container();
546
+ const displayItems = getDisplayItems(aggregator.messages);
547
+ const finalOutput = getResultFinalOutput(aggregator);
548
+
549
+ container.addChild(new Spacer(1));
550
+ container.addChild(
551
+ new Text(
552
+ `${theme.fg("muted", "─── fan-in → ") + theme.fg("accent", aggregator.agent)} ${rIcon}`,
553
+ 0,
554
+ 0,
555
+ ),
556
+ );
557
+ container.addChild(
558
+ new Text(theme.fg("muted", "Task: ") + theme.fg("dim", aggregator.task), 0, 0),
559
+ );
560
+ if (aggregatorFailed && aggregator.errorMessage)
398
561
  container.addChild(
399
- new Text(
400
- `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`,
401
- 0,
402
- 0,
403
- ),
562
+ new Text(theme.fg("error", `Error: ${aggregator.errorMessage}`), 0, 0),
404
563
  );
405
-
406
- for (const r of details.results) {
407
- const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
408
- const displayItems = getDisplayItems(r.messages);
409
- const finalOutput = getResultFinalOutput(r);
410
-
411
- container.addChild(new Spacer(1));
412
- container.addChild(
413
- new Text(`${theme.fg("muted", "─── ") + theme.fg("accent", r.agent)} ${rIcon}`, 0, 0),
414
- );
415
- container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
416
-
417
- // Show tool calls
418
- for (const item of displayItems) {
419
- if (item.type === "toolCall") {
420
- container.addChild(
421
- new Text(
422
- theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
423
- 0,
424
- 0,
425
- ),
426
- );
427
- }
428
- }
429
-
430
- // Show final output as markdown
431
- if (finalOutput) {
432
- container.addChild(new Spacer(1));
433
- container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
434
- }
435
-
436
- const taskUsage = formatUsageStats(r.usage, r.model, r.thinkingLevel);
437
- if (taskUsage) container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0));
438
- }
439
-
440
- if (aggregator) {
441
- const rIcon = aggregator.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
442
- const displayItems = getDisplayItems(aggregator.messages);
443
- const finalOutput = getResultFinalOutput(aggregator);
444
-
445
- container.addChild(new Spacer(1));
564
+ for (const item of displayItems) {
565
+ if (item.type === "toolCall") {
446
566
  container.addChild(
447
567
  new Text(
448
- `${theme.fg("muted", "─── fan-in → ") + theme.fg("accent", aggregator.agent)} ${rIcon}`,
568
+ theme.fg("muted", "→ ") +
569
+ formatToolCall(item.name, item.args, theme.fg.bind(theme)),
449
570
  0,
450
571
  0,
451
572
  ),
452
573
  );
453
- container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", aggregator.task), 0, 0));
454
- for (const item of displayItems) {
455
- if (item.type === "toolCall") {
456
- container.addChild(
457
- new Text(
458
- theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
459
- 0,
460
- 0,
461
- ),
462
- );
463
- }
464
- }
465
- if (finalOutput) {
466
- container.addChild(new Spacer(1));
467
- container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
468
- }
469
- const fanInUsage = formatUsageStats(aggregator.usage, aggregator.model, aggregator.thinkingLevel);
470
- if (fanInUsage) container.addChild(new Text(theme.fg("dim", fanInUsage), 0, 0));
471
- }
472
-
473
- const usageResults = aggregator ? [...details.results, aggregator] : details.results;
474
- const usageStr = formatUsageStats(aggregateUsage(usageResults));
475
- if (usageStr) {
476
- container.addChild(new Spacer(1));
477
- container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
478
574
  }
479
- return container;
480
- }
481
-
482
- // Collapsed view (or still running)
483
- let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`;
484
- for (const r of details.results) {
485
- const rIcon =
486
- r.exitCode === -1
487
- ? theme.fg("warning", "⏳")
488
- : r.exitCode === 0
489
- ? theme.fg("success", "✓")
490
- : theme.fg("error", "✗");
491
- const displayItems = getDisplayItems(r.messages);
492
- text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`;
493
- if (displayItems.length === 0)
494
- text += `\n${theme.fg("muted", r.exitCode === -1 ? "(running...)" : "(no output)")}`;
495
- else text += `\n${renderDisplayItems(displayItems, 5)}`;
496
575
  }
497
- if (aggregator) {
498
- const rIcon =
499
- aggregator.exitCode === -1
500
- ? theme.fg("warning", "⏳")
501
- : aggregator.exitCode === 0
502
- ? theme.fg("success", "✓")
503
- : theme.fg("error", "✗");
504
- const displayItems = getDisplayItems(aggregator.messages);
505
- text += `\n\n${theme.fg("muted", "─── fan-in → ")}${theme.fg("accent", aggregator.agent)} ${rIcon}`;
506
- if (displayItems.length === 0)
507
- text += `\n${theme.fg("muted", aggregator.exitCode === -1 ? "(running...)" : "(no output)")}`;
508
- else text += `\n${renderDisplayItems(displayItems, 5)}`;
509
- }
510
- if (!isRunning) {
511
- const usageResults = aggregator ? [...details.results, aggregator] : details.results;
512
- const usageStr = formatUsageStats(aggregateUsage(usageResults));
513
- if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
576
+ if (finalOutput) {
577
+ container.addChild(new Spacer(1));
578
+ container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
514
579
  }
515
- if (!expanded) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
516
- return new Text(text, 0, 0);
580
+ const fanInUsage = formatResultUsageStats(aggregator);
581
+ if (fanInUsage) container.addChild(new Text(theme.fg("dim", fanInUsage), 0, 0));
582
+ }
583
+
584
+ const usageResults = aggregator ? [...details.results, aggregator] : details.results;
585
+ const usageStr = formatUsageStats(aggregateUsage(usageResults));
586
+ if (usageStr) {
587
+ container.addChild(new Spacer(1));
588
+ container.addChild(new Text(theme.fg("dim", `Total: ${usageStr}`), 0, 0));
517
589
  }
590
+ return container;
591
+ }
592
+
593
+ // Collapsed view (or still running)
594
+ let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`;
595
+ for (const r of details.results) {
596
+ const rFailed = isResultError(r);
597
+ const rRunning = resultIsRunning(r);
598
+ const rIcon = rFailed
599
+ ? theme.fg("error", "✗")
600
+ : rRunning
601
+ ? theme.fg("warning", "⏳")
602
+ : theme.fg("success", "✓");
603
+ const collapsed = getCollapsedDisplayItems(r);
604
+ const finalOutput = getResultFinalOutput(r).trim();
605
+ text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`;
606
+ if (rFailed && r.errorMessage)
607
+ text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
608
+ if (collapsed.items.length > 0)
609
+ text += `\n${renderDisplayItems(collapsed.items, 5, collapsed.total)}`;
610
+ else if (rRunning) text += `\n${theme.fg("muted", "(running...)")}`;
611
+ else if (finalOutput)
612
+ text += `\n${theme.fg("toolOutput", finalOutput.split("\n").slice(0, 3).join("\n"))}`;
613
+ else if (!rFailed || !r.errorMessage) text += `\n${theme.fg("muted", "(no output)")}`;
614
+ }
615
+ if (aggregator) {
616
+ const rIcon = aggregatorFailed
617
+ ? theme.fg("error", "✗")
618
+ : aggregatorRunning
619
+ ? theme.fg("warning", "⏳")
620
+ : theme.fg("success", "✓");
621
+ const collapsed = getCollapsedDisplayItems(aggregator);
622
+ const finalOutput = getResultFinalOutput(aggregator).trim();
623
+ text += `\n\n${theme.fg("muted", "─── fan-in → ")}${theme.fg("accent", aggregator.agent)} ${rIcon}`;
624
+ if (aggregatorFailed && aggregator.errorMessage)
625
+ text += `\n${theme.fg("error", `Error: ${aggregator.errorMessage}`)}`;
626
+ if (collapsed.items.length > 0)
627
+ text += `\n${renderDisplayItems(collapsed.items, 5, collapsed.total)}`;
628
+ else if (aggregatorRunning) text += `\n${theme.fg("muted", "(running...)")}`;
629
+ else if (finalOutput)
630
+ text += `\n${theme.fg("toolOutput", finalOutput.split("\n").slice(0, 3).join("\n"))}`;
631
+ else if (!aggregatorFailed || !aggregator.errorMessage)
632
+ text += `\n${theme.fg("muted", "(no output)")}`;
633
+ }
634
+ if (!isRunning) {
635
+ const usageResults = aggregator ? [...details.results, aggregator] : details.results;
636
+ const usageStr = formatUsageStats(aggregateUsage(usageResults));
637
+ if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
638
+ }
639
+ if (!expanded) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
640
+ return new Text(text, 0, 0);
641
+ }
518
642
 
519
- const text = result.content[0];
520
- return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
643
+ const text = result.content[0];
644
+ return new Text(text?.type === "text" ? text.text : "(no output)", 0, 0);
521
645
  }