oira666_pi-subagent 0.1.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/LICENSE +21 -0
- package/README.md +327 -0
- package/agents/code-architect.md +24 -0
- package/agents/code-reviwer.md +23 -0
- package/agents/code-writer.md +18 -0
- package/agents.ts +185 -0
- package/index.ts +865 -0
- package/package.json +68 -0
- package/render.ts +595 -0
- package/runner.ts +419 -0
- package/types.ts +135 -0
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "oira666_pi-subagent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Subagent extension for Pi coding agent. Delegate tasks to specialized agents.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"index.ts",
|
|
9
|
+
"agents.ts",
|
|
10
|
+
"runner.ts",
|
|
11
|
+
"render.ts",
|
|
12
|
+
"types.ts",
|
|
13
|
+
"agents/*.md",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"pi": {
|
|
18
|
+
"extensions": [
|
|
19
|
+
"./index.ts"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"pi",
|
|
24
|
+
"subagent",
|
|
25
|
+
"delegation",
|
|
26
|
+
"pi-package"
|
|
27
|
+
],
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/gee666/pi-subagent.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/gee666/pi-subagent/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/gee666/pi-subagent#readme",
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"license": "MIT",
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^25.2.3",
|
|
42
|
+
"typescript": "^5.9.3"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@mariozechner/pi-agent-core": ">=0.37.0",
|
|
46
|
+
"@mariozechner/pi-ai": ">=0.37.0",
|
|
47
|
+
"@mariozechner/pi-coding-agent": ">=0.37.0",
|
|
48
|
+
"@mariozechner/pi-tui": ">=0.37.0",
|
|
49
|
+
"@sinclair/typebox": ">=0.34.0"
|
|
50
|
+
},
|
|
51
|
+
"peerDependenciesMeta": {
|
|
52
|
+
"@mariozechner/pi-agent-core": {
|
|
53
|
+
"optional": true
|
|
54
|
+
},
|
|
55
|
+
"@mariozechner/pi-coding-agent": {
|
|
56
|
+
"optional": true
|
|
57
|
+
},
|
|
58
|
+
"@mariozechner/pi-tui": {
|
|
59
|
+
"optional": true
|
|
60
|
+
},
|
|
61
|
+
"@mariozechner/pi-ai": {
|
|
62
|
+
"optional": true
|
|
63
|
+
},
|
|
64
|
+
"@sinclair/typebox": {
|
|
65
|
+
"optional": true
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
package/render.ts
ADDED
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI rendering for subagent tool calls and results.
|
|
3
|
+
*/
|
|
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";
|
|
8
|
+
import {
|
|
9
|
+
type DelegationMode,
|
|
10
|
+
type DisplayItem,
|
|
11
|
+
type NestedSubagentResult,
|
|
12
|
+
type SingleResult,
|
|
13
|
+
type SubagentDetails,
|
|
14
|
+
type UsageStats,
|
|
15
|
+
DEFAULT_DELEGATION_MODE,
|
|
16
|
+
aggregateUsage,
|
|
17
|
+
getDisplayItems,
|
|
18
|
+
getFinalOutput,
|
|
19
|
+
getNestedSubagentResults,
|
|
20
|
+
isResultError,
|
|
21
|
+
} from "./types.js";
|
|
22
|
+
|
|
23
|
+
const COLLAPSED_LINE_COUNT = 10;
|
|
24
|
+
const COLLAPSED_PARALLEL_LINE_COUNT = 5;
|
|
25
|
+
const NESTED_PREVIEW_LINE_COUNT = 6;
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Formatting helpers
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
function formatTokens(count: number): string {
|
|
32
|
+
if (count < 1000) return count.toString();
|
|
33
|
+
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
34
|
+
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
|
35
|
+
return `${(count / 1000000).toFixed(1)}M`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function formatUsage(usage: Partial<UsageStats>, model?: string): string {
|
|
39
|
+
const parts: string[] = [];
|
|
40
|
+
const totalTokens =
|
|
41
|
+
(usage.input || 0) +
|
|
42
|
+
(usage.output || 0) +
|
|
43
|
+
(usage.cacheRead || 0) +
|
|
44
|
+
(usage.cacheWrite || 0);
|
|
45
|
+
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
46
|
+
if (totalTokens > 0) parts.push(`tokens:${formatTokens(totalTokens)}`);
|
|
47
|
+
if (usage.input) parts.push(`in:${formatTokens(usage.input)}`);
|
|
48
|
+
if (usage.output) parts.push(`out:${formatTokens(usage.output)}`);
|
|
49
|
+
if (usage.cacheRead) parts.push(`cacheR:${formatTokens(usage.cacheRead)}`);
|
|
50
|
+
if (usage.cacheWrite) parts.push(`cacheW:${formatTokens(usage.cacheWrite)}`);
|
|
51
|
+
if (usage.cost) parts.push(`cost:$${usage.cost.toFixed(4)}`);
|
|
52
|
+
if (usage.contextTokens && usage.contextTokens > 0) parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
53
|
+
if (model) parts.push(model);
|
|
54
|
+
return parts.join(" ");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function truncate(text: string, maxLen: number): string {
|
|
58
|
+
return text.length > maxLen ? `${text.slice(0, maxLen)}...` : text;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function shortenPath(p: string): string {
|
|
62
|
+
const home = os.homedir();
|
|
63
|
+
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeDelegationMode(raw: unknown): DelegationMode {
|
|
67
|
+
return raw === "fork" ? "fork" : DEFAULT_DELEGATION_MODE;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type ThemeFg = (color: string, text: string) => string;
|
|
71
|
+
|
|
72
|
+
function firstNonEmptyLine(text: string): string {
|
|
73
|
+
for (const line of splitOutputLines(text)) {
|
|
74
|
+
if (line.trim()) return line.trim();
|
|
75
|
+
}
|
|
76
|
+
return "";
|
|
77
|
+
}
|
|
78
|
+
|
|
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)}`);
|
|
126
|
+
default:
|
|
127
|
+
return fg("accent", toolName) + fg("dim", ` ${truncate(JSON.stringify(args), 50)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Shared rendering building blocks
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
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;
|
|
139
|
+
}
|
|
140
|
+
|
|
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;
|
|
147
|
+
}
|
|
148
|
+
|
|
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)));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
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
|
+
}
|
|
175
|
+
|
|
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", "✓");
|
|
179
|
+
}
|
|
180
|
+
|
|
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 };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function renderNestedDelegationLines(
|
|
198
|
+
items: NestedSubagentResult[],
|
|
199
|
+
theme: { fg: ThemeFg },
|
|
200
|
+
expanded: boolean,
|
|
201
|
+
depth = 0,
|
|
202
|
+
): string[] {
|
|
203
|
+
const lines: string[] = [];
|
|
204
|
+
for (const item of items) {
|
|
205
|
+
lines.push(...renderNestedDetailsLines(item.details, theme, expanded, depth));
|
|
206
|
+
}
|
|
207
|
+
return lines;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function renderNestedDetailsLines(
|
|
211
|
+
details: SubagentDetails,
|
|
212
|
+
theme: { fg: ThemeFg },
|
|
213
|
+
expanded: boolean,
|
|
214
|
+
depth = 0,
|
|
215
|
+
): 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
|
+
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
|
+
|
|
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)}`;
|
|
252
|
+
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))}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return lines;
|
|
267
|
+
}
|
|
268
|
+
|
|
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");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// renderCall — shown while the tool is being invoked
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
export function renderCall(args: Record<string, any>, theme: { fg: ThemeFg; bold: (s: string) => string }): Text {
|
|
290
|
+
const delegationMode = normalizeDelegationMode(args.mode);
|
|
291
|
+
const modeBadge = theme.fg("muted", ` [${delegationMode}]`);
|
|
292
|
+
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);
|
|
304
|
+
}
|
|
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)}`;
|
|
314
|
+
return new Text(text, 0, 0);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
// renderResult — shown after the tool completes
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
|
|
321
|
+
export function renderResult(
|
|
322
|
+
result: { content: Array<{ type: string; text?: string }>; details?: unknown },
|
|
323
|
+
expanded: boolean,
|
|
324
|
+
theme: { fg: ThemeFg; bold: (s: string) => string },
|
|
325
|
+
): Container | Text {
|
|
326
|
+
const details = result.details as SubagentDetails | undefined;
|
|
327
|
+
if (!details || details.results.length === 0) {
|
|
328
|
+
const first = result.content[0];
|
|
329
|
+
return new Text(first?.type === "text" && first.text ? first.text : "(no output)", 0, 0);
|
|
330
|
+
}
|
|
331
|
+
|
|
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
|
|
480
|
+
? 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
|
+
}
|
|
502
|
+
|
|
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
|
+
const container = new Container();
|
|
512
|
+
container.addChild(
|
|
513
|
+
new Text(
|
|
514
|
+
`${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}${theme.fg("muted", ` [${delegationMode}]`)}`,
|
|
515
|
+
0,
|
|
516
|
+
0,
|
|
517
|
+
),
|
|
518
|
+
);
|
|
519
|
+
|
|
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
|
+
}
|
|
555
|
+
|
|
556
|
+
return container;
|
|
557
|
+
}
|
|
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
|
+
}
|