oira666_pi-subagent 0.3.11 → 0.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/package.json +1 -1
- package/render.ts +114 -25
- package/tree.ts +163 -76
- package/types.ts +11 -7
package/README.md
CHANGED
|
@@ -175,7 +175,10 @@ reported immediately as failures.
|
|
|
175
175
|
## Timestamps & Status Footer
|
|
176
176
|
|
|
177
177
|
Subagent tool calls and live activity lines render a dim `hh:mm:ss` timestamp
|
|
178
|
-
(call start, per-subagent run start, and each live log entry).
|
|
178
|
+
(call start, per-subagent run start, and each live log entry). The collapsed tool
|
|
179
|
+
row keeps the normal compact progress text. Press `Ctrl+O` to expand it into the
|
|
180
|
+
subagent tree immediately; every running node shows its latest six activity
|
|
181
|
+
lines (thinking, tool starts/completions, and completed turns).
|
|
179
182
|
|
|
180
183
|
In the interactive TUI the extension publishes the combined `total` usage line
|
|
181
184
|
(parent + all subagents, recursively) via Pi's normal `ctx.ui.setStatus()`
|
package/package.json
CHANGED
package/render.ts
CHANGED
|
@@ -7,12 +7,19 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { Container, Spacer, Text } from "@mariozechner/pi-tui";
|
|
10
|
-
import
|
|
10
|
+
import {
|
|
11
|
+
MAX_LIVE_LOG_ENTRIES,
|
|
12
|
+
isResultError,
|
|
13
|
+
isSubagentDetails,
|
|
14
|
+
type SingleResult,
|
|
15
|
+
type SubagentDetails,
|
|
16
|
+
} from "./types.js";
|
|
11
17
|
import {
|
|
12
18
|
type ThemeFg,
|
|
13
19
|
buildTopLevelNodes,
|
|
14
20
|
countNodes,
|
|
15
21
|
formatClockTime,
|
|
22
|
+
formatLiveLogEntry,
|
|
16
23
|
hasNestedChildren,
|
|
17
24
|
renderTreeLines,
|
|
18
25
|
setBroadcastNumberingActive,
|
|
@@ -122,37 +129,119 @@ export function renderResumeCall(
|
|
|
122
129
|
// renderResult — shown after the tool completes / streams updates
|
|
123
130
|
// ---------------------------------------------------------------------------
|
|
124
131
|
|
|
132
|
+
function getResultText(
|
|
133
|
+
result: { content?: Array<{ type: string; text?: string }> },
|
|
134
|
+
): string {
|
|
135
|
+
const first = Array.isArray(result.content) ? result.content[0] : undefined;
|
|
136
|
+
return first?.type === "text" && typeof first.text === "string" && first.text
|
|
137
|
+
? first.text
|
|
138
|
+
: "(no output)";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function compactText(text: string, maxLength = 240): string {
|
|
142
|
+
const firstLine = text.replace(/\r\n?/g, "\n").split("\n").find((line) => line.trim()) ?? "(no output)";
|
|
143
|
+
return truncate(firstLine, maxLength);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isRenderableResult(value: unknown): value is SingleResult {
|
|
147
|
+
return value !== null &&
|
|
148
|
+
typeof value === "object" &&
|
|
149
|
+
!Array.isArray(value) &&
|
|
150
|
+
typeof (value as Partial<SingleResult>).agent === "string" &&
|
|
151
|
+
typeof (value as Partial<SingleResult>).exitCode === "number";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getCollapsedResultText(details: SubagentDetails, fallbackText: string): string {
|
|
155
|
+
const results = details.results.filter(isRenderableResult);
|
|
156
|
+
if (results.length === 0) return compactText(fallbackText);
|
|
157
|
+
|
|
158
|
+
const running = results.filter((item) => item.exitCode === -1).length;
|
|
159
|
+
const finished = results.length - running;
|
|
160
|
+
const succeeded = results.filter((item) => item.exitCode === 0 && !isResultError(item)).length;
|
|
161
|
+
const failed = finished - succeeded;
|
|
162
|
+
|
|
163
|
+
if (details.mode === "parallel") {
|
|
164
|
+
return running > 0
|
|
165
|
+
? `Parallel: ${finished}/${results.length} done, ${running} running...`
|
|
166
|
+
: `Parallel: ${succeeded}/${results.length} succeeded${failed > 0 ? `, ${failed} failed` : ""}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const item = results[0];
|
|
170
|
+
if (item.exitCode === -1) return `Agent ${item.agent}: running...`;
|
|
171
|
+
if (isResultError(item)) {
|
|
172
|
+
const reason = [item.errorMessage, item.stderr, item.stopReason]
|
|
173
|
+
.find((value): value is string => typeof value === "string" && value.length > 0);
|
|
174
|
+
return `Agent ${item.agent}: failed${reason ? ` — ${compactText(reason, 180)}` : ""}`;
|
|
175
|
+
}
|
|
176
|
+
return `Agent ${item.agent}: completed`;
|
|
177
|
+
}
|
|
178
|
+
|
|
125
179
|
export function renderResult(
|
|
126
180
|
result: { content: Array<{ type: string; text?: string }>; details?: unknown },
|
|
127
|
-
|
|
181
|
+
expanded: boolean,
|
|
128
182
|
theme: { fg: ThemeFg; bold: (s: string) => string },
|
|
129
183
|
): Container | Text {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
184
|
+
const fallbackText = getResultText(result);
|
|
185
|
+
|
|
186
|
+
// Keep the normal row compact. Ctrl+O switches to the live tree below.
|
|
187
|
+
if (!expanded) {
|
|
188
|
+
const text = isSubagentDetails(result.details)
|
|
189
|
+
? getCollapsedResultText(result.details, fallbackText)
|
|
190
|
+
: compactText(fallbackText);
|
|
191
|
+
return new Text(text, 0, 0);
|
|
134
192
|
}
|
|
135
193
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
? theme.fg("warning", "⏳")
|
|
141
|
-
: counts.error > 0
|
|
142
|
-
? theme.fg("error", "❌")
|
|
143
|
-
: theme.fg("success", "✅");
|
|
194
|
+
if (!isSubagentDetails(result.details) || result.details.results.length === 0) {
|
|
195
|
+
return new Text(fallbackText, 0, 0);
|
|
196
|
+
}
|
|
197
|
+
const details: SubagentDetails = result.details;
|
|
144
198
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
199
|
+
try {
|
|
200
|
+
const nodes = buildTopLevelNodes(details);
|
|
201
|
+
const counts = countNodes(nodes);
|
|
202
|
+
const showOutputPreview = !hasNestedChildren(nodes);
|
|
203
|
+
const icon = counts.running > 0
|
|
204
|
+
? theme.fg("warning", "⏳")
|
|
205
|
+
: counts.error > 0
|
|
206
|
+
? theme.fg("error", "❌")
|
|
207
|
+
: theme.fg("success", "✅");
|
|
153
208
|
|
|
154
|
-
|
|
155
|
-
|
|
209
|
+
const container = new Container();
|
|
210
|
+
container.addChild(
|
|
211
|
+
new Text(
|
|
212
|
+
`${icon} ${theme.fg("toolTitle", theme.bold("subagent tree "))}${theme.fg("dim", topLevelSummary(details, counts))}`,
|
|
213
|
+
0,
|
|
214
|
+
0,
|
|
215
|
+
),
|
|
216
|
+
);
|
|
156
217
|
|
|
157
|
-
|
|
218
|
+
container.addChild(new Spacer(1));
|
|
219
|
+
container.addChild(new Text(renderTreeLines(nodes, theme, showOutputPreview).join("\n"), 0, 0));
|
|
220
|
+
return container;
|
|
221
|
+
} catch {
|
|
222
|
+
// Pi falls back to raw result.content when a custom renderer throws, which
|
|
223
|
+
// made Ctrl+O appear broken until the offending rolling log entry expired.
|
|
224
|
+
// Keep an expanded tree visible even for malformed third-party/RPC data.
|
|
225
|
+
const lines: string[] = [];
|
|
226
|
+
for (const item of details.results) {
|
|
227
|
+
const agent = typeof item?.agent === "string" ? item.agent : "unknown agent";
|
|
228
|
+
const icon = item?.exitCode === -1
|
|
229
|
+
? theme.fg("warning", "⏳")
|
|
230
|
+
: item?.exitCode === 0
|
|
231
|
+
? theme.fg("success", "✅")
|
|
232
|
+
: theme.fg("error", "❌");
|
|
233
|
+
lines.push(`${icon} ${theme.fg("accent", agent)}`);
|
|
234
|
+
const liveLog = Array.isArray(item?.liveLog)
|
|
235
|
+
? item.liveLog.slice(-MAX_LIVE_LOG_ENTRIES)
|
|
236
|
+
: [];
|
|
237
|
+
for (const entry of liveLog) {
|
|
238
|
+
lines.push(` ${formatLiveLogEntry(entry, theme)}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return new Text(
|
|
242
|
+
`${theme.fg("toolTitle", theme.bold("subagent tree"))}\n${lines.join("\n") || fallbackText}`,
|
|
243
|
+
0,
|
|
244
|
+
0,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
158
247
|
}
|
package/tree.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type LiveLogEntry,
|
|
14
14
|
type NestedSubagentResult,
|
|
15
15
|
type SingleResult,
|
|
16
|
+
MAX_LIVE_LOG_ENTRIES,
|
|
16
17
|
type SubagentDetails,
|
|
17
18
|
type UsageStats,
|
|
18
19
|
aggregateUsage,
|
|
@@ -65,17 +66,32 @@ interface PendingSubagentCall {
|
|
|
65
66
|
// Formatting helpers
|
|
66
67
|
// ---------------------------------------------------------------------------
|
|
67
68
|
|
|
69
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
70
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
71
|
+
? value as Record<string, unknown>
|
|
72
|
+
: {};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function finiteNumber(value: unknown): number {
|
|
76
|
+
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function stringValue(value: unknown, fallback = ""): string {
|
|
80
|
+
return typeof value === "string" ? value : fallback;
|
|
81
|
+
}
|
|
82
|
+
|
|
68
83
|
export function formatClockTime(epochMs: number): string {
|
|
69
|
-
const d = new Date(epochMs);
|
|
84
|
+
const d = new Date(Number.isFinite(epochMs) ? epochMs : 0);
|
|
70
85
|
const pad = (n: number) => String(n).padStart(2, "0");
|
|
71
86
|
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
72
87
|
}
|
|
73
88
|
|
|
74
89
|
export function formatTokens(count: number): string {
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
if (
|
|
78
|
-
return `${(
|
|
90
|
+
const safeCount = finiteNumber(count);
|
|
91
|
+
if (safeCount < 1000) return safeCount.toString();
|
|
92
|
+
if (safeCount < 10000) return `${(safeCount / 1000).toFixed(1)}k`;
|
|
93
|
+
if (safeCount < 1000000) return `${Math.round(safeCount / 1000)}k`;
|
|
94
|
+
return `${(safeCount / 1000000).toFixed(1)}M`;
|
|
79
95
|
}
|
|
80
96
|
|
|
81
97
|
export function formatCombinedUsageStatusLine(
|
|
@@ -95,36 +111,41 @@ export function formatCombinedUsageStatusLine(
|
|
|
95
111
|
return `WITH SUBS: (${subagentCount}) Σ $${cost.toFixed(4)} • ↑${formatTokens(input)} ↓${formatTokens(output)}${cache} T${formatTokens(total)} • ${turns} turn${turns === 1 ? "" : "s"}`;
|
|
96
112
|
}
|
|
97
113
|
|
|
98
|
-
export function formatUsage(usage: Partial<UsageStats
|
|
114
|
+
export function formatUsage(usage: Partial<UsageStats> | unknown, model?: unknown): string {
|
|
115
|
+
const safeUsage = asRecord(usage);
|
|
116
|
+
const input = finiteNumber(safeUsage.input);
|
|
117
|
+
const output = finiteNumber(safeUsage.output);
|
|
118
|
+
const cacheRead = finiteNumber(safeUsage.cacheRead);
|
|
119
|
+
const cacheWrite = finiteNumber(safeUsage.cacheWrite);
|
|
120
|
+
const cost = finiteNumber(safeUsage.cost);
|
|
121
|
+
const contextTokens = finiteNumber(safeUsage.contextTokens);
|
|
122
|
+
const turns = finiteNumber(safeUsage.turns);
|
|
99
123
|
const parts: string[] = [];
|
|
100
|
-
const totalTokens =
|
|
101
|
-
|
|
102
|
-
(usage.output || 0) +
|
|
103
|
-
(usage.cacheRead || 0) +
|
|
104
|
-
(usage.cacheWrite || 0);
|
|
105
|
-
if (usage.turns) parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
124
|
+
const totalTokens = input + output + cacheRead + cacheWrite;
|
|
125
|
+
if (turns) parts.push(`${turns} turn${turns > 1 ? "s" : ""}`);
|
|
106
126
|
if (totalTokens > 0) parts.push(`tok:${formatTokens(totalTokens)}`);
|
|
107
|
-
if (
|
|
108
|
-
if (
|
|
109
|
-
if (
|
|
110
|
-
if (
|
|
111
|
-
if (
|
|
112
|
-
if (
|
|
113
|
-
if (model) parts.push(model);
|
|
127
|
+
if (input) parts.push(`in:${formatTokens(input)}`);
|
|
128
|
+
if (output) parts.push(`out:${formatTokens(output)}`);
|
|
129
|
+
if (cacheRead) parts.push(`cacheR:${formatTokens(cacheRead)}`);
|
|
130
|
+
if (cacheWrite) parts.push(`cacheW:${formatTokens(cacheWrite)}`);
|
|
131
|
+
if (cost) parts.push(`$${cost.toFixed(4)}`);
|
|
132
|
+
if (contextTokens > 0) parts.push(`ctx:${formatTokens(contextTokens)}`);
|
|
133
|
+
if (typeof model === "string" && model) parts.push(model);
|
|
114
134
|
return parts.join(" • ");
|
|
115
135
|
}
|
|
116
136
|
|
|
117
|
-
export function truncate(text:
|
|
118
|
-
|
|
137
|
+
export function truncate(text: unknown, maxLen: number): string {
|
|
138
|
+
const safeText = stringValue(text);
|
|
139
|
+
return safeText.length > maxLen ? `${safeText.slice(0, maxLen)}...` : safeText;
|
|
119
140
|
}
|
|
120
141
|
|
|
121
|
-
function splitOutputLines(text:
|
|
122
|
-
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
142
|
+
function splitOutputLines(text: unknown): string[] {
|
|
143
|
+
const lines = stringValue(text).replace(/\r\n?/g, "\n").split("\n");
|
|
123
144
|
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
124
145
|
return lines;
|
|
125
146
|
}
|
|
126
147
|
|
|
127
|
-
function lastNonEmptyLines(text:
|
|
148
|
+
function lastNonEmptyLines(text: unknown, limit: number): string[] {
|
|
128
149
|
return splitOutputLines(text)
|
|
129
150
|
.map((line) => line.trimEnd())
|
|
130
151
|
.filter((line) => line.trim().length > 0)
|
|
@@ -173,11 +194,12 @@ export function hasNestedChildren(nodes: TreeNode[]): boolean {
|
|
|
173
194
|
return nodes.some((node) => node.children.length > 0 || hasNestedChildren(node.children));
|
|
174
195
|
}
|
|
175
196
|
|
|
176
|
-
function extractPendingSubagentCalls(messages: SingleResult["messages"]): PendingSubagentCall[] {
|
|
197
|
+
function extractPendingSubagentCalls(messages: SingleResult["messages"] | unknown): PendingSubagentCall[] {
|
|
198
|
+
const history = Array.isArray(messages) ? messages : [];
|
|
177
199
|
const calls: PendingSubagentCall[] = [];
|
|
178
|
-
for (let messageIndex = 0; messageIndex <
|
|
179
|
-
const message =
|
|
180
|
-
if (message
|
|
200
|
+
for (let messageIndex = 0; messageIndex < history.length; messageIndex++) {
|
|
201
|
+
const message = history[messageIndex] as any;
|
|
202
|
+
if (message?.role !== "assistant" || !Array.isArray(message.content)) continue;
|
|
181
203
|
for (let partIndex = 0; partIndex < message.content.length; partIndex++) {
|
|
182
204
|
const part = message.content[partIndex] as any;
|
|
183
205
|
if (part?.type !== "toolCall" || !isSubagentToolName(part?.name)) continue;
|
|
@@ -235,11 +257,16 @@ function subagentCallSignature(call: PendingSubagentCall): string {
|
|
|
235
257
|
|
|
236
258
|
function nestedResultIsHealthy(nested: NestedSubagentResult | undefined): boolean {
|
|
237
259
|
if (!nested || nested.isError) return false;
|
|
238
|
-
return nested.details.results.every(
|
|
260
|
+
return nested.details.results.every(
|
|
261
|
+
(result) => result !== null && typeof result === "object" && !Array.isArray(result) && !isResultError(result),
|
|
262
|
+
);
|
|
239
263
|
}
|
|
240
264
|
|
|
241
265
|
function buildLiveDetailsSignature(details: SubagentDetails): string {
|
|
242
|
-
return JSON.stringify(details.results.map((result) =>
|
|
266
|
+
return JSON.stringify(details.results.map((result) => {
|
|
267
|
+
const value = asRecord(result);
|
|
268
|
+
return { agent: stringValue(value.agent), task: stringValue(value.task) };
|
|
269
|
+
}));
|
|
243
270
|
}
|
|
244
271
|
|
|
245
272
|
function findLiveNestedDetailsForCall(
|
|
@@ -276,7 +303,9 @@ function findLiveNestedDetailsForCall(
|
|
|
276
303
|
const agentSignature = JSON.stringify(call.tasks.map((task) => task.agent));
|
|
277
304
|
for (const [key, details] of Object.entries(live)) {
|
|
278
305
|
if (usedLiveKeys.has(key) || !isSubagentDetails(details)) continue;
|
|
279
|
-
const liveAgentSignature = JSON.stringify(
|
|
306
|
+
const liveAgentSignature = JSON.stringify(
|
|
307
|
+
details.results.map((nestedResult) => stringValue(asRecord(nestedResult).agent)),
|
|
308
|
+
);
|
|
280
309
|
if (liveAgentSignature !== agentSignature) continue;
|
|
281
310
|
usedLiveKeys.add(key);
|
|
282
311
|
return details;
|
|
@@ -339,38 +368,53 @@ function buildNestedChildren(result: SingleResult): TreeNode[] {
|
|
|
339
368
|
return nodes;
|
|
340
369
|
}
|
|
341
370
|
|
|
342
|
-
function formatToolArgPreview(toolName:
|
|
343
|
-
const
|
|
371
|
+
function formatToolArgPreview(toolName: unknown, rawArgs: unknown): string {
|
|
372
|
+
const args = asRecord(rawArgs);
|
|
373
|
+
const shorten = (value: unknown) => {
|
|
374
|
+
const p = stringValue(value);
|
|
344
375
|
const home = os.homedir();
|
|
345
376
|
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
346
377
|
};
|
|
347
|
-
const truncateTo = (
|
|
348
|
-
|
|
378
|
+
const truncateTo = (value: unknown, n: number) => {
|
|
379
|
+
const text = stringValue(value);
|
|
380
|
+
return text.length > n ? text.slice(0, n) + "\u2026" : text;
|
|
381
|
+
};
|
|
349
382
|
|
|
350
383
|
switch (toolName) {
|
|
351
|
-
case "bash":
|
|
352
|
-
|
|
353
|
-
return truncateTo(cmd.replace(/\s+/g, " "), 52);
|
|
354
|
-
}
|
|
384
|
+
case "bash":
|
|
385
|
+
return truncateTo(stringValue(args.command).replace(/\s+/g, " "), 52);
|
|
355
386
|
case "read":
|
|
356
387
|
case "write":
|
|
357
388
|
case "edit":
|
|
358
|
-
return shorten(truncateTo(
|
|
359
|
-
case "grep":
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
389
|
+
return shorten(truncateTo(args.path ?? args.file_path, 52));
|
|
390
|
+
case "grep": {
|
|
391
|
+
const pattern = stringValue(args.pattern);
|
|
392
|
+
const target = stringValue(args.path);
|
|
393
|
+
return truncateTo(`/${pattern}/`, 30) + (target ? ` in ${shorten(target)}` : "");
|
|
394
|
+
}
|
|
395
|
+
case "find": {
|
|
396
|
+
const pattern = stringValue(args.pattern, "*");
|
|
397
|
+
const target = stringValue(args.path);
|
|
398
|
+
return truncateTo(pattern, 30) + (target ? ` in ${shorten(target)}` : "");
|
|
399
|
+
}
|
|
365
400
|
case "subagent":
|
|
366
401
|
case "subagents": {
|
|
367
|
-
const tasks = (args.tasks
|
|
368
|
-
return tasks
|
|
402
|
+
const tasks = Array.isArray(args.tasks) ? args.tasks : [];
|
|
403
|
+
return tasks
|
|
404
|
+
.map((task) => stringValue(asRecord(task).agent))
|
|
405
|
+
.filter(Boolean)
|
|
406
|
+
.join(", ");
|
|
369
407
|
}
|
|
370
408
|
case "resume_subagents": {
|
|
371
|
-
const raw =
|
|
372
|
-
const resumes = Array.isArray(raw) ? raw : raw ? [raw] : [];
|
|
373
|
-
return resumes
|
|
409
|
+
const raw = args.resumes;
|
|
410
|
+
const resumes = Array.isArray(raw) ? raw : raw && typeof raw === "object" ? [raw] : [];
|
|
411
|
+
return resumes
|
|
412
|
+
.map((resume) => {
|
|
413
|
+
const item = asRecord(resume);
|
|
414
|
+
return stringValue(item.subagent) || stringValue(item.name);
|
|
415
|
+
})
|
|
416
|
+
.filter(Boolean)
|
|
417
|
+
.join(", ");
|
|
374
418
|
}
|
|
375
419
|
default:
|
|
376
420
|
return "";
|
|
@@ -381,12 +425,14 @@ export function formatLiveLogEntry(
|
|
|
381
425
|
entry: LiveLogEntry,
|
|
382
426
|
theme: { fg: ThemeFg },
|
|
383
427
|
): string {
|
|
384
|
-
const
|
|
385
|
-
|
|
428
|
+
const value = asRecord(entry);
|
|
429
|
+
const at = typeof value.at === "number" && Number.isFinite(value.at) ? value.at : undefined;
|
|
430
|
+
const stamp = at !== undefined ? theme.fg("dim", formatClockTime(at)) + " " : "";
|
|
431
|
+
return stamp + formatLiveLogEntryBody(value, theme);
|
|
386
432
|
}
|
|
387
433
|
|
|
388
434
|
function formatLiveLogEntryBody(
|
|
389
|
-
entry:
|
|
435
|
+
entry: Record<string, unknown>,
|
|
390
436
|
theme: { fg: ThemeFg },
|
|
391
437
|
): string {
|
|
392
438
|
switch (entry.kind) {
|
|
@@ -394,24 +440,28 @@ function formatLiveLogEntryBody(
|
|
|
394
440
|
return theme.fg("muted", "\u27f3") + " " + theme.fg("dim", "thinking\u2026");
|
|
395
441
|
|
|
396
442
|
case "turn_end": {
|
|
397
|
-
const
|
|
443
|
+
const inputTokens = finiteNumber(entry.inputTokens);
|
|
444
|
+
const outputTokens = finiteNumber(entry.outputTokens);
|
|
445
|
+
const tokens = inputTokens || outputTokens
|
|
398
446
|
? " " + theme.fg("dim",
|
|
399
|
-
`\u2191${formatTokens(
|
|
447
|
+
`\u2191${formatTokens(inputTokens)} \u2193${formatTokens(outputTokens)}`)
|
|
400
448
|
: "";
|
|
449
|
+
const turn = finiteNumber(entry.turn);
|
|
401
450
|
return (
|
|
402
451
|
theme.fg("success", "\u2713") +
|
|
403
452
|
" " +
|
|
404
|
-
theme.fg("muted", `turn ${
|
|
453
|
+
theme.fg("muted", `turn ${turn}`) +
|
|
405
454
|
tokens
|
|
406
455
|
);
|
|
407
456
|
}
|
|
408
457
|
|
|
409
458
|
case "tool_start": {
|
|
410
|
-
const
|
|
459
|
+
const toolName = stringValue(entry.toolName, "unknown tool");
|
|
460
|
+
const argPreview = formatToolArgPreview(toolName, entry.args);
|
|
411
461
|
return (
|
|
412
462
|
theme.fg("muted", "\u2192") +
|
|
413
463
|
" " +
|
|
414
|
-
theme.fg("accent",
|
|
464
|
+
theme.fg("accent", toolName) +
|
|
415
465
|
(argPreview ? " " + theme.fg("dim", argPreview) : "")
|
|
416
466
|
);
|
|
417
467
|
}
|
|
@@ -420,8 +470,11 @@ function formatLiveLogEntryBody(
|
|
|
420
470
|
return (
|
|
421
471
|
theme.fg("success", "\u2713") +
|
|
422
472
|
" " +
|
|
423
|
-
theme.fg("accent", entry.toolName)
|
|
473
|
+
theme.fg("accent", stringValue(entry.toolName, "unknown tool"))
|
|
424
474
|
);
|
|
475
|
+
|
|
476
|
+
default:
|
|
477
|
+
return theme.fg("muted", "activity");
|
|
425
478
|
}
|
|
426
479
|
}
|
|
427
480
|
|
|
@@ -482,33 +535,64 @@ function loadMessagesFromSession(sessionDir: string | undefined): SingleResult["
|
|
|
482
535
|
}
|
|
483
536
|
|
|
484
537
|
function hydrateResultFromSession(result: SingleResult): SingleResult {
|
|
485
|
-
if (result.messages
|
|
486
|
-
const messages = loadMessagesFromSession(
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
538
|
+
if (Array.isArray(result.messages) && result.messages.length > 0) return result;
|
|
539
|
+
const messages = loadMessagesFromSession(
|
|
540
|
+
typeof result.sessionDir === "string" ? result.sessionDir : undefined,
|
|
541
|
+
);
|
|
542
|
+
if (messages.length > 0) return { ...result, messages };
|
|
543
|
+
return Array.isArray(result.messages) ? result : { ...result, messages: [] };
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function buildResultNode(rawResult: SingleResult): TreeNode {
|
|
547
|
+
let result: SingleResult;
|
|
548
|
+
if (
|
|
549
|
+
rawResult !== null &&
|
|
550
|
+
typeof rawResult === "object" &&
|
|
551
|
+
!Array.isArray(rawResult) &&
|
|
552
|
+
typeof rawResult.agent === "string" &&
|
|
553
|
+
typeof rawResult.exitCode === "number"
|
|
554
|
+
) {
|
|
555
|
+
result = hydrateResultFromSession(rawResult);
|
|
556
|
+
} else {
|
|
557
|
+
result = {
|
|
558
|
+
agent: "unknown agent",
|
|
559
|
+
agentSource: "unknown",
|
|
560
|
+
task: "",
|
|
561
|
+
exitCode: 1,
|
|
562
|
+
messages: [],
|
|
563
|
+
stderr: "Malformed subagent result.",
|
|
564
|
+
usage: {} as UsageStats,
|
|
565
|
+
toolCalls: {},
|
|
566
|
+
completedTurns: 0,
|
|
567
|
+
turnInProgress: false,
|
|
568
|
+
liveLog: [],
|
|
569
|
+
};
|
|
570
|
+
}
|
|
492
571
|
const status = statusFromResult(result);
|
|
493
|
-
const usage = formatUsage(result.usage
|
|
572
|
+
const usage = formatUsage(result.usage, result.model);
|
|
494
573
|
const metaParts: string[] = [];
|
|
495
|
-
if (result.name) metaParts.push(result.name);
|
|
496
|
-
metaParts.push(result.agentSource);
|
|
574
|
+
if (typeof result.name === "string" && result.name) metaParts.push(result.name);
|
|
575
|
+
if (typeof result.agentSource === "string" && result.agentSource) metaParts.push(result.agentSource);
|
|
497
576
|
if (usage) metaParts.push(usage);
|
|
498
577
|
if (status === "error") {
|
|
499
578
|
const errorText = result.errorMessage || result.stderr || result.stopReason;
|
|
500
|
-
if (
|
|
579
|
+
if (typeof errorText === "string" && errorText) {
|
|
580
|
+
metaParts.push(truncate(errorText.replace(/\s+/g, " "), 120));
|
|
581
|
+
}
|
|
501
582
|
}
|
|
502
583
|
|
|
503
584
|
const children = buildNestedChildren(result);
|
|
504
585
|
const isRunning = status === "running";
|
|
586
|
+
const liveLog = Array.isArray(result.liveLog) ? result.liveLog : [];
|
|
505
587
|
return {
|
|
506
|
-
label: result.agent,
|
|
588
|
+
label: stringValue(result.agent, "unknown agent"),
|
|
507
589
|
status,
|
|
508
590
|
meta: metaParts.join(" • "),
|
|
509
|
-
task: result.task,
|
|
510
|
-
startedAt: result.startedAt
|
|
511
|
-
|
|
591
|
+
task: typeof result.task === "string" ? result.task : undefined,
|
|
592
|
+
startedAt: typeof result.startedAt === "number" && Number.isFinite(result.startedAt)
|
|
593
|
+
? result.startedAt
|
|
594
|
+
: undefined,
|
|
595
|
+
liveActivity: isRunning && liveLog.length > 0 ? liveLog.slice(-MAX_LIVE_LOG_ENTRIES) : undefined,
|
|
512
596
|
outputPreview: !isRunning && children.length === 0 ? buildLeafPreview(result) : undefined,
|
|
513
597
|
children,
|
|
514
598
|
};
|
|
@@ -567,8 +651,11 @@ export function renderTreeLines(
|
|
|
567
651
|
export function topLevelSummary(details: SubagentDetails, counts: TreeCounts): string {
|
|
568
652
|
// aggregatedUsage includes own agents + all their nested descendants;
|
|
569
653
|
// fall back to summing only direct results for old serialised data lacking the field.
|
|
654
|
+
const safeResults = details.results.filter(
|
|
655
|
+
(result): result is SingleResult => result !== null && typeof result === "object" && !Array.isArray(result),
|
|
656
|
+
);
|
|
570
657
|
const totalUsage = formatUsage(
|
|
571
|
-
usageSummaryToUsageStats(details.usageSummary) ?? details.aggregatedUsage ?? aggregateUsage(
|
|
658
|
+
usageSummaryToUsageStats(details.usageSummary) ?? details.aggregatedUsage ?? aggregateUsage(safeResults),
|
|
572
659
|
);
|
|
573
660
|
const parts = [
|
|
574
661
|
`${counts.running} running`,
|
package/types.ts
CHANGED
|
@@ -204,7 +204,7 @@ function asMessageArray(messages: unknown): Message[] {
|
|
|
204
204
|
export function extractToolCalls(messages: unknown): ToolCallCounts {
|
|
205
205
|
const counts: ToolCallCounts = {};
|
|
206
206
|
for (const msg of asMessageArray(messages)) {
|
|
207
|
-
if (msg
|
|
207
|
+
if ((msg as any)?.role !== "assistant") continue;
|
|
208
208
|
for (const part of (Array.isArray((msg as any).content) ? (msg as any).content : [])) {
|
|
209
209
|
if ((part as any)?.type !== "toolCall") continue;
|
|
210
210
|
const name: string = typeof (part as any).name === "string" ? (part as any).name : "unknown";
|
|
@@ -479,10 +479,10 @@ export function getFinalOutput(messages: unknown, fallback?: string): string {
|
|
|
479
479
|
const history = asMessageArray(messages);
|
|
480
480
|
for (let i = history.length - 1; i >= 0; i--) {
|
|
481
481
|
const msg = history[i];
|
|
482
|
-
if (msg
|
|
482
|
+
if ((msg as any)?.role === "assistant" && Array.isArray((msg as any).content)) {
|
|
483
483
|
for (let j = (msg as any).content.length - 1; j >= 0; j--) {
|
|
484
484
|
const part = (msg as any).content[j];
|
|
485
|
-
if (part?.type === "text") return part.text;
|
|
485
|
+
if (part?.type === "text" && typeof part.text === "string") return part.text;
|
|
486
486
|
}
|
|
487
487
|
}
|
|
488
488
|
}
|
|
@@ -493,12 +493,16 @@ export function getFinalOutput(messages: unknown, fallback?: string): string {
|
|
|
493
493
|
export function getDisplayItems(messages: unknown): DisplayItem[] {
|
|
494
494
|
const items: DisplayItem[] = [];
|
|
495
495
|
for (const msg of asMessageArray(messages)) {
|
|
496
|
-
if (msg
|
|
496
|
+
if ((msg as any)?.role === "assistant" && Array.isArray((msg as any).content)) {
|
|
497
497
|
for (const part of (msg as any).content) {
|
|
498
|
-
if (part?.type === "text") {
|
|
498
|
+
if (part?.type === "text" && typeof part.text === "string") {
|
|
499
499
|
items.push({ type: "text", text: part.text });
|
|
500
|
-
} else if (part?.type === "toolCall") {
|
|
501
|
-
items.push({
|
|
500
|
+
} else if (part?.type === "toolCall" && typeof part.name === "string") {
|
|
501
|
+
items.push({
|
|
502
|
+
type: "toolCall",
|
|
503
|
+
name: part.name,
|
|
504
|
+
args: part.arguments && typeof part.arguments === "object" ? part.arguments : {},
|
|
505
|
+
});
|
|
502
506
|
}
|
|
503
507
|
}
|
|
504
508
|
}
|