@bacnh85/pi-subagent 0.10.1 → 0.12.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.
@@ -15,55 +15,55 @@ import type { Model } from "@earendil-works/pi-ai";
15
15
  import type { ModelRegistry } from "@earendil-works/pi-coding-agent";
16
16
 
17
17
  export interface ResolvedModel {
18
- model: Model<any> | null;
19
- attempted: string[];
20
- /** The raw candidate name that matched, if a candidate resolved. Undefined for parent fallback. */
21
- matchedCandidate?: string;
18
+ model: Model<any> | null;
19
+ attempted: string[];
20
+ /** The raw candidate name that matched, if a candidate resolved. Undefined for parent fallback. */
21
+ matchedCandidate?: string;
22
22
  }
23
23
 
24
24
  /** Known provider prefixes for unqualified model names. */
25
25
  const KNOWN_PROVIDERS: [string, RegExp][] = [
26
- ["openai", /^gpt-/i],
27
- ["anthropic", /^claude-/i],
28
- ["google", /^gemini-/i],
29
- ["cohere", /^command-/i],
30
- ["deepseek", /^(deepseek-|ds-)/i],
31
- ["mistral", /^mistral-/i],
32
- ["groq", /^(groq-|llama-)/i],
26
+ ["openai", /^gpt-/i],
27
+ ["anthropic", /^claude-/i],
28
+ ["google", /^gemini-/i],
29
+ ["cohere", /^command-/i],
30
+ ["deepseek", /^(deepseek-|ds-)/i],
31
+ ["mistral", /^mistral-/i],
32
+ ["groq", /^(groq-|llama-)/i],
33
33
  ];
34
34
 
35
35
  export async function resolveModel(
36
- modelNames: readonly string[],
37
- parentModel: Model<any> | undefined,
38
- modelRegistry?: ModelRegistry,
36
+ modelNames: readonly string[],
37
+ parentModel: Model<any> | undefined,
38
+ modelRegistry?: ModelRegistry,
39
39
  ): Promise<ResolvedModel> {
40
- const attempted: string[] = [];
41
- const available = modelRegistry?.getAvailable() ?? [];
42
- const byName = new Map(available.map((model) => [`${model.provider}/${model.id}`, model]));
43
- const tryAvailable = (qualifiedName: string): Model<any> | undefined => {
44
- if (!attempted.includes(qualifiedName)) attempted.push(qualifiedName);
45
- return byName.get(qualifiedName);
46
- };
40
+ const attempted: string[] = [];
41
+ const available = modelRegistry?.getAvailable() ?? [];
42
+ const byName = new Map(available.map((model) => [`${model.provider}/${model.id}`, model]));
43
+ const tryAvailable = (qualifiedName: string): Model<any> | undefined => {
44
+ if (!attempted.includes(qualifiedName)) attempted.push(qualifiedName);
45
+ return byName.get(qualifiedName);
46
+ };
47
47
 
48
- for (const modelName of [...new Set(modelNames.map((name) => name.trim()).filter(Boolean))]) {
49
- const idx = modelName.indexOf("/");
50
- if (idx > 0) {
51
- const found = tryAvailable(modelName);
52
- if (found) return { model: found, attempted, matchedCandidate: modelName };
53
- continue;
54
- }
55
- for (const [provider, pattern] of KNOWN_PROVIDERS) {
56
- if (!pattern.test(modelName)) continue;
57
- const found = tryAvailable(`${provider}/${modelName}`);
58
- if (found) return { model: found, attempted, matchedCandidate: modelName };
59
- }
60
- const found = tryAvailable(`anthropic/${modelName}`);
61
- if (found) return { model: found, attempted, matchedCandidate: modelName };
62
- }
48
+ for (const modelName of [...new Set(modelNames.map((name) => name.trim()).filter(Boolean))]) {
49
+ const idx = modelName.indexOf("/");
50
+ if (idx > 0) {
51
+ const found = tryAvailable(modelName);
52
+ if (found) return { model: found, attempted, matchedCandidate: modelName };
53
+ continue;
54
+ }
55
+ for (const [provider, pattern] of KNOWN_PROVIDERS) {
56
+ if (!pattern.test(modelName)) continue;
57
+ const found = tryAvailable(`${provider}/${modelName}`);
58
+ if (found) return { model: found, attempted, matchedCandidate: modelName };
59
+ }
60
+ const found = tryAvailable(`anthropic/${modelName}`);
61
+ if (found) return { model: found, attempted, matchedCandidate: modelName };
62
+ }
63
63
 
64
- if (parentModel) {
65
- const found = tryAvailable(`${parentModel.provider}/${parentModel.id}`);
66
- if (found) return { model: found, attempted };
67
- }
68
- return { model: null, attempted };
64
+ if (parentModel) {
65
+ const found = tryAvailable(`${parentModel.provider}/${parentModel.id}`);
66
+ if (found) return { model: found, attempted };
67
+ }
68
+ return { model: null, attempted };
69
69
  }
@@ -17,17 +17,17 @@ import { type SubAgentResult, isFailedResult, getResultOutput } from "./runner.t
17
17
  // ---------------------------------------------------------------------------
18
18
 
19
19
  export function asString(value: unknown, fallback = "..."): string {
20
- return typeof value === "string" ? value : fallback;
20
+ return typeof value === "string" ? value : fallback;
21
21
  }
22
22
 
23
23
  export function asNumber(value: unknown): number | undefined {
24
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
24
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
25
25
  }
26
26
 
27
27
  export function asRecord(value: unknown): Record<string, unknown> {
28
- return value && typeof value === "object" && !Array.isArray(value)
29
- ? (value as Record<string, unknown>)
30
- : {};
28
+ return value && typeof value === "object" && !Array.isArray(value)
29
+ ? (value as Record<string, unknown>)
30
+ : {};
31
31
  }
32
32
 
33
33
  // ---------------------------------------------------------------------------
@@ -35,123 +35,123 @@ export function asRecord(value: unknown): Record<string, unknown> {
35
35
  // ---------------------------------------------------------------------------
36
36
 
37
37
  function formatTokens(count: number): string {
38
- if (count < 1000) return count.toString();
39
- if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
40
- if (count < 1000000) return `${Math.round(count / 1000)}k`;
41
- return `${(count / 1000000).toFixed(1)}M`;
38
+ if (count < 1000) return count.toString();
39
+ if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
40
+ if (count < 1000000) return `${Math.round(count / 1000)}k`;
41
+ return `${(count / 1000000).toFixed(1)}M`;
42
42
  }
43
43
 
44
44
  export function formatUsageStats(
45
- usage: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number; contextTokens?: number; turns?: number },
46
- model?: string,
45
+ usage: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number; contextTokens?: number; turns?: number },
46
+ model?: string,
47
47
  ): string {
48
- const parts: string[] = [];
49
- if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
50
- if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
51
- if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
52
- if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
53
- if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
54
- if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
55
- if (usage.contextTokens && usage.contextTokens > 0) {
56
- parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
57
- }
58
- if (model) parts.push(model);
59
- return parts.join(" ");
48
+ const parts: string[] = [];
49
+ if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
50
+ if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
51
+ if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
52
+ if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
53
+ if (usage.cacheWrite) parts.push(`W${formatTokens(usage.cacheWrite)}`);
54
+ if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
55
+ if (usage.contextTokens && usage.contextTokens > 0) {
56
+ parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
57
+ }
58
+ if (model) parts.push(model);
59
+ return parts.join(" ");
60
60
  }
61
61
 
62
62
  export function formatToolCall(
63
- toolName: string,
64
- args: Record<string, unknown>,
65
- themeFg: (color: string, text: string) => string,
63
+ toolName: string,
64
+ args: Record<string, unknown>,
65
+ themeFg: (color: string, text: string) => string,
66
66
  ): string {
67
- const shortenPath = (p: string) => {
68
- const home = os.homedir();
69
- return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
70
- };
67
+ const shortenPath = (p: string) => {
68
+ const home = os.homedir();
69
+ return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
70
+ };
71
71
 
72
- switch (toolName) {
73
- case "bash": {
74
- const command = asString(args.command);
75
- const preview = command.length > 60 ? `${command.slice(0, 60)}...` : command;
76
- return themeFg("muted", "$ ") + themeFg("toolOutput", preview);
77
- }
78
- case "read": {
79
- const rawPath = asString(args.file_path ?? args.path);
80
- const filePath = shortenPath(rawPath);
81
- const offset = asNumber(args.offset);
82
- const limit = asNumber(args.limit);
83
- let text = themeFg("accent", filePath);
84
- if (offset !== undefined || limit !== undefined) {
85
- const startLine = offset ?? 1;
86
- const endLine = limit !== undefined ? startLine + limit - 1 : "";
87
- text += themeFg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
88
- }
89
- return themeFg("muted", "read ") + text;
90
- }
91
- case "write": {
92
- const rawPath = asString(args.file_path ?? args.path);
93
- const content = asString(args.content, "");
94
- const lines = content.split("\n").length;
95
- let text = themeFg("muted", "write ") + themeFg("accent", shortenPath(rawPath));
96
- if (lines > 1) text += themeFg("dim", ` (${lines} lines)`);
97
- return text;
98
- }
99
- case "edit": {
100
- const rawPath = asString(args.file_path ?? args.path);
101
- return themeFg("muted", "edit ") + themeFg("accent", shortenPath(rawPath));
102
- }
103
- case "ls": {
104
- const rawPath = asString(args.path, ".");
105
- return themeFg("muted", "ls ") + themeFg("accent", shortenPath(rawPath));
106
- }
107
- case "find": {
108
- const pattern = asString(args.pattern, "*");
109
- const rawPath = asString(args.path, ".");
110
- return (
111
- themeFg("muted", "find ") +
112
- themeFg("accent", pattern) +
113
- themeFg("dim", ` in ${shortenPath(rawPath)}`)
114
- );
115
- }
116
- case "grep": {
117
- const pattern = asString(args.pattern);
118
- const rawPath = asString(args.path, ".");
119
- return (
120
- themeFg("muted", "grep ") +
121
- themeFg("accent", `/${pattern}/`) +
122
- themeFg("dim", ` in ${shortenPath(rawPath)}`)
123
- );
124
- }
125
- default: {
126
- const argsStr = JSON.stringify(args);
127
- const preview = argsStr.length > 50 ? `${argsStr.slice(0, 50)}...` : argsStr;
128
- return themeFg("accent", toolName) + themeFg("dim", ` ${preview}`);
129
- }
130
- }
72
+ switch (toolName) {
73
+ case "bash": {
74
+ const command = asString(args.command);
75
+ const preview = command.length > 60 ? `${command.slice(0, 60)}...` : command;
76
+ return themeFg("muted", "$ ") + themeFg("toolOutput", preview);
77
+ }
78
+ case "read": {
79
+ const rawPath = asString(args.file_path ?? args.path);
80
+ const filePath = shortenPath(rawPath);
81
+ const offset = asNumber(args.offset);
82
+ const limit = asNumber(args.limit);
83
+ let text = themeFg("accent", filePath);
84
+ if (offset !== undefined || limit !== undefined) {
85
+ const startLine = offset ?? 1;
86
+ const endLine = limit !== undefined ? startLine + limit - 1 : "";
87
+ text += themeFg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
88
+ }
89
+ return themeFg("muted", "read ") + text;
90
+ }
91
+ case "write": {
92
+ const rawPath = asString(args.file_path ?? args.path);
93
+ const content = asString(args.content, "");
94
+ const lines = content.split("\n").length;
95
+ let text = themeFg("muted", "write ") + themeFg("accent", shortenPath(rawPath));
96
+ if (lines > 1) text += themeFg("dim", ` (${lines} lines)`);
97
+ return text;
98
+ }
99
+ case "edit": {
100
+ const rawPath = asString(args.file_path ?? args.path);
101
+ return themeFg("muted", "edit ") + themeFg("accent", shortenPath(rawPath));
102
+ }
103
+ case "ls": {
104
+ const rawPath = asString(args.path, ".");
105
+ return themeFg("muted", "ls ") + themeFg("accent", shortenPath(rawPath));
106
+ }
107
+ case "find": {
108
+ const pattern = asString(args.pattern, "*");
109
+ const rawPath = asString(args.path, ".");
110
+ return (
111
+ themeFg("muted", "find ") +
112
+ themeFg("accent", pattern) +
113
+ themeFg("dim", ` in ${shortenPath(rawPath)}`)
114
+ );
115
+ }
116
+ case "grep": {
117
+ const pattern = asString(args.pattern);
118
+ const rawPath = asString(args.path, ".");
119
+ return (
120
+ themeFg("muted", "grep ") +
121
+ themeFg("accent", `/${pattern}/`) +
122
+ themeFg("dim", ` in ${shortenPath(rawPath)}`)
123
+ );
124
+ }
125
+ default: {
126
+ const argsStr = JSON.stringify(args);
127
+ const preview = argsStr.length > 50 ? `${argsStr.slice(0, 50)}...` : argsStr;
128
+ return themeFg("accent", toolName) + themeFg("dim", ` ${preview}`);
129
+ }
130
+ }
131
131
  }
132
132
 
133
133
  export type DisplayItem =
134
- | { type: "text"; text: string }
135
- | { type: "toolCall"; name: string; args: Record<string, unknown> };
134
+ | { type: "text"; text: string }
135
+ | { type: "toolCall"; name: string; args: Record<string, unknown> };
136
136
 
137
137
  export function getDisplayItems(messages: Message[]): DisplayItem[] {
138
- const items: DisplayItem[] = [];
139
- for (const msg of messages) {
140
- if (msg.role === "assistant") {
141
- for (const part of msg.content) {
142
- if (part.type === "text" && part.text.trim()) {
143
- items.push({ type: "text", text: part.text });
144
- } else if (part.type === "toolCall") {
145
- items.push({
146
- type: "toolCall",
147
- name: part.name,
148
- args: asRecord(part.arguments),
149
- });
150
- }
151
- }
152
- }
153
- }
154
- return items;
138
+ const items: DisplayItem[] = [];
139
+ for (const msg of messages) {
140
+ if (msg.role === "assistant") {
141
+ for (const part of msg.content) {
142
+ if (part.type === "text" && part.text.trim()) {
143
+ items.push({ type: "text", text: part.text });
144
+ } else if (part.type === "toolCall") {
145
+ items.push({
146
+ type: "toolCall",
147
+ name: part.name,
148
+ args: asRecord(part.arguments),
149
+ });
150
+ }
151
+ }
152
+ }
153
+ }
154
+ return items;
155
155
  }
156
156
 
157
157
  // ---------------------------------------------------------------------------
@@ -161,23 +161,23 @@ export function getDisplayItems(messages: Message[]): DisplayItem[] {
161
161
  const COLLAPSED_ITEM_COUNT = 10;
162
162
 
163
163
  function renderDisplayItems(
164
- items: DisplayItem[],
165
- theme: { fg: (c: string, t: string) => string },
166
- limit?: number,
164
+ items: DisplayItem[],
165
+ theme: { fg: (c: string, t: string) => string },
166
+ limit?: number,
167
167
  ): string {
168
- const toShow = limit ? items.slice(-limit) : items;
169
- const skipped = limit && items.length > limit ? items.length - limit : 0;
170
- let text = "";
171
- if (skipped > 0) text += theme.fg("muted", `... ${skipped} earlier items\n`);
172
- for (const item of toShow) {
173
- if (item.type === "text") {
174
- const preview = item.text.split("\n").slice(0, 3).join("\n");
175
- text += `${theme.fg("toolOutput", preview)}\n`;
176
- } else {
177
- text += `${theme.fg("muted", "→ ")}${formatToolCall(item.name, item.args, theme.fg.bind(theme))}\n`;
178
- }
179
- }
180
- return text.trimEnd();
168
+ const toShow = limit ? items.slice(-limit) : items;
169
+ const skipped = limit && items.length > limit ? items.length - limit : 0;
170
+ let text = "";
171
+ if (skipped > 0) text += theme.fg("muted", `... ${skipped} earlier items\n`);
172
+ for (const item of toShow) {
173
+ if (item.type === "text") {
174
+ const preview = item.text.split("\n").slice(0, 3).join("\n");
175
+ text += `${theme.fg("toolOutput", preview)}\n`;
176
+ } else {
177
+ text += `${theme.fg("muted", "→ ")}${formatToolCall(item.name, item.args, theme.fg.bind(theme))}\n`;
178
+ }
179
+ }
180
+ return text.trimEnd();
181
181
  }
182
182
 
183
183
  // ---------------------------------------------------------------------------
@@ -185,80 +185,80 @@ function renderDisplayItems(
185
185
  // ---------------------------------------------------------------------------
186
186
 
187
187
  export function renderSingleResult(
188
- result: SubAgentResult,
189
- expanded: boolean,
190
- theme: { fg: (c: any, t: string) => string; bold: (t: string) => string },
191
- agentColor?: string,
188
+ result: SubAgentResult,
189
+ expanded: boolean,
190
+ theme: { fg: (c: any, t: string) => string; bold: (t: string) => string },
191
+ agentColor?: string,
192
192
  ): Container | Text {
193
- const isError = isFailedResult(result);
194
- const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
195
- const displayItems = getDisplayItems(result.messages);
196
- const finalOutput = getResultOutput(result);
193
+ const isError = isFailedResult(result);
194
+ const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
195
+ const displayItems = getDisplayItems(result.messages);
196
+ const finalOutput = getResultOutput(result);
197
197
 
198
- if (expanded) {
199
- const mdTheme = getMarkdownTheme();
200
- const container = new Container();
201
- let header = `${icon} ${theme.fg(agentColor ?? "toolTitle", theme.bold(result.agent))}`;
202
- if (isError && result.stopReason) {
203
- const reasonColor = result.stopReason === "timeout" ? "warning" : "error";
204
- header += ` ${theme.fg(reasonColor, `[${result.stopReason}]`)}`;
205
- }
206
- container.addChild(new Text(header, 0, 0));
207
- if (isError && result.errorMessage) {
208
- const messageColor = result.stopReason === "timeout" ? "warning" : "error";
209
- container.addChild(new Text(theme.fg(messageColor, `Error: ${result.errorMessage}`), 0, 0));
210
- }
211
- container.addChild(new Spacer(1));
212
- container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
213
- container.addChild(new Text(theme.fg("dim", result.task), 0, 0));
214
- container.addChild(new Spacer(1));
215
- container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0));
216
- if (displayItems.length === 0 && !finalOutput) {
217
- container.addChild(new Text(theme.fg("muted", "(no output)"), 0, 0));
218
- } else {
219
- for (const item of displayItems) {
220
- if (item.type === "toolCall") {
221
- container.addChild(
222
- new Text(
223
- theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
224
- 0, 0,
225
- ),
226
- );
227
- }
228
- }
229
- if (finalOutput) {
230
- container.addChild(new Spacer(1));
231
- container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
232
- }
233
- }
234
- const usageStr = formatUsageStats(result.usage, result.model);
235
- if (usageStr) {
236
- container.addChild(new Spacer(1));
237
- container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
238
- }
239
- return container;
240
- }
198
+ if (expanded) {
199
+ const mdTheme = getMarkdownTheme();
200
+ const container = new Container();
201
+ let header = `${icon} ${theme.fg(agentColor ?? "toolTitle", theme.bold(result.agent))}`;
202
+ if (isError && result.stopReason) {
203
+ const reasonColor = result.stopReason === "timeout" ? "warning" : "error";
204
+ header += ` ${theme.fg(reasonColor, `[${result.stopReason}]`)}`;
205
+ }
206
+ container.addChild(new Text(header, 0, 0));
207
+ if (isError && result.errorMessage) {
208
+ const messageColor = result.stopReason === "timeout" ? "warning" : "error";
209
+ container.addChild(new Text(theme.fg(messageColor, `Error: ${result.errorMessage}`), 0, 0));
210
+ }
211
+ container.addChild(new Spacer(1));
212
+ container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
213
+ container.addChild(new Text(theme.fg("dim", result.task), 0, 0));
214
+ container.addChild(new Spacer(1));
215
+ container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0));
216
+ if (displayItems.length === 0 && !finalOutput) {
217
+ container.addChild(new Text(theme.fg("muted", "(no output)"), 0, 0));
218
+ } else {
219
+ for (const item of displayItems) {
220
+ if (item.type === "toolCall") {
221
+ container.addChild(
222
+ new Text(
223
+ theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme)),
224
+ 0, 0,
225
+ ),
226
+ );
227
+ }
228
+ }
229
+ if (finalOutput) {
230
+ container.addChild(new Spacer(1));
231
+ container.addChild(new Markdown(finalOutput.trim(), 0, 0, mdTheme));
232
+ }
233
+ }
234
+ const usageStr = formatUsageStats(result.usage, result.model);
235
+ if (usageStr) {
236
+ container.addChild(new Spacer(1));
237
+ container.addChild(new Text(theme.fg("dim", usageStr), 0, 0));
238
+ }
239
+ return container;
240
+ }
241
241
 
242
- // Collapsed
243
- let text = `${icon} ${theme.fg(agentColor ?? "toolTitle", theme.bold(result.agent))}`;
244
- if (isError && result.stopReason) {
245
- const reasonColor = result.stopReason === "timeout" ? "warning" : "error";
246
- text += ` ${theme.fg(reasonColor, `[${result.stopReason}]`)}`;
247
- }
248
- if (isError && result.errorMessage) {
249
- const messageColor = result.stopReason === "timeout" ? "warning" : "error";
250
- text += `\n${theme.fg(messageColor, `Error: ${result.errorMessage}`)}`;
251
- }
252
- else if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
253
- else {
254
- text += `\n${renderDisplayItems(displayItems, theme, COLLAPSED_ITEM_COUNT)}`;
255
- if (displayItems.length > COLLAPSED_ITEM_COUNT) {
256
- text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
257
- }
258
- }
259
- const usageStr = formatUsageStats(result.usage, result.model);
260
- if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
261
- return new Text(text, 0, 0);
242
+ // Collapsed
243
+ let text = `${icon} ${theme.fg(agentColor ?? "toolTitle", theme.bold(result.agent))}`;
244
+ if (isError && result.stopReason) {
245
+ const reasonColor = result.stopReason === "timeout" ? "warning" : "error";
246
+ text += ` ${theme.fg(reasonColor, `[${result.stopReason}]`)}`;
247
+ }
248
+ if (isError && result.errorMessage) {
249
+ const messageColor = result.stopReason === "timeout" ? "warning" : "error";
250
+ text += `\n${theme.fg(messageColor, `Error: ${result.errorMessage}`)}`;
251
+ }
252
+ else if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
253
+ else {
254
+ text += `\n${renderDisplayItems(displayItems, theme, COLLAPSED_ITEM_COUNT)}`;
255
+ if (displayItems.length > COLLAPSED_ITEM_COUNT) {
256
+ text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
257
+ }
258
+ }
259
+ const usageStr = formatUsageStats(result.usage, result.model);
260
+ if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
261
+ return new Text(text, 0, 0);
262
262
  }
263
263
 
264
264
  // ---------------------------------------------------------------------------
@@ -266,14 +266,14 @@ export function renderSingleResult(
266
266
  // ---------------------------------------------------------------------------
267
267
 
268
268
  export function aggregateUsage(results: SubAgentResult[]) {
269
- const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
270
- for (const r of results) {
271
- total.input += r.usage.input;
272
- total.output += r.usage.output;
273
- total.cacheRead += r.usage.cacheRead;
274
- total.cacheWrite += r.usage.cacheWrite;
275
- total.cost += r.usage.cost;
276
- total.turns += r.usage.turns;
277
- }
278
- return total;
269
+ const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
270
+ for (const r of results) {
271
+ total.input += r.usage.input;
272
+ total.output += r.usage.output;
273
+ total.cacheRead += r.usage.cacheRead;
274
+ total.cacheWrite += r.usage.cacheWrite;
275
+ total.cost += r.usage.cost;
276
+ total.turns += r.usage.turns;
277
+ }
278
+ return total;
279
279
  }