oira666_pi-subagent 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -19,13 +19,13 @@ There are many subagent extensions for pi, this one is mine.
19
19
  ### Option 1: Install from npm (recommended)
20
20
 
21
21
  ```bash
22
- pi install npm:@mjakl/pi-subagent
22
+ pi install npm:oira666_pi-subagent
23
23
  ```
24
24
 
25
25
  ### Option 2: Install via git
26
26
 
27
27
  ```bash
28
- pi install git:github.com/mjakl/pi-subagent
28
+ pi install git:github.com/gee666/pi-subagent.git
29
29
  ```
30
30
 
31
31
  ### Option 3: Manual Installation
@@ -34,7 +34,7 @@ Clone this repository to your Pi extensions directory:
34
34
 
35
35
  ```bash
36
36
  cd ~/.pi/agent/extensions
37
- git clone https://github.com/mjakl/pi-subagent.git
37
+ git clone https://github.com/gee666/pi-subagent.git
38
38
  cd pi-subagent
39
39
  npm install
40
40
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oira666_pi-subagent",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Subagent extension for Pi coding agent. Delegate tasks to specialized agents.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/render.ts CHANGED
@@ -2,12 +2,9 @@
2
2
  * TUI rendering for subagent tool calls and results.
3
3
  */
4
4
 
5
- import * as os from "node:os";
6
- import { getMarkdownTheme } from "@mariozechner/pi-coding-agent";
7
- import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
5
+ import { Container, Spacer, Text } from "@mariozechner/pi-tui";
8
6
  import {
9
7
  type DelegationMode,
10
- type DisplayItem,
11
8
  type NestedSubagentResult,
12
9
  type SingleResult,
13
10
  type SubagentDetails,
@@ -18,11 +15,36 @@ import {
18
15
  getFinalOutput,
19
16
  getNestedSubagentResults,
20
17
  isResultError,
18
+ isSubagentDetails,
21
19
  } from "./types.js";
22
20
 
23
- const COLLAPSED_LINE_COUNT = 10;
24
- const COLLAPSED_PARALLEL_LINE_COUNT = 5;
25
- const NESTED_PREVIEW_LINE_COUNT = 6;
21
+ const OUTPUT_PREVIEW_LINE_COUNT = 6;
22
+
23
+ type ThemeFg = (color: string, text: string) => string;
24
+ type NodeStatus = "running" | "success" | "error";
25
+
26
+ interface TreeNode {
27
+ label: string;
28
+ status: NodeStatus;
29
+ meta?: string;
30
+ task?: string;
31
+ outputPreview?: string[];
32
+ children: TreeNode[];
33
+ }
34
+
35
+ interface TreeCounts {
36
+ total: number;
37
+ running: number;
38
+ success: number;
39
+ error: number;
40
+ finished: number;
41
+ }
42
+
43
+ interface PendingSubagentCall {
44
+ toolCallId: string;
45
+ mode: DelegationMode;
46
+ tasks: Array<{ agent: string; task?: string }>;
47
+ }
26
48
 
27
49
  // ---------------------------------------------------------------------------
28
50
  // Formatting helpers
@@ -43,243 +65,221 @@ function formatUsage(usage: Partial<UsageStats>, model?: string): string {
43
65
  (usage.cacheRead || 0) +
44
66
  (usage.cacheWrite || 0);
45
67
  if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
46
- if (totalTokens > 0) parts.push(`tokens:${formatTokens(totalTokens)}`);
68
+ if (totalTokens > 0) parts.push(`tok:${formatTokens(totalTokens)}`);
47
69
  if (usage.input) parts.push(`in:${formatTokens(usage.input)}`);
48
70
  if (usage.output) parts.push(`out:${formatTokens(usage.output)}`);
49
71
  if (usage.cacheRead) parts.push(`cacheR:${formatTokens(usage.cacheRead)}`);
50
72
  if (usage.cacheWrite) parts.push(`cacheW:${formatTokens(usage.cacheWrite)}`);
51
- if (usage.cost) parts.push(`cost:$${usage.cost.toFixed(4)}`);
73
+ if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
52
74
  if (usage.contextTokens && usage.contextTokens > 0) parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
53
75
  if (model) parts.push(model);
54
- return parts.join(" ");
76
+ return parts.join(" ");
55
77
  }
56
78
 
57
79
  function truncate(text: string, maxLen: number): string {
58
80
  return text.length > maxLen ? `${text.slice(0, maxLen)}...` : text;
59
81
  }
60
82
 
61
- function shortenPath(p: string): string {
62
- const home = os.homedir();
63
- return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
64
- }
65
-
66
83
  function normalizeDelegationMode(raw: unknown): DelegationMode {
67
84
  return raw === "fork" ? "fork" : DEFAULT_DELEGATION_MODE;
68
85
  }
69
86
 
70
- type ThemeFg = (color: string, text: string) => string;
87
+ function splitOutputLines(text: string): string[] {
88
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
89
+ if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
90
+ return lines;
91
+ }
71
92
 
72
- function firstNonEmptyLine(text: string): string {
73
- for (const line of splitOutputLines(text)) {
74
- if (line.trim()) return line.trim();
75
- }
76
- return "";
93
+ function lastNonEmptyLines(text: string, limit: number): string[] {
94
+ return splitOutputLines(text)
95
+ .map((line) => line.trimEnd())
96
+ .filter((line) => line.trim().length > 0)
97
+ .slice(-limit);
77
98
  }
78
99
 
79
- function formatToolCall(toolName: string, args: Record<string, unknown>, fg: ThemeFg): string {
80
- const pathArg = (args.file_path || args.path || "...") as string;
81
-
82
- switch (toolName) {
83
- case "subagent": {
84
- const mode = normalizeDelegationMode(args.mode);
85
- const tasks = Array.isArray(args.tasks) ? args.tasks : [];
86
- if (tasks.length > 1) {
87
- const agents = tasks
88
- .slice(0, 3)
89
- .map((task) => typeof task?.agent === "string" ? task.agent : "...")
90
- .join(", ");
91
- const extra = tasks.length > 3 ? ` +${tasks.length - 3}` : "";
92
- return fg("toolTitle", "subagent ") + fg("accent", `parallel (${tasks.length})`) + fg("muted", ` [${mode}]`) + fg("dim", agents ? ` ${agents}${extra}` : "");
93
- }
94
- const task = tasks[0] as { agent?: string; task?: string } | undefined;
95
- return fg("toolTitle", "subagent ") + fg("accent", task?.agent || "...") + fg("muted", ` [${mode}]`) + fg("dim", task?.task ? ` ${truncate(task.task, 48)}` : "");
96
- }
97
- case "bash": {
98
- const cmd = (args.command as string) || "...";
99
- return fg("muted", "$ ") + fg("toolOutput", truncate(cmd, 60));
100
- }
101
- case "read": {
102
- let text = fg("accent", shortenPath(pathArg));
103
- const offset = args.offset as number | undefined;
104
- const limit = args.limit as number | undefined;
105
- if (offset !== undefined || limit !== undefined) {
106
- const start = offset ?? 1;
107
- const end = limit !== undefined ? start + limit - 1 : "";
108
- text += fg("warning", `:${start}${end ? `-${end}` : ""}`);
109
- }
110
- return fg("muted", "read ") + text;
111
- }
112
- case "write": {
113
- const lines = ((args.content || "") as string).split("\n").length;
114
- let text = fg("muted", "write ") + fg("accent", shortenPath(pathArg));
115
- if (lines > 1) text += fg("dim", ` (${lines} lines)`);
116
- return text;
117
- }
118
- case "edit":
119
- return fg("muted", "edit ") + fg("accent", shortenPath(pathArg));
120
- case "ls":
121
- return fg("muted", "ls ") + fg("accent", shortenPath((args.path || ".") as string));
122
- case "find":
123
- return fg("muted", "find ") + fg("accent", (args.pattern || "*") as string) + fg("dim", ` in ${shortenPath((args.path || ".") as string)}`);
124
- case "grep":
125
- return fg("muted", "grep ") + fg("accent", `/${(args.pattern || "") as string}/`) + fg("dim", ` in ${shortenPath((args.path || ".") as string)}`);
100
+ function statusEmoji(status: NodeStatus, theme: { fg: ThemeFg }): string {
101
+ switch (status) {
102
+ case "running":
103
+ return theme.fg("warning", "⏳");
104
+ case "error":
105
+ return theme.fg("error", "❌");
126
106
  default:
127
- return fg("accent", toolName) + fg("dim", ` ${truncate(JSON.stringify(args), 50)}`);
107
+ return theme.fg("success", "");
128
108
  }
129
109
  }
130
110
 
131
- // ---------------------------------------------------------------------------
132
- // Shared rendering building blocks
133
- // ---------------------------------------------------------------------------
111
+ function statusFromResult(result: SingleResult): NodeStatus {
112
+ if (result.exitCode === -1) return "running";
113
+ return isResultError(result) ? "error" : "success";
114
+ }
134
115
 
135
- function splitOutputLines(text: string): string[] {
136
- const lines = text.replace(/\r\n?/g, "\n").split("\n");
137
- if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
138
- return lines;
116
+ function countNodes(nodes: TreeNode[]): TreeCounts {
117
+ const counts: TreeCounts = {
118
+ total: 0,
119
+ running: 0,
120
+ success: 0,
121
+ error: 0,
122
+ finished: 0,
123
+ };
124
+
125
+ const visit = (node: TreeNode) => {
126
+ counts.total++;
127
+ if (node.status === "running") counts.running++;
128
+ if (node.status === "success") counts.success++;
129
+ if (node.status === "error") counts.error++;
130
+ if (node.status !== "running") counts.finished++;
131
+ for (const child of node.children) visit(child);
132
+ };
133
+
134
+ for (const node of nodes) visit(node);
135
+ return counts;
139
136
  }
140
137
 
141
- function countDisplayLines(items: DisplayItem[]): number {
142
- let count = 0;
143
- for (const item of items) {
144
- count += item.type === "text" ? splitOutputLines(item.text).length : 1;
145
- }
146
- return count;
138
+ function hasNestedChildren(nodes: TreeNode[]): boolean {
139
+ return nodes.some((node) => node.children.length > 0 || hasNestedChildren(node.children));
147
140
  }
148
141
 
149
- function renderDisplayItems(
150
- items: DisplayItem[],
151
- expanded: boolean,
152
- theme: { fg: ThemeFg },
153
- limit?: number,
154
- ): string {
155
- const lines: string[] = [];
156
- for (const item of items) {
157
- if (item.type === "text") {
158
- for (const line of splitOutputLines(item.text)) {
159
- lines.push(theme.fg("toolOutput", line));
160
- }
161
- } else {
162
- lines.push(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)));
142
+ function extractPendingSubagentCalls(messages: SingleResult["messages"]): PendingSubagentCall[] {
143
+ const calls: PendingSubagentCall[] = [];
144
+ for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {
145
+ const message = messages[messageIndex] as any;
146
+ if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
147
+ for (let partIndex = 0; partIndex < message.content.length; partIndex++) {
148
+ const part = message.content[partIndex] as any;
149
+ if (part?.type !== "toolCall" || part?.name !== "subagent") continue;
150
+ const args = part.arguments && typeof part.arguments === "object" ? part.arguments : {};
151
+ const tasks = Array.isArray((args as any).tasks)
152
+ ? (args as any).tasks
153
+ .filter((task: any) => task && typeof task.agent === "string")
154
+ .map((task: any) => ({
155
+ agent: task.agent,
156
+ task: typeof task.task === "string" ? task.task : undefined,
157
+ }))
158
+ : [];
159
+ calls.push({
160
+ toolCallId:
161
+ typeof part.toolCallId === "string"
162
+ ? part.toolCallId
163
+ : typeof part.id === "string"
164
+ ? part.id
165
+ : `${messageIndex}:${partIndex}`,
166
+ mode: normalizeDelegationMode((args as any).mode),
167
+ tasks,
168
+ });
163
169
  }
164
170
  }
171
+ return calls;
172
+ }
165
173
 
166
- const shouldTail = !expanded && typeof limit === "number";
167
- const toShow = shouldTail ? lines.slice(-limit) : lines;
168
- const skipped = shouldTail && lines.length > limit ? lines.length - limit : 0;
169
-
170
- let text = "";
171
- if (skipped > 0) text += theme.fg("muted", `... ${skipped} earlier lines\n`);
172
- text += toShow.join("\n");
173
- return text.trimEnd();
174
+ function buildPendingNodes(call: PendingSubagentCall): TreeNode[] {
175
+ return call.tasks.map((task) => ({
176
+ label: task.agent,
177
+ status: "running",
178
+ meta: call.mode === "fork" ? "fork" : "spawn",
179
+ task: task.task,
180
+ children: [],
181
+ }));
174
182
  }
175
183
 
176
- function statusIcon(r: SingleResult, theme: { fg: ThemeFg }): string {
177
- if (r.exitCode === -1) return theme.fg("warning", "⏳");
178
- return isResultError(r) ? theme.fg("error", "✗") : theme.fg("success", "✓");
184
+ function buildNodesFromNestedResult(nested: NestedSubagentResult): TreeNode[] {
185
+ return nested.details.results.map((result) => buildResultNode(result, nested.details.delegationMode));
179
186
  }
180
187
 
181
- function parallelStatus(details: SubagentDetails): {
182
- icon: string;
183
- status: string;
184
- isRunning: boolean;
185
- } {
186
- const running = details.results.filter((r) => r.exitCode === -1).length;
187
- const successCount = details.results.filter((r) => r.exitCode === 0).length;
188
- const failCount = details.results.filter((r) => r.exitCode > 0).length;
189
- const isRunning = running > 0;
190
- const icon = isRunning ? "⏳" : failCount > 0 ? "◐" : "✓";
191
- const status = isRunning
192
- ? `${successCount + failCount}/${details.results.length} done, ${running} running`
193
- : `${successCount}/${details.results.length} tasks`;
194
- return { icon, status, isRunning };
188
+ function buildNestedChildren(result: SingleResult): TreeNode[] {
189
+ const completedByToolCallId = new Map<string, NestedSubagentResult>();
190
+ for (const nested of getNestedSubagentResults(result.messages)) {
191
+ completedByToolCallId.set(nested.toolCallId, nested);
192
+ }
193
+
194
+ const nodes: TreeNode[] = [];
195
+ for (const call of extractPendingSubagentCalls(result.messages)) {
196
+ const completed = completedByToolCallId.get(call.toolCallId);
197
+ if (completed && isSubagentDetails(completed.details)) {
198
+ nodes.push(...buildNodesFromNestedResult(completed));
199
+ continue;
200
+ }
201
+ nodes.push(...buildPendingNodes(call));
202
+ }
203
+ return nodes;
195
204
  }
196
205
 
197
- function renderNestedDelegationLines(
198
- items: NestedSubagentResult[],
199
- theme: { fg: ThemeFg },
200
- expanded: boolean,
201
- depth = 0,
202
- ): string[] {
206
+ function buildLeafPreview(result: SingleResult): string[] | undefined {
207
+ const items = getDisplayItems(result.messages);
203
208
  const lines: string[] = [];
204
209
  for (const item of items) {
205
- lines.push(...renderNestedDetailsLines(item.details, theme, expanded, depth));
210
+ if (item.type === "text") {
211
+ lines.push(...lastNonEmptyLines(item.text, OUTPUT_PREVIEW_LINE_COUNT));
212
+ }
206
213
  }
207
- return lines;
214
+ const finalOutput = getFinalOutput(result.messages);
215
+ if (finalOutput) lines.push(...lastNonEmptyLines(finalOutput, OUTPUT_PREVIEW_LINE_COUNT));
216
+ const unique = lines.filter((line, index) => line && lines.indexOf(line) === index);
217
+ return unique.length > 0 ? unique.slice(-OUTPUT_PREVIEW_LINE_COUNT) : undefined;
208
218
  }
209
219
 
210
- function renderNestedDetailsLines(
211
- details: SubagentDetails,
220
+ function buildResultNode(result: SingleResult, delegationMode: DelegationMode): TreeNode {
221
+ const status = statusFromResult(result);
222
+ const usage = formatUsage(result.usage, result.model);
223
+ const metaParts: string[] = [result.agentSource, delegationMode];
224
+ if (usage) metaParts.push(usage);
225
+ if (status === "error") {
226
+ const errorText = result.errorMessage || result.stderr || result.stopReason;
227
+ if (errorText) metaParts.push(truncate(errorText.replace(/\s+/g, " "), 120));
228
+ }
229
+
230
+ const children = buildNestedChildren(result);
231
+ return {
232
+ label: result.agent,
233
+ status,
234
+ meta: metaParts.join(" • "),
235
+ task: result.task,
236
+ outputPreview: children.length === 0 ? buildLeafPreview(result) : undefined,
237
+ children,
238
+ };
239
+ }
240
+
241
+ function buildTopLevelNodes(details: SubagentDetails): TreeNode[] {
242
+ return details.results.map((result) => buildResultNode(result, details.delegationMode));
243
+ }
244
+
245
+ function renderTreeLines(
246
+ nodes: TreeNode[],
212
247
  theme: { fg: ThemeFg },
213
- expanded: boolean,
248
+ showOutputPreview: boolean,
214
249
  depth = 0,
215
250
  ): string[] {
216
- const indent = " ".repeat(depth);
217
- if (details.mode === "single") {
218
- const result = details.results[0];
219
- if (!result) return [];
220
-
221
- const lines: string[] = [];
222
- let header = `${indent}${theme.fg("muted", "↳ ")}${theme.fg("toolTitle", "subagent ")}${theme.fg("accent", result.agent)} ${statusIcon(result, theme)}${theme.fg("muted", ` [${details.delegationMode}]`)}`;
223
- const usage = formatUsage(result.usage, result.model);
224
- if (usage) header += ` ${theme.fg("dim", usage)}`;
225
- lines.push(header);
226
-
227
- if (!expanded) return lines;
228
-
229
- lines.push(`${indent} ${theme.fg("muted", "task: ")}${theme.fg("dim", truncate(result.task, 96))}`);
230
- const nested = getNestedSubagentResults(result.messages);
231
- if (nested.length > 0) {
232
- lines.push(...renderNestedDelegationLines(nested, theme, true, depth + 1));
233
- }
234
- const finalLine = firstNonEmptyLine(getFinalOutput(result.messages));
235
- if (finalLine) {
236
- lines.push(`${indent} ${theme.fg("muted", "final: ")}${theme.fg("toolOutput", truncate(finalLine, 96))}`);
237
- }
238
- return lines;
239
- }
240
-
241
- const { icon, status } = parallelStatus(details);
242
251
  const lines: string[] = [];
243
- let header = `${indent}${theme.fg("muted", "↳ ")}${theme.fg("toolTitle", "parallel delegation ")}${theme.fg("accent", status)} ${theme.fg("muted", `[${details.delegationMode}]`)} ${theme.fg(icon === "✓" ? "success" : icon === "⏳" ? "warning" : "warning", icon)}`;
244
- const totalUsage = formatUsage(aggregateUsage(details.results));
245
- if (totalUsage) header += ` ${theme.fg("dim", totalUsage)}`;
246
- lines.push(header);
247
252
 
248
- for (const result of details.results) {
249
- let line = `${indent} ${statusIcon(result, theme)} ${theme.fg("accent", result.agent)}`;
250
- const usage = formatUsage(result.usage, result.model);
251
- if (usage) line += ` ${theme.fg("dim", usage)}`;
253
+ for (const node of nodes) {
254
+ const indent = " ".repeat(depth);
255
+ let line = `${indent}${statusEmoji(node.status, theme)} ${theme.fg("accent", node.label)}`;
256
+ if (node.meta) line += ` ${theme.fg("dim", node.meta)}`;
252
257
  lines.push(line);
253
- if (expanded) {
254
- lines.push(`${indent} ${theme.fg("muted", "task: ")}${theme.fg("dim", truncate(result.task, 88))}`);
255
- const nested = getNestedSubagentResults(result.messages);
256
- if (nested.length > 0) {
257
- lines.push(...renderNestedDelegationLines(nested, theme, true, depth + 2));
258
- }
259
- const finalLine = firstNonEmptyLine(getFinalOutput(result.messages));
260
- if (finalLine) {
261
- lines.push(`${indent} ${theme.fg("muted", "final: ")}${theme.fg("toolOutput", truncate(finalLine, 88))}`);
258
+
259
+ if (showOutputPreview && node.outputPreview && node.outputPreview.length > 0) {
260
+ for (const outputLine of node.outputPreview) {
261
+ lines.push(`${indent} ${theme.fg("toolOutput", outputLine)}`);
262
262
  }
263
263
  }
264
+
265
+ if (node.children.length > 0) {
266
+ lines.push(...renderTreeLines(node.children, theme, false, depth + 1));
267
+ }
264
268
  }
265
269
 
266
270
  return lines;
267
271
  }
268
272
 
269
- function nestedDelegationText(
270
- messages: SingleResult["messages"],
271
- theme: { fg: ThemeFg },
272
- expanded: boolean,
273
- ): string {
274
- const nested = getNestedSubagentResults(messages);
275
- if (nested.length === 0) return "";
276
- const lines = renderNestedDelegationLines(nested, theme, expanded);
277
- if (expanded) return lines.join("\n");
278
- if (lines.length <= NESTED_PREVIEW_LINE_COUNT) return lines.join("\n");
279
- return [
280
- ...lines.slice(0, NESTED_PREVIEW_LINE_COUNT),
281
- theme.fg("muted", `... ${lines.length - NESTED_PREVIEW_LINE_COUNT} more delegation lines`),
282
- ].join("\n");
273
+ function topLevelSummary(details: SubagentDetails, counts: TreeCounts): string {
274
+ const totalUsage = formatUsage(aggregateUsage(details.results));
275
+ const parts = [
276
+ `${counts.running} running`,
277
+ `${counts.finished}/${counts.total} finished`,
278
+ `${counts.success} ok`,
279
+ `${counts.error} error`,
280
+ ];
281
+ if (totalUsage) parts.push(totalUsage);
282
+ return parts.join("");
283
283
  }
284
284
 
285
285
  // ---------------------------------------------------------------------------
@@ -288,39 +288,25 @@ function nestedDelegationText(
288
288
 
289
289
  export function renderCall(args: Record<string, any>, theme: { fg: ThemeFg; bold: (s: string) => string }): Text {
290
290
  const delegationMode = normalizeDelegationMode(args.mode);
291
- const modeBadge = theme.fg("muted", ` [${delegationMode}]`);
292
291
  const tasks = Array.isArray(args.tasks) ? args.tasks : [];
293
-
294
- if (tasks.length > 1) {
295
- let text =
296
- theme.fg("toolTitle", theme.bold("subagent ")) +
297
- theme.fg("accent", `parallel (${tasks.length} tasks)`) +
298
- modeBadge;
299
- for (const t of tasks.slice(0, 3)) {
300
- text += `\n ${theme.fg("accent", t.agent)}${theme.fg("dim", ` ${truncate(t.task, 40)}`)}`;
301
- }
302
- if (tasks.length > 3) text += `\n ${theme.fg("muted", `... +${tasks.length - 3} more`)}`;
303
- return new Text(text, 0, 0);
292
+ const count = tasks.length;
293
+ let text = `${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("muted", `[${delegationMode}]`)} ${theme.fg("accent", `${count} task${count === 1 ? "" : "s"}`)}`;
294
+ for (const task of tasks.slice(0, 6)) {
295
+ const agent = typeof task?.agent === "string" ? task.agent : "...";
296
+ const preview = typeof task?.task === "string" ? ` ${truncate(task.task, 56)}` : "";
297
+ text += `\n ${theme.fg("warning", "⏳")} ${theme.fg("accent", agent)}${theme.fg("dim", preview)}`;
304
298
  }
305
-
306
- const singleTask = tasks[0];
307
- const agentName = singleTask?.agent || args.agent || "...";
308
- const preview = singleTask?.task ? truncate(singleTask.task, 60) : args.task ? truncate(args.task, 60) : "...";
309
- let text =
310
- theme.fg("toolTitle", theme.bold("subagent ")) +
311
- theme.fg("accent", agentName) +
312
- modeBadge;
313
- text += `\n ${theme.fg("dim", preview)}`;
299
+ if (tasks.length > 6) text += `\n ${theme.fg("muted", `... +${tasks.length - 6} more`)}`;
314
300
  return new Text(text, 0, 0);
315
301
  }
316
302
 
317
303
  // ---------------------------------------------------------------------------
318
- // renderResult — shown after the tool completes
304
+ // renderResult — shown after the tool completes / streams updates
319
305
  // ---------------------------------------------------------------------------
320
306
 
321
307
  export function renderResult(
322
308
  result: { content: Array<{ type: string; text?: string }>; details?: unknown },
323
- expanded: boolean,
309
+ _expanded: boolean,
324
310
  theme: { fg: ThemeFg; bold: (s: string) => string },
325
311
  ): Container | Text {
326
312
  const details = result.details as SubagentDetails | undefined;
@@ -329,267 +315,26 @@ export function renderResult(
329
315
  return new Text(first?.type === "text" && first.text ? first.text : "(no output)", 0, 0);
330
316
  }
331
317
 
332
- const delegationMode = normalizeDelegationMode(
333
- (details as Partial<SubagentDetails>).delegationMode,
334
- );
335
- if (details.mode === "single") {
336
- return renderSingleResult(details.results[0], delegationMode, expanded, theme);
337
- }
338
- return renderParallelResult(details, delegationMode, expanded, theme);
339
- }
340
-
341
- // ---------------------------------------------------------------------------
342
- // Single-mode result
343
- // ---------------------------------------------------------------------------
344
-
345
- function renderSingleResult(
346
- r: SingleResult,
347
- delegationMode: DelegationMode,
348
- expanded: boolean,
349
- theme: { fg: ThemeFg; bold: (s: string) => string },
350
- ): Container | Text {
351
- const error = isResultError(r);
352
- const icon = statusIcon(r, theme);
353
- const displayItems = getDisplayItems(r.messages);
354
- const finalOutput = getFinalOutput(r.messages);
355
- const nestedDelegations = nestedDelegationText(r.messages, theme, expanded);
356
-
357
- if (expanded) {
358
- return renderSingleExpanded(
359
- r,
360
- delegationMode,
361
- icon,
362
- error,
363
- displayItems,
364
- finalOutput,
365
- nestedDelegations,
366
- theme,
367
- );
368
- }
369
- return renderSingleCollapsed(r, delegationMode, icon, error, displayItems, nestedDelegations, theme);
370
- }
371
-
372
- function renderSingleExpanded(
373
- r: SingleResult,
374
- delegationMode: DelegationMode,
375
- icon: string,
376
- error: boolean,
377
- displayItems: DisplayItem[],
378
- finalOutput: string,
379
- nestedDelegations: string,
380
- theme: { fg: ThemeFg; bold: (s: string) => string },
381
- ): Container {
382
- const mdTheme = getMarkdownTheme();
383
- const container = new Container();
384
-
385
- // Header
386
- let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource}, ${delegationMode})`)}`;
387
- if (error && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
388
- container.addChild(new Text(header, 0, 0));
389
- if (error && r.errorMessage) {
390
- container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
391
- }
392
-
393
- // Task
394
- container.addChild(new Spacer(1));
395
- container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
396
- container.addChild(new Text(theme.fg("dim", r.task), 0, 0));
397
-
398
- if (nestedDelegations) {
399
- container.addChild(new Spacer(1));
400
- container.addChild(new Text(theme.fg("muted", "─── Delegation tree ───"), 0, 0));
401
- container.addChild(new Text(nestedDelegations, 0, 0));
402
- }
403
-
404
- // Output
405
- container.addChild(new Spacer(1));
406
- container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0));
407
- if (displayItems.length === 0 && !finalOutput) {
408
- container.addChild(new Text(theme.fg("muted", "(no output)"), 0, 0));
409
- } else {
410
- for (const item of displayItems) {
411
- if (item.type === "toolCall") {
412
- container.addChild(new Text(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0));
413
- }
414
- }
415
- if (finalOutput) {
416
- container.addChild(new Spacer(1));
417
- container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
418
- }
419
- }
420
-
421
- // Usage
422
- const usageStr = formatUsage(r.usage, r.model);
423
- if (usageStr) {
424
- container.addChild(new Spacer(1));
425
- container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
426
- }
427
-
428
- return container;
429
- }
430
-
431
- function renderSingleCollapsed(
432
- r: SingleResult,
433
- delegationMode: DelegationMode,
434
- icon: string,
435
- error: boolean,
436
- displayItems: DisplayItem[],
437
- nestedDelegations: string,
438
- theme: { fg: ThemeFg; bold: (s: string) => string },
439
- ): Text {
440
- let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource}, ${delegationMode})`)}`;
441
- if (error && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
442
-
443
- if (error && r.errorMessage) {
444
- text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
445
- } else if (displayItems.length === 0) {
446
- text += `\n${theme.fg("muted", "(no output)")}`;
447
- } else {
448
- text += `\n${renderDisplayItems(displayItems, false, theme, COLLAPSED_LINE_COUNT)}`;
449
- if (countDisplayLines(displayItems) > COLLAPSED_LINE_COUNT) {
450
- text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
451
- }
452
- }
453
-
454
- if (nestedDelegations) {
455
- text += `\n\n${theme.fg("muted", "Delegation tree:")}`;
456
- text += `\n${nestedDelegations}`;
457
- }
458
-
459
- const usageStr = formatUsage(r.usage, r.model);
460
- if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
461
- return new Text(text, 0, 0);
462
- }
463
-
464
- // ---------------------------------------------------------------------------
465
- // Parallel-mode result
466
- // ---------------------------------------------------------------------------
467
-
468
- function renderParallelResult(
469
- details: SubagentDetails,
470
- delegationMode: DelegationMode,
471
- expanded: boolean,
472
- theme: { fg: ThemeFg; bold: (s: string) => string },
473
- ): Container | Text {
474
- const running = details.results.filter((r) => r.exitCode === -1).length;
475
- const successCount = details.results.filter((r) => r.exitCode === 0).length;
476
- const failCount = details.results.filter((r) => r.exitCode > 0).length;
477
- const isRunning = running > 0;
478
-
479
- const icon = isRunning
318
+ const nodes = buildTopLevelNodes(details);
319
+ const counts = countNodes(nodes);
320
+ const showOutputPreview = !hasNestedChildren(nodes);
321
+ const icon = counts.running > 0
480
322
  ? theme.fg("warning", "⏳")
481
- : failCount > 0
482
- ? theme.fg("warning", "")
483
- : theme.fg("success", "");
484
-
485
- const status = isRunning
486
- ? `${successCount + failCount}/${details.results.length} done, ${running} running`
487
- : `${successCount}/${details.results.length} tasks`;
488
-
489
- if (expanded && !isRunning) {
490
- return renderParallelExpanded(details, delegationMode, icon, status, theme);
491
- }
492
- return renderParallelCollapsed(
493
- details,
494
- delegationMode,
495
- icon,
496
- status,
497
- isRunning,
498
- expanded,
499
- theme,
500
- );
501
- }
323
+ : counts.error > 0
324
+ ? theme.fg("error", "")
325
+ : theme.fg("success", "");
502
326
 
503
- function renderParallelExpanded(
504
- details: SubagentDetails,
505
- delegationMode: DelegationMode,
506
- icon: string,
507
- status: string,
508
- theme: { fg: ThemeFg; bold: (s: string) => string },
509
- ): Container {
510
- const mdTheme = getMarkdownTheme();
511
327
  const container = new Container();
512
328
  container.addChild(
513
329
  new Text(
514
- `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}${theme.fg("muted", ` [${delegationMode}]`)}`,
330
+ `${icon} ${theme.fg("toolTitle", theme.bold("subagent tree "))}${theme.fg("muted", `[${details.delegationMode}]`)} ${theme.fg("dim", topLevelSummary(details, counts))}`,
515
331
  0,
516
332
  0,
517
333
  ),
518
334
  );
519
335
 
520
- for (const r of details.results) {
521
- const rIcon = statusIcon(r, theme);
522
- const displayItems = getDisplayItems(r.messages);
523
- const finalOutput = getFinalOutput(r.messages);
524
- const nestedDelegations = nestedDelegationText(r.messages, theme, true);
525
-
526
- container.addChild(new Spacer(1));
527
- container.addChild(new Text(`${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`, 0, 0));
528
- container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
529
-
530
- if (nestedDelegations) {
531
- container.addChild(new Text(theme.fg("muted", "Delegation tree:"), 0, 0));
532
- container.addChild(new Text(nestedDelegations, 0, 0));
533
- }
534
-
535
- for (const item of displayItems) {
536
- if (item.type === "toolCall") {
537
- container.addChild(new Text(theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)), 0, 0));
538
- }
539
- }
540
-
541
- if (finalOutput) {
542
- container.addChild(new Spacer(1));
543
- container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
544
- }
545
-
546
- const taskUsage = formatUsage(r.usage, r.model);
547
- if (taskUsage) container.addChild(new Text(theme.fg("dim", taskUsage), 0, 0));
548
- }
549
-
550
- const totalUsage = formatUsage(aggregateUsage(details.results));
551
- if (totalUsage) {
552
- container.addChild(new Spacer(1));
553
- container.addChild(new Text(theme.fg("dim", `Total: ${totalUsage}`), 0, 0));
554
- }
336
+ container.addChild(new Spacer(1));
337
+ container.addChild(new Text(renderTreeLines(nodes, theme, showOutputPreview).join("\n"), 0, 0));
555
338
 
556
339
  return container;
557
340
  }
558
-
559
- function renderParallelCollapsed(
560
- details: SubagentDetails,
561
- delegationMode: DelegationMode,
562
- icon: string,
563
- status: string,
564
- isRunning: boolean,
565
- expanded: boolean,
566
- theme: { fg: ThemeFg; bold: (s: string) => string },
567
- ): Text {
568
- let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}${theme.fg("muted", ` [${delegationMode}]`)}`;
569
-
570
- for (const r of details.results) {
571
- const rIcon = statusIcon(r, theme);
572
- const displayItems = getDisplayItems(r.messages);
573
- const nestedDelegations = nestedDelegationText(r.messages, theme, false);
574
- text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`;
575
- if (displayItems.length === 0) {
576
- text += `\n${theme.fg("muted", r.exitCode === -1 ? "(running...)" : "(no output)")}`;
577
- } else {
578
- text += `\n${renderDisplayItems(displayItems, false, theme, COLLAPSED_PARALLEL_LINE_COUNT)}`;
579
- }
580
- if (nestedDelegations) {
581
- text += `\n${theme.fg("muted", "Delegation tree:")}`;
582
- text += `\n${nestedDelegations}`;
583
- }
584
- const taskUsage = formatUsage(r.usage, r.model);
585
- if (taskUsage) text += `\n${theme.fg("dim", taskUsage)}`;
586
- }
587
-
588
- if (!isRunning) {
589
- const totalUsage = formatUsage(aggregateUsage(details.results));
590
- if (totalUsage) text += `\n\n${theme.fg("dim", `Total: ${totalUsage}`)}`;
591
- }
592
- if (!expanded) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
593
-
594
- return new Text(text, 0, 0);
595
- }
package/runner.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  type SubagentDetails,
18
18
  emptyUsage,
19
19
  getFinalOutput,
20
+ getNestedSubagentErrorSummary,
20
21
  } from "./types.js";
21
22
 
22
23
  const SIGKILL_TIMEOUT_MS = 5000;
@@ -291,6 +292,8 @@ export async function runAgent(opts: RunAgentOptions): Promise<SingleResult> {
291
292
  });
292
293
  };
293
294
 
295
+ emitUpdate();
296
+
294
297
  // Write system prompt to temp file if needed
295
298
  let promptTmpDir: string | null = null;
296
299
  let promptTmpPath: string | null = null;
@@ -382,6 +385,17 @@ export async function runAgent(opts: RunAgentOptions): Promise<SingleResult> {
382
385
  result.errorMessage = "Subagent was aborted.";
383
386
  if (!result.stderr.trim()) result.stderr = "Subagent was aborted.";
384
387
  }
388
+
389
+ if (result.exitCode === 0) {
390
+ const nestedErrorSummary = getNestedSubagentErrorSummary(result.messages);
391
+ if (nestedErrorSummary) {
392
+ result.exitCode = 1;
393
+ result.stopReason = "error";
394
+ result.errorMessage = nestedErrorSummary;
395
+ if (!result.stderr.trim()) result.stderr = nestedErrorSummary;
396
+ }
397
+ }
398
+
385
399
  return result;
386
400
  } finally {
387
401
  cleanupTempDir(promptTmpDir);
package/types.ts CHANGED
@@ -133,3 +133,37 @@ export function getNestedSubagentResults(messages: Message[]): NestedSubagentRes
133
133
  }
134
134
  return results;
135
135
  }
136
+
137
+ function collectSubagentErrorLinesFromDetails(
138
+ details: SubagentDetails,
139
+ lines: string[],
140
+ prefix = "",
141
+ ): void {
142
+ for (const result of details.results) {
143
+ if (isResultError(result)) {
144
+ const reason = result.errorMessage || result.stderr || result.stopReason || "failed";
145
+ lines.push(`${prefix}${result.agent}: ${reason}`);
146
+ }
147
+ const nested = getNestedSubagentResults(result.messages);
148
+ for (const child of nested) {
149
+ if (child.isError) {
150
+ collectSubagentErrorLinesFromDetails(
151
+ child.details,
152
+ lines,
153
+ `${prefix}${result.agent} -> `,
154
+ );
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ /** Summarize nested subagent failures captured in a message history. */
161
+ export function getNestedSubagentErrorSummary(messages: Message[]): string | null {
162
+ const lines: string[] = [];
163
+ for (const nested of getNestedSubagentResults(messages)) {
164
+ if (!nested.isError) continue;
165
+ collectSubagentErrorLinesFromDetails(nested.details, lines);
166
+ }
167
+ if (lines.length === 0) return null;
168
+ return `Nested subagent failure: ${lines.join("; ")}`;
169
+ }