@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.
- package/CHANGELOG.md +20 -0
- package/README.md +11 -9
- package/agent-format.md +4 -2
- package/agents/general-purpose.md +4 -2
- package/agents/planner.md +2 -1
- package/agents/reviewer.md +2 -1
- package/agents/scout.md +3 -3
- package/agents/tester.md +3 -3
- package/agents/worker.md +4 -2
- package/extensions/agents.ts +310 -310
- package/extensions/index.ts +1357 -1357
- package/extensions/model.ts +41 -41
- package/extensions/render.ts +205 -205
- package/extensions/runner.ts +200 -200
- package/extensions/security.ts +313 -313
- package/extensions/service.ts +115 -115
- package/extensions/thread-viewer.ts +246 -246
- package/extensions/threads.ts +99 -99
- package/package.json +9 -9
package/extensions/model.ts
CHANGED
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
modelNames: readonly string[],
|
|
37
|
+
parentModel: Model<any> | undefined,
|
|
38
|
+
modelRegistry?: ModelRegistry,
|
|
39
39
|
): Promise<ResolvedModel> {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
}
|
package/extensions/render.ts
CHANGED
|
@@ -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
|
-
|
|
20
|
+
return typeof value === "string" ? value : fallback;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export function asNumber(value: unknown): number | undefined {
|
|
24
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
46
|
-
|
|
45
|
+
usage: { input: number; output: number; cacheRead: number; cacheWrite: number; cost: number; contextTokens?: number; turns?: number },
|
|
46
|
+
model?: string,
|
|
47
47
|
): string {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
toolName: string,
|
|
64
|
+
args: Record<string, unknown>,
|
|
65
|
+
themeFg: (color: string, text: string) => string,
|
|
66
66
|
): string {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
164
|
+
items: DisplayItem[],
|
|
165
|
+
theme: { fg: (c: string, t: string) => string },
|
|
166
|
+
limit?: number,
|
|
167
167
|
): string {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
}
|