oira666_pi-subagent 0.2.19 → 0.2.21
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 -21
- package/README.md +456 -456
- package/agents/code-architect.md +22 -22
- package/agents/code-reviwer.md +21 -21
- package/agents/code-writer.md +16 -16
- package/agents.ts +188 -188
- package/index.ts +360 -32
- package/package.json +74 -74
- package/render.ts +471 -463
- package/resume.ts +227 -228
- package/runner.ts +1059 -1028
- package/shared.ts +97 -97
- package/types.ts +311 -311
package/render.ts
CHANGED
|
@@ -1,463 +1,471 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TUI rendering for subagent tool calls and results.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import * as os from "node:os";
|
|
6
|
-
import { Container, Spacer, Text } from "@mariozechner/pi-tui";
|
|
7
|
-
import {
|
|
8
|
-
type LiveLogEntry,
|
|
9
|
-
type NestedSubagentResult,
|
|
10
|
-
type SingleResult,
|
|
11
|
-
type SubagentDetails,
|
|
12
|
-
type UsageStats,
|
|
13
|
-
aggregateUsage,
|
|
14
|
-
getDisplayItems,
|
|
15
|
-
getFinalOutput,
|
|
16
|
-
getNestedSubagentResults,
|
|
17
|
-
isResultError,
|
|
18
|
-
isSubagentDetails,
|
|
19
|
-
} from "./types.js";
|
|
20
|
-
|
|
21
|
-
const OUTPUT_PREVIEW_LINE_COUNT = 6;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (usage.
|
|
75
|
-
if (
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
return
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
const
|
|
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
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
case "
|
|
252
|
-
|
|
253
|
-
return
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
case "
|
|
258
|
-
return truncateTo((args.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
" " +
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
container.addChild(
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
1
|
+
/**
|
|
2
|
+
* TUI rendering for subagent tool calls and results.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as os from "node:os";
|
|
6
|
+
import { Container, Spacer, Text } from "@mariozechner/pi-tui";
|
|
7
|
+
import {
|
|
8
|
+
type LiveLogEntry,
|
|
9
|
+
type NestedSubagentResult,
|
|
10
|
+
type SingleResult,
|
|
11
|
+
type SubagentDetails,
|
|
12
|
+
type UsageStats,
|
|
13
|
+
aggregateUsage,
|
|
14
|
+
getDisplayItems,
|
|
15
|
+
getFinalOutput,
|
|
16
|
+
getNestedSubagentResults,
|
|
17
|
+
isResultError,
|
|
18
|
+
isSubagentDetails,
|
|
19
|
+
} from "./types.js";
|
|
20
|
+
|
|
21
|
+
const OUTPUT_PREVIEW_LINE_COUNT = 6;
|
|
22
|
+
|
|
23
|
+
let broadcastNumberingActive = false;
|
|
24
|
+
|
|
25
|
+
export function setBroadcastNumberingActive(active: boolean): void {
|
|
26
|
+
broadcastNumberingActive = active;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type ThemeFg = (color: string, text: string) => string;
|
|
30
|
+
type NodeStatus = "running" | "success" | "error";
|
|
31
|
+
|
|
32
|
+
interface TreeNode {
|
|
33
|
+
label: string;
|
|
34
|
+
status: NodeStatus;
|
|
35
|
+
meta?: string;
|
|
36
|
+
task?: string;
|
|
37
|
+
outputPreview?: string[];
|
|
38
|
+
liveActivity?: LiveLogEntry[];
|
|
39
|
+
children: TreeNode[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
interface TreeCounts {
|
|
44
|
+
total: number;
|
|
45
|
+
running: number;
|
|
46
|
+
success: number;
|
|
47
|
+
error: number;
|
|
48
|
+
finished: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface PendingSubagentCall {
|
|
52
|
+
toolCallId: string;
|
|
53
|
+
tasks: Array<{ agent: string; task?: string }>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Formatting helpers
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
function formatTokens(count: number): string {
|
|
61
|
+
if (count < 1000) return count.toString();
|
|
62
|
+
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
63
|
+
if (count < 1000000) return `${Math.round(count / 1000)}k`;
|
|
64
|
+
return `${(count / 1000000).toFixed(1)}M`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function formatUsage(usage: Partial<UsageStats>, model?: string): string {
|
|
68
|
+
const parts: string[] = [];
|
|
69
|
+
const totalTokens =
|
|
70
|
+
(usage.input || 0) +
|
|
71
|
+
(usage.output || 0) +
|
|
72
|
+
(usage.cacheRead || 0) +
|
|
73
|
+
(usage.cacheWrite || 0);
|
|
74
|
+
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
75
|
+
if (totalTokens > 0) parts.push(`tok:${formatTokens(totalTokens)}`);
|
|
76
|
+
if (usage.input) parts.push(`in:${formatTokens(usage.input)}`);
|
|
77
|
+
if (usage.output) parts.push(`out:${formatTokens(usage.output)}`);
|
|
78
|
+
if (usage.cacheRead) parts.push(`cacheR:${formatTokens(usage.cacheRead)}`);
|
|
79
|
+
if (usage.cacheWrite) parts.push(`cacheW:${formatTokens(usage.cacheWrite)}`);
|
|
80
|
+
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
81
|
+
if (usage.contextTokens && usage.contextTokens > 0) parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
82
|
+
if (model) parts.push(model);
|
|
83
|
+
return parts.join(" • ");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function truncate(text: string, maxLen: number): string {
|
|
87
|
+
return text.length > maxLen ? `${text.slice(0, maxLen)}...` : text;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function splitOutputLines(text: string): string[] {
|
|
91
|
+
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
92
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
93
|
+
return lines;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function lastNonEmptyLines(text: string, limit: number): string[] {
|
|
97
|
+
return splitOutputLines(text)
|
|
98
|
+
.map((line) => line.trimEnd())
|
|
99
|
+
.filter((line) => line.trim().length > 0)
|
|
100
|
+
.slice(-limit);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function statusEmoji(status: NodeStatus, theme: { fg: ThemeFg }): string {
|
|
104
|
+
switch (status) {
|
|
105
|
+
case "running":
|
|
106
|
+
return theme.fg("warning", "⏳");
|
|
107
|
+
case "error":
|
|
108
|
+
return theme.fg("error", "❌");
|
|
109
|
+
default:
|
|
110
|
+
return theme.fg("success", "✅");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function statusFromResult(result: SingleResult): NodeStatus {
|
|
115
|
+
if (result.exitCode === -1) return "running";
|
|
116
|
+
return isResultError(result) ? "error" : "success";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function countNodes(nodes: TreeNode[]): TreeCounts {
|
|
120
|
+
const counts: TreeCounts = {
|
|
121
|
+
total: 0,
|
|
122
|
+
running: 0,
|
|
123
|
+
success: 0,
|
|
124
|
+
error: 0,
|
|
125
|
+
finished: 0,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const visit = (node: TreeNode) => {
|
|
129
|
+
counts.total++;
|
|
130
|
+
if (node.status === "running") counts.running++;
|
|
131
|
+
if (node.status === "success") counts.success++;
|
|
132
|
+
if (node.status === "error") counts.error++;
|
|
133
|
+
if (node.status !== "running") counts.finished++;
|
|
134
|
+
for (const child of node.children) visit(child);
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
for (const node of nodes) visit(node);
|
|
138
|
+
return counts;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function hasNestedChildren(nodes: TreeNode[]): boolean {
|
|
142
|
+
return nodes.some((node) => node.children.length > 0 || hasNestedChildren(node.children));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function extractPendingSubagentCalls(messages: SingleResult["messages"]): PendingSubagentCall[] {
|
|
146
|
+
const calls: PendingSubagentCall[] = [];
|
|
147
|
+
for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {
|
|
148
|
+
const message = messages[messageIndex] as any;
|
|
149
|
+
if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
150
|
+
for (let partIndex = 0; partIndex < message.content.length; partIndex++) {
|
|
151
|
+
const part = message.content[partIndex] as any;
|
|
152
|
+
if (part?.type !== "toolCall" || part?.name !== "subagent") continue;
|
|
153
|
+
const args = part.arguments && typeof part.arguments === "object" ? part.arguments : {};
|
|
154
|
+
const tasks = Array.isArray((args as any).tasks)
|
|
155
|
+
? (args as any).tasks
|
|
156
|
+
.filter((task: any) => task && typeof task.agent === "string")
|
|
157
|
+
.map((task: any) => ({
|
|
158
|
+
agent: task.agent,
|
|
159
|
+
task: typeof task.task === "string" ? task.task : undefined,
|
|
160
|
+
}))
|
|
161
|
+
: [];
|
|
162
|
+
calls.push({
|
|
163
|
+
toolCallId:
|
|
164
|
+
typeof part.toolCallId === "string"
|
|
165
|
+
? part.toolCallId
|
|
166
|
+
: typeof part.id === "string"
|
|
167
|
+
? part.id
|
|
168
|
+
: `${messageIndex}:${partIndex}`,
|
|
169
|
+
tasks,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return calls;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function buildPendingNodes(call: PendingSubagentCall): TreeNode[] {
|
|
177
|
+
return call.tasks.map((task) => ({
|
|
178
|
+
label: task.agent,
|
|
179
|
+
status: "running",
|
|
180
|
+
task: task.task,
|
|
181
|
+
children: [],
|
|
182
|
+
}));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function buildNodesFromNestedResult(nested: NestedSubagentResult): TreeNode[] {
|
|
186
|
+
return nested.details.results.map((result) => buildResultNode(result));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function subagentCallSignature(call: PendingSubagentCall): string {
|
|
190
|
+
return JSON.stringify(call.tasks.map((task) => ({ agent: task.agent, task: task.task ?? "" })));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function nestedResultIsHealthy(nested: NestedSubagentResult | undefined): boolean {
|
|
194
|
+
if (!nested || nested.isError) return false;
|
|
195
|
+
return nested.details.results.every((result) => !isResultError(result));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function buildNestedChildren(result: SingleResult): TreeNode[] {
|
|
199
|
+
const parentIsRunning = result.exitCode === -1;
|
|
200
|
+
const completedByToolCallId = new Map<string, NestedSubagentResult>();
|
|
201
|
+
for (const nested of getNestedSubagentResults(result.messages)) {
|
|
202
|
+
completedByToolCallId.set(nested.toolCallId, nested);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const calls = extractPendingSubagentCalls(result.messages);
|
|
206
|
+
const laterResumeBySignature = new Map<string, number>();
|
|
207
|
+
calls.forEach((call, index) => {
|
|
208
|
+
const completed = completedByToolCallId.get(call.toolCallId);
|
|
209
|
+
// A resumed call has the same task signature as the interrupted call but a
|
|
210
|
+
// newer toolCallId. Prefer that newer running/successful tree over the old
|
|
211
|
+
// synthetic/aborted result so resumed nested subagents render in-place.
|
|
212
|
+
if (!completed || nestedResultIsHealthy(completed)) {
|
|
213
|
+
laterResumeBySignature.set(subagentCallSignature(call), index);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const nodes: TreeNode[] = [];
|
|
218
|
+
calls.forEach((call, index) => {
|
|
219
|
+
const completed = completedByToolCallId.get(call.toolCallId);
|
|
220
|
+
const newerEquivalent = laterResumeBySignature.get(subagentCallSignature(call));
|
|
221
|
+
if (
|
|
222
|
+
newerEquivalent !== undefined &&
|
|
223
|
+
newerEquivalent > index &&
|
|
224
|
+
(!completed || completed.isError || !nestedResultIsHealthy(completed))
|
|
225
|
+
) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (completed && isSubagentDetails(completed.details)) {
|
|
230
|
+
nodes.push(...buildNodesFromNestedResult(completed));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
// Unmatched subagent tool calls are useful while the parent is still
|
|
234
|
+
// running (they show live pending children). Once the parent finished,
|
|
235
|
+
// unmatched calls are stale history from an interrupted/resumed session and
|
|
236
|
+
// must not keep the whole tree in a perpetual "running" state.
|
|
237
|
+
if (parentIsRunning) nodes.push(...buildPendingNodes(call));
|
|
238
|
+
});
|
|
239
|
+
return nodes;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function formatToolArgPreview(toolName: string, args: Record<string, unknown>): string {
|
|
243
|
+
const shorten = (p: string) => {
|
|
244
|
+
const home = os.homedir();
|
|
245
|
+
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
246
|
+
};
|
|
247
|
+
const truncateTo = (s: string, n: number) =>
|
|
248
|
+
s.length > n ? s.slice(0, n) + "\u2026" : s;
|
|
249
|
+
|
|
250
|
+
switch (toolName) {
|
|
251
|
+
case "bash": {
|
|
252
|
+
const cmd = (args.command as string) || "";
|
|
253
|
+
return truncateTo(cmd.replace(/\s+/g, " "), 52);
|
|
254
|
+
}
|
|
255
|
+
case "read":
|
|
256
|
+
case "write":
|
|
257
|
+
case "edit":
|
|
258
|
+
return shorten(truncateTo((args.path ?? args.file_path ?? "") as string, 52));
|
|
259
|
+
case "grep":
|
|
260
|
+
return truncateTo(`/${args.pattern}/`, 30) +
|
|
261
|
+
(args.path ? ` in ${shorten(args.path as string)}` : "");
|
|
262
|
+
case "find":
|
|
263
|
+
return truncateTo((args.pattern ?? "*") as string, 30) +
|
|
264
|
+
(args.path ? ` in ${shorten(args.path as string)}` : "");
|
|
265
|
+
case "subagent": {
|
|
266
|
+
const tasks = (args.tasks as any[]) ?? [];
|
|
267
|
+
return tasks.map((t: any) => t.agent).join(", ");
|
|
268
|
+
}
|
|
269
|
+
default:
|
|
270
|
+
return "";
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function formatLiveLogEntry(
|
|
275
|
+
entry: LiveLogEntry,
|
|
276
|
+
theme: { fg: ThemeFg },
|
|
277
|
+
): string {
|
|
278
|
+
switch (entry.kind) {
|
|
279
|
+
case "turn_start":
|
|
280
|
+
return theme.fg("muted", "\u27f3") + " " + theme.fg("dim", "thinking\u2026");
|
|
281
|
+
|
|
282
|
+
case "turn_end": {
|
|
283
|
+
const tokens = entry.inputTokens || entry.outputTokens
|
|
284
|
+
? " " + theme.fg("dim",
|
|
285
|
+
`\u2191${formatTokens(entry.inputTokens)} \u2193${formatTokens(entry.outputTokens)}`)
|
|
286
|
+
: "";
|
|
287
|
+
return (
|
|
288
|
+
theme.fg("success", "\u2713") +
|
|
289
|
+
" " +
|
|
290
|
+
theme.fg("muted", `turn ${entry.turn}`) +
|
|
291
|
+
tokens
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
case "tool_start": {
|
|
296
|
+
const argPreview = formatToolArgPreview(entry.toolName, entry.args);
|
|
297
|
+
return (
|
|
298
|
+
theme.fg("muted", "\u2192") +
|
|
299
|
+
" " +
|
|
300
|
+
theme.fg("accent", entry.toolName) +
|
|
301
|
+
(argPreview ? " " + theme.fg("dim", argPreview) : "")
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
case "tool_end":
|
|
306
|
+
return (
|
|
307
|
+
theme.fg("success", "\u2713") +
|
|
308
|
+
" " +
|
|
309
|
+
theme.fg("accent", entry.toolName)
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function buildLeafPreview(result: SingleResult): string[] | undefined {
|
|
315
|
+
const items = getDisplayItems(result.messages);
|
|
316
|
+
const lines: string[] = [];
|
|
317
|
+
for (const item of items) {
|
|
318
|
+
if (item.type === "text") {
|
|
319
|
+
lines.push(...lastNonEmptyLines(item.text, OUTPUT_PREVIEW_LINE_COUNT));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const finalOutput = getFinalOutput(result.messages);
|
|
323
|
+
if (finalOutput) lines.push(...lastNonEmptyLines(finalOutput, OUTPUT_PREVIEW_LINE_COUNT));
|
|
324
|
+
const unique = lines.filter((line, index) => line && lines.indexOf(line) === index);
|
|
325
|
+
return unique.length > 0 ? unique.slice(-OUTPUT_PREVIEW_LINE_COUNT) : undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function buildResultNode(result: SingleResult): TreeNode {
|
|
329
|
+
const status = statusFromResult(result);
|
|
330
|
+
const usage = formatUsage(result.usage, result.model);
|
|
331
|
+
const metaParts: string[] = [result.agentSource];
|
|
332
|
+
if (usage) metaParts.push(usage);
|
|
333
|
+
if (status === "error") {
|
|
334
|
+
const errorText = result.errorMessage || result.stderr || result.stopReason;
|
|
335
|
+
if (errorText) metaParts.push(truncate(errorText.replace(/\s+/g, " "), 120));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const children = buildNestedChildren(result);
|
|
339
|
+
const isRunning = status === "running";
|
|
340
|
+
return {
|
|
341
|
+
label: result.agent,
|
|
342
|
+
status,
|
|
343
|
+
meta: metaParts.join(" • "),
|
|
344
|
+
task: result.task,
|
|
345
|
+
liveActivity: isRunning && result.liveLog?.length > 0 ? result.liveLog : undefined,
|
|
346
|
+
outputPreview: !isRunning && children.length === 0 ? buildLeafPreview(result) : undefined,
|
|
347
|
+
children,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function buildTopLevelNodes(details: SubagentDetails): TreeNode[] {
|
|
352
|
+
return details.results.map((result) => buildResultNode(result));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function renderTreeLines(
|
|
356
|
+
nodes: TreeNode[],
|
|
357
|
+
theme: { fg: ThemeFg },
|
|
358
|
+
showOutputPreview: boolean,
|
|
359
|
+
depth = 0,
|
|
360
|
+
prefix = "",
|
|
361
|
+
): string[] {
|
|
362
|
+
const lines: string[] = [];
|
|
363
|
+
|
|
364
|
+
nodes.forEach((node, index) => {
|
|
365
|
+
const indent = " ".repeat(depth);
|
|
366
|
+
const number = prefix ? `${prefix}.${index + 1}` : `${index + 1}`;
|
|
367
|
+
const numberPrefix = broadcastNumberingActive ? `${number}. ` : "";
|
|
368
|
+
let line = `${indent}${numberPrefix}${statusEmoji(node.status, theme)} ${theme.fg("accent", node.label)}`;
|
|
369
|
+
if (node.meta) line += ` ${theme.fg("dim", node.meta)}`;
|
|
370
|
+
lines.push(line);
|
|
371
|
+
|
|
372
|
+
if (showOutputPreview && node.outputPreview && node.outputPreview.length > 0) {
|
|
373
|
+
for (const outputLine of node.outputPreview) {
|
|
374
|
+
lines.push(`${indent} ${theme.fg("toolOutput", outputLine)}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (showOutputPreview && node.liveActivity && node.liveActivity.length > 0) {
|
|
379
|
+
for (const entry of node.liveActivity) {
|
|
380
|
+
lines.push(`${indent} ${formatLiveLogEntry(entry, theme)}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (node.children.length > 0) {
|
|
385
|
+
lines.push(...renderTreeLines(node.children, theme, false, depth + 1, number));
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
return lines;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function topLevelSummary(details: SubagentDetails, counts: TreeCounts): string {
|
|
393
|
+
// aggregatedUsage includes own agents + all their nested descendants;
|
|
394
|
+
// fall back to summing only direct results for old serialised data lacking the field.
|
|
395
|
+
const totalUsage = formatUsage(
|
|
396
|
+
details.aggregatedUsage ?? aggregateUsage(details.results),
|
|
397
|
+
);
|
|
398
|
+
const parts = [
|
|
399
|
+
`${counts.running} running`,
|
|
400
|
+
`${counts.finished}/${counts.total} finished`,
|
|
401
|
+
`${counts.success} ok`,
|
|
402
|
+
`${counts.error} error`,
|
|
403
|
+
];
|
|
404
|
+
if (totalUsage) parts.push(totalUsage);
|
|
405
|
+
return parts.join(" • ");
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
// renderCall — shown while the tool is being invoked
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
export function renderCall(
|
|
413
|
+
args: Record<string, any>,
|
|
414
|
+
theme: { fg: ThemeFg; bold: (s: string) => string },
|
|
415
|
+
context?: { isPartial?: boolean; isError?: boolean },
|
|
416
|
+
): Text {
|
|
417
|
+
const tasks = Array.isArray(args.tasks) ? args.tasks : [];
|
|
418
|
+
const count = tasks.length;
|
|
419
|
+
const icon = context?.isPartial === false
|
|
420
|
+
? context.isError
|
|
421
|
+
? theme.fg("error", "❌")
|
|
422
|
+
: theme.fg("success", "✅")
|
|
423
|
+
: theme.fg("warning", "⏳");
|
|
424
|
+
let text = `${theme.fg("toolTitle", theme.bold("subagent "))}${theme.fg("accent", `${count} task${count === 1 ? "" : "s"}`)}`;
|
|
425
|
+
for (const task of tasks.slice(0, 6)) {
|
|
426
|
+
const agent = typeof task?.agent === "string" ? task.agent : "...";
|
|
427
|
+
const preview = typeof task?.task === "string" ? ` ${truncate(task.task, 56)}` : "";
|
|
428
|
+
text += `\n ${icon} ${theme.fg("accent", agent)}${theme.fg("dim", preview)}`;
|
|
429
|
+
}
|
|
430
|
+
if (tasks.length > 6) text += `\n ${theme.fg("muted", `... +${tasks.length - 6} more`)}`;
|
|
431
|
+
return new Text(text, 0, 0);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ---------------------------------------------------------------------------
|
|
435
|
+
// renderResult — shown after the tool completes / streams updates
|
|
436
|
+
// ---------------------------------------------------------------------------
|
|
437
|
+
|
|
438
|
+
export function renderResult(
|
|
439
|
+
result: { content: Array<{ type: string; text?: string }>; details?: unknown },
|
|
440
|
+
_expanded: boolean,
|
|
441
|
+
theme: { fg: ThemeFg; bold: (s: string) => string },
|
|
442
|
+
): Container | Text {
|
|
443
|
+
const details = result.details as SubagentDetails | undefined;
|
|
444
|
+
if (!details || details.results.length === 0) {
|
|
445
|
+
const first = result.content[0];
|
|
446
|
+
return new Text(first?.type === "text" && first.text ? first.text : "(no output)", 0, 0);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const nodes = buildTopLevelNodes(details);
|
|
450
|
+
const counts = countNodes(nodes);
|
|
451
|
+
const showOutputPreview = !hasNestedChildren(nodes);
|
|
452
|
+
const icon = counts.running > 0
|
|
453
|
+
? theme.fg("warning", "⏳")
|
|
454
|
+
: counts.error > 0
|
|
455
|
+
? theme.fg("error", "❌")
|
|
456
|
+
: theme.fg("success", "✅");
|
|
457
|
+
|
|
458
|
+
const container = new Container();
|
|
459
|
+
container.addChild(
|
|
460
|
+
new Text(
|
|
461
|
+
`${icon} ${theme.fg("toolTitle", theme.bold("subagent tree "))}${theme.fg("dim", topLevelSummary(details, counts))}`,
|
|
462
|
+
0,
|
|
463
|
+
0,
|
|
464
|
+
),
|
|
465
|
+
);
|
|
466
|
+
|
|
467
|
+
container.addChild(new Spacer(1));
|
|
468
|
+
container.addChild(new Text(renderTreeLines(nodes, theme, showOutputPreview).join("\n"), 0, 0));
|
|
469
|
+
|
|
470
|
+
return container;
|
|
471
|
+
}
|