@narumitw/pi-subagents 0.43.0 → 0.46.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/README.md +53 -13
- package/package.json +1 -1
- package/src/agents.ts +16 -0
- package/src/config-ui.ts +151 -20
- package/src/consult-render.ts +194 -0
- package/src/consult.ts +164 -37
- package/src/cwd-policy.ts +183 -0
- package/src/execution.ts +127 -64
- package/src/in-process-transport.ts +3 -3
- package/src/inspect-render.ts +234 -0
- package/src/inspect.ts +51 -3
- package/src/persistence.ts +29 -0
- package/src/pi-invocation.ts +168 -0
- package/src/registry.ts +12 -0
- package/src/render-common.ts +252 -0
- package/src/render.ts +134 -99
- package/src/runner.ts +19 -22
- package/src/settings.ts +111 -11
- package/src/stateful-guidance.ts +35 -0
- package/src/stateful-lifecycle.ts +31 -0
- package/src/stateful-render.ts +249 -0
- package/src/stateful-safety.ts +91 -0
- package/src/stateful.ts +235 -218
- package/src/subagents.ts +60 -18
- package/src/subprocess-transport.ts +19 -2
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import {
|
|
4
|
+
keyHint,
|
|
5
|
+
type Theme,
|
|
6
|
+
type ThemeColor,
|
|
7
|
+
type ToolRenderResultOptions,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
10
|
+
import { boundedPrivateText, safeTerminalLine } from "./safe-text.js";
|
|
11
|
+
|
|
12
|
+
export const COLLAPSED_LIST_LIMIT = 5;
|
|
13
|
+
export const COLLAPSED_ANSWER_LINES = 3;
|
|
14
|
+
|
|
15
|
+
export interface ToolRendererContext<TArgs> {
|
|
16
|
+
args: TArgs;
|
|
17
|
+
isError: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type RenderStatus =
|
|
21
|
+
| "starting"
|
|
22
|
+
| "running"
|
|
23
|
+
| "completed"
|
|
24
|
+
| "failed"
|
|
25
|
+
| "cancelled"
|
|
26
|
+
| "interrupted"
|
|
27
|
+
| "idle"
|
|
28
|
+
| "closed"
|
|
29
|
+
| "warning";
|
|
30
|
+
|
|
31
|
+
const STATUS_PRESENTATION: Record<
|
|
32
|
+
RenderStatus,
|
|
33
|
+
{ icon: string; label: string; color: ThemeColor }
|
|
34
|
+
> = {
|
|
35
|
+
starting: { icon: "⏳", label: "Starting", color: "warning" },
|
|
36
|
+
running: { icon: "⏳", label: "Running", color: "warning" },
|
|
37
|
+
completed: { icon: "✓", label: "Completed", color: "success" },
|
|
38
|
+
failed: { icon: "✗", label: "Failed", color: "error" },
|
|
39
|
+
cancelled: { icon: "■", label: "Cancelled", color: "warning" },
|
|
40
|
+
interrupted: { icon: "■", label: "Interrupted", color: "warning" },
|
|
41
|
+
idle: { icon: "○", label: "Idle", color: "muted" },
|
|
42
|
+
closed: { icon: "✓", label: "Closed", color: "muted" },
|
|
43
|
+
warning: { icon: "◐", label: "Warning", color: "warning" },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export function recordValue(value: unknown): Record<string, unknown> | undefined {
|
|
47
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
48
|
+
? (value as Record<string, unknown>)
|
|
49
|
+
: undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function recordList(value: unknown): Record<string, unknown>[] {
|
|
53
|
+
return Array.isArray(value)
|
|
54
|
+
? value.flatMap((item) => {
|
|
55
|
+
const record = recordValue(item);
|
|
56
|
+
return record ? [record] : [];
|
|
57
|
+
})
|
|
58
|
+
: [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function stringValue(value: unknown, fallback = ""): string {
|
|
62
|
+
return typeof value === "string" ? value : fallback;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function numberValue(value: unknown, fallback = 0): number {
|
|
66
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function booleanValue(value: unknown): boolean {
|
|
70
|
+
return value === true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function safeLine(value: unknown, fallback = "...", maxBytes = 2 * 1024): string {
|
|
74
|
+
if (typeof value !== "string" || !value.trim()) return fallback;
|
|
75
|
+
return safeTerminalLine(value, maxBytes) || fallback;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function safeBlock(value: unknown, fallback = "", maxBytes = 50 * 1024): string {
|
|
79
|
+
if (typeof value !== "string" || !value) return fallback;
|
|
80
|
+
return boundedPrivateText(value, maxBytes);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function previewLines(value: unknown, maxLines = COLLAPSED_ANSWER_LINES): string {
|
|
84
|
+
const text = safeBlock(value, "", 8 * 1024).trim();
|
|
85
|
+
return text.split("\n").slice(0, maxLines).join("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function textResult(result: AgentToolResult<unknown>): string {
|
|
89
|
+
return result.content
|
|
90
|
+
.flatMap((part) => (part.type === "text" ? [part.text] : []))
|
|
91
|
+
.join("\n")
|
|
92
|
+
.trim();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function toolHeader(
|
|
96
|
+
theme: Theme,
|
|
97
|
+
toolName: string,
|
|
98
|
+
primary?: unknown,
|
|
99
|
+
metadata: readonly string[] = [],
|
|
100
|
+
): string {
|
|
101
|
+
let text = theme.fg("toolTitle", theme.bold(`${toolName} `));
|
|
102
|
+
if (primary !== undefined) text += theme.fg("accent", safeLine(primary));
|
|
103
|
+
const safeMetadata = metadata.filter(Boolean).map((item) => safeLine(item, "", 512));
|
|
104
|
+
if (safeMetadata.length > 0) text += theme.fg("muted", ` · ${safeMetadata.join(" · ")}`);
|
|
105
|
+
return text.trimEnd();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function statusBadge(theme: Theme, status: RenderStatus, suffix?: string): string {
|
|
109
|
+
const presentation = STATUS_PRESENTATION[status];
|
|
110
|
+
const label = suffix
|
|
111
|
+
? `${presentation.label} · ${safeLine(suffix, "", 2 * 1024)}`
|
|
112
|
+
: presentation.label;
|
|
113
|
+
return `${theme.fg(presentation.color, presentation.icon)} ${theme.fg(presentation.color, label)}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function renderFallbackResult(
|
|
117
|
+
result: AgentToolResult<unknown>,
|
|
118
|
+
options: ToolRenderResultOptions,
|
|
119
|
+
theme: Theme,
|
|
120
|
+
isError = false,
|
|
121
|
+
) {
|
|
122
|
+
const status: RenderStatus = isError ? "failed" : options.isPartial ? "running" : "completed";
|
|
123
|
+
const content = safeBlock(textResult(result), "(no output)", 8 * 1024);
|
|
124
|
+
return new Text(`${statusBadge(theme, status)}\n${theme.fg("toolOutput", content)}`, 0, 0);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function expansionHint(): string {
|
|
128
|
+
return keyHint("app.tools.expand", "to expand");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface RenderActivityItem {
|
|
132
|
+
type: "text" | "toolCall";
|
|
133
|
+
text?: string;
|
|
134
|
+
name?: string;
|
|
135
|
+
args?: Record<string, unknown>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function projectRenderActivity(value: unknown): RenderActivityItem[] {
|
|
139
|
+
if (!Array.isArray(value)) return [];
|
|
140
|
+
const items: RenderActivityItem[] = [];
|
|
141
|
+
for (const item of value) {
|
|
142
|
+
const record = recordValue(item);
|
|
143
|
+
if (!record) continue;
|
|
144
|
+
if (record.type === "text" && typeof record.text === "string") {
|
|
145
|
+
items.push({ type: "text", text: safeBlock(record.text, "", 1024) });
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (record.type === "toolCall" && typeof record.name === "string") {
|
|
149
|
+
items.push({
|
|
150
|
+
type: "toolCall",
|
|
151
|
+
name: safeLine(record.name, "tool", 256),
|
|
152
|
+
args: recordValue(record.args) ?? {},
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return items;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function renderActivityLines(
|
|
160
|
+
items: readonly RenderActivityItem[],
|
|
161
|
+
theme: Theme,
|
|
162
|
+
limit?: number,
|
|
163
|
+
total = items.length,
|
|
164
|
+
): string {
|
|
165
|
+
const selected = limit === undefined ? items : items.slice(-limit);
|
|
166
|
+
const lines: string[] = [];
|
|
167
|
+
const skipped = Math.max(0, total - selected.length);
|
|
168
|
+
if (skipped > 0) lines.push(theme.fg("muted", `… ${skipped} earlier activities`));
|
|
169
|
+
for (const item of selected) {
|
|
170
|
+
if (item.type === "text") {
|
|
171
|
+
const text = safeBlock(item.text, "", 1024).trim();
|
|
172
|
+
if (text) lines.push(theme.fg("toolOutput", text));
|
|
173
|
+
} else {
|
|
174
|
+
lines.push(
|
|
175
|
+
theme.fg("muted", "→ ") +
|
|
176
|
+
formatToolActivity(item.name ?? "tool", item.args ?? {}, theme.fg.bind(theme)),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return lines.join("\n");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function formatToolActivity(
|
|
184
|
+
toolNameValue: unknown,
|
|
185
|
+
argsValue: unknown,
|
|
186
|
+
themeFg: (color: ThemeColor, text: string) => string,
|
|
187
|
+
): string {
|
|
188
|
+
const toolName = safeLine(toolNameValue, "tool", 256);
|
|
189
|
+
const args = recordValue(argsValue) ?? {};
|
|
190
|
+
const shortenPath = (value: unknown, fallback = ".") => {
|
|
191
|
+
const filePath = safeLine(value, fallback, 2 * 1024);
|
|
192
|
+
const home = os.homedir();
|
|
193
|
+
return filePath.startsWith(home) ? `~${filePath.slice(home.length)}` : filePath;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
switch (toolName) {
|
|
197
|
+
case "bash": {
|
|
198
|
+
const command = safeLine(args.command, "...", 512);
|
|
199
|
+
return themeFg("muted", "$ ") + themeFg("toolOutput", command);
|
|
200
|
+
}
|
|
201
|
+
case "read": {
|
|
202
|
+
const filePath = shortenPath(args.file_path ?? args.path, "...");
|
|
203
|
+
const offset = typeof args.offset === "number" ? args.offset : undefined;
|
|
204
|
+
const limit = typeof args.limit === "number" ? args.limit : undefined;
|
|
205
|
+
let text = themeFg("accent", filePath);
|
|
206
|
+
if (offset !== undefined || limit !== undefined) {
|
|
207
|
+
const startLine = offset ?? 1;
|
|
208
|
+
const endLine = limit !== undefined ? startLine + limit - 1 : "";
|
|
209
|
+
text += themeFg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
|
|
210
|
+
}
|
|
211
|
+
return themeFg("muted", "read ") + text;
|
|
212
|
+
}
|
|
213
|
+
case "write": {
|
|
214
|
+
const filePath = shortenPath(args.file_path ?? args.path, "...");
|
|
215
|
+
const content = safeBlock(args.content, "", 2 * 1024);
|
|
216
|
+
const lines = content ? content.split("\n").length : 0;
|
|
217
|
+
return (
|
|
218
|
+
themeFg("muted", "write ") +
|
|
219
|
+
themeFg("accent", filePath) +
|
|
220
|
+
(lines > 1 ? themeFg("dim", ` (${lines} lines)`) : "")
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
case "edit":
|
|
224
|
+
return (
|
|
225
|
+
themeFg("muted", "edit ") +
|
|
226
|
+
themeFg("accent", shortenPath(args.file_path ?? args.path, "..."))
|
|
227
|
+
);
|
|
228
|
+
case "ls":
|
|
229
|
+
return themeFg("muted", "ls ") + themeFg("accent", shortenPath(args.path));
|
|
230
|
+
case "find":
|
|
231
|
+
return (
|
|
232
|
+
themeFg("muted", "find ") +
|
|
233
|
+
themeFg("accent", safeLine(args.pattern, "*", 512)) +
|
|
234
|
+
themeFg("dim", ` in ${shortenPath(args.path)}`)
|
|
235
|
+
);
|
|
236
|
+
case "grep":
|
|
237
|
+
return (
|
|
238
|
+
themeFg("muted", "grep ") +
|
|
239
|
+
themeFg("accent", `/${safeLine(args.pattern, "", 512)}/`) +
|
|
240
|
+
themeFg("dim", ` in ${shortenPath(args.path)}`)
|
|
241
|
+
);
|
|
242
|
+
default: {
|
|
243
|
+
let serialized = "{}";
|
|
244
|
+
try {
|
|
245
|
+
serialized = JSON.stringify(args);
|
|
246
|
+
} catch {
|
|
247
|
+
serialized = "{…}";
|
|
248
|
+
}
|
|
249
|
+
return themeFg("accent", toolName) + themeFg("dim", ` ${safeLine(serialized, "{}", 512)}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
package/src/render.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import * as os from "node:os";
|
|
2
1
|
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
3
2
|
import type { Message } from "@earendil-works/pi-ai";
|
|
4
3
|
import {
|
|
@@ -10,6 +9,7 @@ import {
|
|
|
10
9
|
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
11
10
|
import type { AgentScope, SubagentThinkingLevel } from "./agents.js";
|
|
12
11
|
import { hasUsableAggregator, type SubagentParams } from "./params.js";
|
|
12
|
+
import { expansionHint, formatToolActivity, safeBlock, safeLine } from "./render-common.js";
|
|
13
13
|
import {
|
|
14
14
|
getResultFinalOutput,
|
|
15
15
|
isResultError,
|
|
@@ -17,15 +17,15 @@ import {
|
|
|
17
17
|
type SubagentDetails,
|
|
18
18
|
} from "./runner.js";
|
|
19
19
|
|
|
20
|
-
const COLLAPSED_ITEM_COUNT =
|
|
20
|
+
const COLLAPSED_ITEM_COUNT = 5;
|
|
21
21
|
|
|
22
22
|
function previewTask(task: unknown, maxLength = 40): string {
|
|
23
|
-
|
|
24
|
-
return
|
|
23
|
+
const safe = safeLine(task, "...", 2 * 1024);
|
|
24
|
+
return safe.length > maxLength ? `${safe.slice(0, maxLength)}...` : safe;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function previewAgent(agent: unknown): string {
|
|
28
|
-
return
|
|
28
|
+
return safeLine(agent, "...", 256);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export function formatTokens(count: number): string {
|
|
@@ -59,12 +59,12 @@ export function formatUsageStats(
|
|
|
59
59
|
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
60
60
|
if (usage.contextTokens && usage.contextTokens > 0)
|
|
61
61
|
parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
62
|
+
const safeProvider = actualProvider ? safeLine(actualProvider, "", 256) : undefined;
|
|
63
|
+
const safeModel = actualModel ? safeLine(actualModel, "", 256) : undefined;
|
|
62
64
|
const actual =
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (actual ?? model) parts.push(actual ?? model ?? "");
|
|
67
|
-
if (thinkingLevel) parts.push(`requested-thinking:${thinkingLevel}`);
|
|
65
|
+
safeProvider && safeModel ? `${safeProvider}/${safeModel}` : (safeModel ?? safeProvider);
|
|
66
|
+
if (actual ?? model) parts.push(actual ?? safeLine(model, "", 256));
|
|
67
|
+
if (thinkingLevel) parts.push(`requested-thinking:${safeLine(thinkingLevel, "", 128)}`);
|
|
68
68
|
return parts.join(" ");
|
|
69
69
|
}
|
|
70
70
|
|
|
@@ -83,71 +83,7 @@ function formatToolCall(
|
|
|
83
83
|
args: Record<string, unknown>,
|
|
84
84
|
themeFg: (color: ThemeColor, text: string) => string,
|
|
85
85
|
): string {
|
|
86
|
-
|
|
87
|
-
const home = os.homedir();
|
|
88
|
-
return p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
switch (toolName) {
|
|
92
|
-
case "bash": {
|
|
93
|
-
const command = (args.command as string) || "...";
|
|
94
|
-
const preview = command.length > 60 ? `${command.slice(0, 60)}...` : command;
|
|
95
|
-
return themeFg("muted", "$ ") + themeFg("toolOutput", preview);
|
|
96
|
-
}
|
|
97
|
-
case "read": {
|
|
98
|
-
const rawPath = (args.file_path || args.path || "...") as string;
|
|
99
|
-
const filePath = shortenPath(rawPath);
|
|
100
|
-
const offset = args.offset as number | undefined;
|
|
101
|
-
const limit = args.limit as number | undefined;
|
|
102
|
-
let text = themeFg("accent", filePath);
|
|
103
|
-
if (offset !== undefined || limit !== undefined) {
|
|
104
|
-
const startLine = offset ?? 1;
|
|
105
|
-
const endLine = limit !== undefined ? startLine + limit - 1 : "";
|
|
106
|
-
text += themeFg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
|
|
107
|
-
}
|
|
108
|
-
return themeFg("muted", "read ") + text;
|
|
109
|
-
}
|
|
110
|
-
case "write": {
|
|
111
|
-
const rawPath = (args.file_path || args.path || "...") as string;
|
|
112
|
-
const filePath = shortenPath(rawPath);
|
|
113
|
-
const content = (args.content || "") as string;
|
|
114
|
-
const lines = content.split("\n").length;
|
|
115
|
-
let text = themeFg("muted", "write ") + themeFg("accent", filePath);
|
|
116
|
-
if (lines > 1) text += themeFg("dim", ` (${lines} lines)`);
|
|
117
|
-
return text;
|
|
118
|
-
}
|
|
119
|
-
case "edit": {
|
|
120
|
-
const rawPath = (args.file_path || args.path || "...") as string;
|
|
121
|
-
return themeFg("muted", "edit ") + themeFg("accent", shortenPath(rawPath));
|
|
122
|
-
}
|
|
123
|
-
case "ls": {
|
|
124
|
-
const rawPath = (args.path || ".") as string;
|
|
125
|
-
return themeFg("muted", "ls ") + themeFg("accent", shortenPath(rawPath));
|
|
126
|
-
}
|
|
127
|
-
case "find": {
|
|
128
|
-
const pattern = (args.pattern || "*") as string;
|
|
129
|
-
const rawPath = (args.path || ".") as string;
|
|
130
|
-
return (
|
|
131
|
-
themeFg("muted", "find ") +
|
|
132
|
-
themeFg("accent", pattern) +
|
|
133
|
-
themeFg("dim", ` in ${shortenPath(rawPath)}`)
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
case "grep": {
|
|
137
|
-
const pattern = (args.pattern || "") as string;
|
|
138
|
-
const rawPath = (args.path || ".") as string;
|
|
139
|
-
return (
|
|
140
|
-
themeFg("muted", "grep ") +
|
|
141
|
-
themeFg("accent", `/${pattern}/`) +
|
|
142
|
-
themeFg("dim", ` in ${shortenPath(rawPath)}`)
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
default: {
|
|
146
|
-
const argsStr = JSON.stringify(args);
|
|
147
|
-
const preview = argsStr.length > 50 ? `${argsStr.slice(0, 50)}...` : argsStr;
|
|
148
|
-
return themeFg("accent", toolName) + themeFg("dim", ` ${preview}`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
86
|
+
return formatToolActivity(toolName, args, themeFg);
|
|
151
87
|
}
|
|
152
88
|
|
|
153
89
|
type DisplayItem =
|
|
@@ -160,10 +96,14 @@ function getDisplayItems(messages: Message[]): DisplayItem[] {
|
|
|
160
96
|
if (msg.role === "assistant") {
|
|
161
97
|
for (const part of msg.content) {
|
|
162
98
|
if (part.type === "text") {
|
|
163
|
-
const text = part.text.trim();
|
|
99
|
+
const text = safeBlock(part.text, "", 8 * 1024).trim();
|
|
164
100
|
if (text) items.push({ type: "text", text });
|
|
165
101
|
} else if (part.type === "toolCall") {
|
|
166
|
-
items.push({
|
|
102
|
+
items.push({
|
|
103
|
+
type: "toolCall",
|
|
104
|
+
name: safeLine(part.name, "tool", 256),
|
|
105
|
+
args: part.arguments,
|
|
106
|
+
});
|
|
167
107
|
}
|
|
168
108
|
}
|
|
169
109
|
}
|
|
@@ -181,6 +121,47 @@ function getCollapsedDisplayItems(result: SingleResult): { items: DisplayItem[];
|
|
|
181
121
|
return { items, total: items.length };
|
|
182
122
|
}
|
|
183
123
|
|
|
124
|
+
function sanitizeSingleResultForRender(result: SingleResult): SingleResult {
|
|
125
|
+
return {
|
|
126
|
+
...result,
|
|
127
|
+
agent: safeLine(result.agent, "subagent", 256),
|
|
128
|
+
task: safeBlock(result.task, "", 50 * 1024),
|
|
129
|
+
stderr: safeBlock(result.stderr, "", 8 * 1024),
|
|
130
|
+
model: result.model ? safeLine(result.model, "", 256) : undefined,
|
|
131
|
+
actualProvider: result.actualProvider ? safeLine(result.actualProvider, "", 256) : undefined,
|
|
132
|
+
actualModel: result.actualModel ? safeLine(result.actualModel, "", 256) : undefined,
|
|
133
|
+
stopReason: result.stopReason ? safeLine(result.stopReason, "", 256) : undefined,
|
|
134
|
+
errorMessage: result.errorMessage ? safeBlock(result.errorMessage, "", 8 * 1024) : undefined,
|
|
135
|
+
finalOutput: result.finalOutput ? safeBlock(result.finalOutput, "", 50 * 1024) : undefined,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function renderResultStatus(result: SingleResult, isPartial: boolean): string {
|
|
140
|
+
if (isResultError(result)) {
|
|
141
|
+
return result.stopReason === "aborted" || result.aborted ? "Cancelled" : "Failed";
|
|
142
|
+
}
|
|
143
|
+
return isPartial || result.exitCode === -1 ? "Running" : "Completed";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function coloredStatus(theme: Theme, status: string): string {
|
|
147
|
+
const color: ThemeColor =
|
|
148
|
+
status === "Failed" ? "error" : status === "Completed" ? "success" : "warning";
|
|
149
|
+
return theme.fg(color, status);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function coloredResultStatus(theme: Theme, result: SingleResult, isPartial: boolean): string {
|
|
153
|
+
return coloredStatus(theme, renderResultStatus(result, isPartial));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function formatResultPolicy(result: SingleResult): string {
|
|
157
|
+
if (!result.policy) return "";
|
|
158
|
+
return [
|
|
159
|
+
`inherited: ${result.policy.inherited.map((tool) => safeLine(tool, "", 256)).join(", ") || "none"}`,
|
|
160
|
+
`overridden: ${result.policy.overridden.map((tool) => safeLine(tool, "", 256)).join(", ") || "none"}`,
|
|
161
|
+
`unsupported: ${result.policy.unsupported.map((tool) => safeLine(tool, "", 256)).join(", ") || "none"}`,
|
|
162
|
+
].join("\n");
|
|
163
|
+
}
|
|
164
|
+
|
|
184
165
|
export function renderSubagentCall(args: SubagentParams, theme: Theme) {
|
|
185
166
|
const scope: AgentScope = args.agentScope ?? "user";
|
|
186
167
|
if (args.chain && args.chain.length > 0) {
|
|
@@ -239,11 +220,22 @@ export function renderSubagentResult(
|
|
|
239
220
|
{ expanded, isPartial }: ToolRenderResultOptions,
|
|
240
221
|
theme: Theme,
|
|
241
222
|
) {
|
|
242
|
-
const
|
|
243
|
-
if (!
|
|
223
|
+
const rawDetails = result.details as SubagentDetails | undefined;
|
|
224
|
+
if (!rawDetails || rawDetails.results.length === 0) {
|
|
244
225
|
const text = result.content[0];
|
|
245
|
-
return new Text(
|
|
226
|
+
return new Text(
|
|
227
|
+
text?.type === "text" ? safeBlock(text.text, "(no output)", 8 * 1024) : "(no output)",
|
|
228
|
+
0,
|
|
229
|
+
0,
|
|
230
|
+
);
|
|
246
231
|
}
|
|
232
|
+
const details: SubagentDetails = {
|
|
233
|
+
...rawDetails,
|
|
234
|
+
results: rawDetails.results.map(sanitizeSingleResultForRender),
|
|
235
|
+
aggregator: rawDetails.aggregator
|
|
236
|
+
? sanitizeSingleResultForRender(rawDetails.aggregator)
|
|
237
|
+
: undefined,
|
|
238
|
+
};
|
|
247
239
|
|
|
248
240
|
const mdTheme = getMarkdownTheme();
|
|
249
241
|
|
|
@@ -254,7 +246,8 @@ export function renderSubagentResult(
|
|
|
254
246
|
if (skipped > 0) text += theme.fg("muted", `... ${skipped} earlier items\n`);
|
|
255
247
|
for (const item of toShow) {
|
|
256
248
|
if (item.type === "text") {
|
|
257
|
-
const
|
|
249
|
+
const safeText = safeBlock(item.text, "", 8 * 1024);
|
|
250
|
+
const preview = expanded ? safeText : safeText.split("\n").slice(0, 3).join("\n");
|
|
258
251
|
text += `${theme.fg("toolOutput", preview)}\n`;
|
|
259
252
|
} else {
|
|
260
253
|
text += `${theme.fg("muted", "→ ") + formatToolCall(item.name, item.args, theme.fg.bind(theme))}\n`;
|
|
@@ -276,7 +269,7 @@ export function renderSubagentResult(
|
|
|
276
269
|
|
|
277
270
|
if (expanded) {
|
|
278
271
|
const container = new Container();
|
|
279
|
-
let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
|
|
272
|
+
let header = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${safeLine(r.agentSource, "unknown", 128)})`)} · ${coloredResultStatus(theme, r, isPartial)}`;
|
|
280
273
|
if (isError && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
281
274
|
container.addChild(new Text(header, 0, 0));
|
|
282
275
|
if (isError && r.errorMessage)
|
|
@@ -284,6 +277,12 @@ export function renderSubagentResult(
|
|
|
284
277
|
container.addChild(new Spacer(1));
|
|
285
278
|
container.addChild(new Text(theme.fg("muted", "─── Task ───"), 0, 0));
|
|
286
279
|
container.addChild(new Text(theme.fg("dim", r.task), 0, 0));
|
|
280
|
+
const policy = formatResultPolicy(r);
|
|
281
|
+
if (policy) {
|
|
282
|
+
container.addChild(new Spacer(1));
|
|
283
|
+
container.addChild(new Text(theme.fg("muted", "─── Policy ───"), 0, 0));
|
|
284
|
+
container.addChild(new Text(theme.fg("dim", policy), 0, 0));
|
|
285
|
+
}
|
|
287
286
|
container.addChild(new Spacer(1));
|
|
288
287
|
container.addChild(new Text(theme.fg("muted", "─── Output ───"), 0, 0));
|
|
289
288
|
if (displayItems.length === 0 && !finalOutput) {
|
|
@@ -314,15 +313,16 @@ export function renderSubagentResult(
|
|
|
314
313
|
}
|
|
315
314
|
|
|
316
315
|
const collapsed = getCollapsedDisplayItems(r);
|
|
317
|
-
let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${r.agentSource})`)}`;
|
|
316
|
+
let text = `${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${theme.fg("muted", ` (${safeLine(r.agentSource, "unknown", 128)})`)} · ${coloredResultStatus(theme, r, isPartial)}`;
|
|
318
317
|
if (isError && r.stopReason) text += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
|
|
319
318
|
if (isError && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
320
319
|
if (collapsed.items.length > 0) {
|
|
321
320
|
text += `\n${renderDisplayItems(collapsed.items, COLLAPSED_ITEM_COUNT, collapsed.total)}`;
|
|
322
|
-
if (collapsed.total > COLLAPSED_ITEM_COUNT)
|
|
323
|
-
text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
321
|
+
if (collapsed.total > COLLAPSED_ITEM_COUNT) text += `\n${expansionHint()}`;
|
|
324
322
|
} else if (finalOutput.trim()) {
|
|
325
|
-
|
|
323
|
+
const outputLines = finalOutput.trim().split("\n");
|
|
324
|
+
text += `\n${theme.fg("toolOutput", outputLines.slice(0, 3).join("\n"))}`;
|
|
325
|
+
if (outputLines.length > 3) text += `\n${expansionHint()}`;
|
|
326
326
|
} else if (!isError || !r.errorMessage) {
|
|
327
327
|
text += `\n${theme.fg("muted", isPartial && !isError ? "(running...)" : "(no output)")}`;
|
|
328
328
|
}
|
|
@@ -356,6 +356,11 @@ export function renderSubagentResult(
|
|
|
356
356
|
: successCount === details.results.length
|
|
357
357
|
? theme.fg("success", "✓")
|
|
358
358
|
: theme.fg("error", "✗");
|
|
359
|
+
const overallStatus = currentIsRunning
|
|
360
|
+
? "Running"
|
|
361
|
+
: successCount === details.results.length
|
|
362
|
+
? "Completed"
|
|
363
|
+
: "Failed";
|
|
359
364
|
|
|
360
365
|
if (expanded) {
|
|
361
366
|
const container = new Container();
|
|
@@ -364,7 +369,8 @@ export function renderSubagentResult(
|
|
|
364
369
|
icon +
|
|
365
370
|
" " +
|
|
366
371
|
theme.fg("toolTitle", theme.bold("chain ")) +
|
|
367
|
-
theme.fg("accent", `${successCount}/${details.results.length} steps`)
|
|
372
|
+
theme.fg("accent", `${successCount}/${details.results.length} steps`) +
|
|
373
|
+
` · ${coloredStatus(theme, overallStatus)}`,
|
|
368
374
|
0,
|
|
369
375
|
0,
|
|
370
376
|
),
|
|
@@ -383,12 +389,17 @@ export function renderSubagentResult(
|
|
|
383
389
|
container.addChild(new Spacer(1));
|
|
384
390
|
container.addChild(
|
|
385
391
|
new Text(
|
|
386
|
-
`${theme.fg("muted", `─── Step ${r.step}: `) + theme.fg("accent", r.agent)} ${rIcon}`,
|
|
392
|
+
`${theme.fg("muted", `─── Step ${r.step}: `) + theme.fg("accent", r.agent)} ${rIcon} ${coloredResultStatus(theme, r, currentIsRunning && r === currentResult)}`,
|
|
387
393
|
0,
|
|
388
394
|
0,
|
|
389
395
|
),
|
|
390
396
|
);
|
|
391
397
|
container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
|
|
398
|
+
const policy = formatResultPolicy(r);
|
|
399
|
+
if (policy)
|
|
400
|
+
container.addChild(
|
|
401
|
+
new Text(theme.fg("muted", "Policy:\n") + theme.fg("dim", policy), 0, 0),
|
|
402
|
+
);
|
|
392
403
|
if (rFailed && r.errorMessage)
|
|
393
404
|
container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
|
|
394
405
|
|
|
@@ -429,7 +440,8 @@ export function renderSubagentResult(
|
|
|
429
440
|
icon +
|
|
430
441
|
" " +
|
|
431
442
|
theme.fg("toolTitle", theme.bold("chain ")) +
|
|
432
|
-
theme.fg("accent", `${successCount}/${details.results.length} steps`)
|
|
443
|
+
theme.fg("accent", `${successCount}/${details.results.length} steps`) +
|
|
444
|
+
` · ${coloredStatus(theme, overallStatus)}`;
|
|
433
445
|
for (const r of details.results) {
|
|
434
446
|
const rFailed = isResultError(r);
|
|
435
447
|
const rIcon = rFailed
|
|
@@ -439,7 +451,7 @@ export function renderSubagentResult(
|
|
|
439
451
|
: theme.fg("success", "✓");
|
|
440
452
|
const collapsed = getCollapsedDisplayItems(r);
|
|
441
453
|
const finalOutput = getResultFinalOutput(r).trim();
|
|
442
|
-
text += `\n\n${theme.fg("muted", `─── Step ${r.step}: `)}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
454
|
+
text += `\n\n${theme.fg("muted", `─── Step ${r.step}: `)}${theme.fg("accent", r.agent)} ${rIcon} ${coloredResultStatus(theme, r, currentIsRunning && r === currentResult)}`;
|
|
443
455
|
if (rFailed && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
444
456
|
if (collapsed.items.length > 0)
|
|
445
457
|
text += `\n${renderDisplayItems(collapsed.items, 5, collapsed.total)}`;
|
|
@@ -451,7 +463,7 @@ export function renderSubagentResult(
|
|
|
451
463
|
}
|
|
452
464
|
const usageStr = formatUsageStats(aggregateUsage(details.results));
|
|
453
465
|
if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
|
|
454
|
-
text += `\n${
|
|
466
|
+
text += `\n${expansionHint()}`;
|
|
455
467
|
return new Text(text, 0, 0);
|
|
456
468
|
}
|
|
457
469
|
|
|
@@ -485,12 +497,17 @@ export function renderSubagentResult(
|
|
|
485
497
|
: aggregator
|
|
486
498
|
? `${successCount}/${details.results.length} tasks + fan-in`
|
|
487
499
|
: `${successCount}/${details.results.length} tasks`;
|
|
500
|
+
const overallStatus = isRunning
|
|
501
|
+
? "Running"
|
|
502
|
+
: failCount > 0 || aggregatorFailed
|
|
503
|
+
? "Failed"
|
|
504
|
+
: "Completed";
|
|
488
505
|
|
|
489
506
|
if (expanded && !isRunning) {
|
|
490
507
|
const container = new Container();
|
|
491
508
|
container.addChild(
|
|
492
509
|
new Text(
|
|
493
|
-
`${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`,
|
|
510
|
+
`${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)} · ${coloredStatus(theme, overallStatus)}`,
|
|
494
511
|
0,
|
|
495
512
|
0,
|
|
496
513
|
),
|
|
@@ -508,9 +525,18 @@ export function renderSubagentResult(
|
|
|
508
525
|
|
|
509
526
|
container.addChild(new Spacer(1));
|
|
510
527
|
container.addChild(
|
|
511
|
-
new Text(
|
|
528
|
+
new Text(
|
|
529
|
+
`${theme.fg("muted", "─── ") + theme.fg("accent", r.agent)} ${rIcon} ${coloredResultStatus(theme, r, resultIsRunning(r))}`,
|
|
530
|
+
0,
|
|
531
|
+
0,
|
|
532
|
+
),
|
|
512
533
|
);
|
|
513
534
|
container.addChild(new Text(theme.fg("muted", "Task: ") + theme.fg("dim", r.task), 0, 0));
|
|
535
|
+
const policy = formatResultPolicy(r);
|
|
536
|
+
if (policy)
|
|
537
|
+
container.addChild(
|
|
538
|
+
new Text(theme.fg("muted", "Policy:\n") + theme.fg("dim", policy), 0, 0),
|
|
539
|
+
);
|
|
514
540
|
if (rFailed && r.errorMessage)
|
|
515
541
|
container.addChild(new Text(theme.fg("error", `Error: ${r.errorMessage}`), 0, 0));
|
|
516
542
|
|
|
@@ -550,7 +576,7 @@ export function renderSubagentResult(
|
|
|
550
576
|
container.addChild(new Spacer(1));
|
|
551
577
|
container.addChild(
|
|
552
578
|
new Text(
|
|
553
|
-
`${theme.fg("muted", "─── fan-in → ") + theme.fg("accent", aggregator.agent)} ${rIcon}`,
|
|
579
|
+
`${theme.fg("muted", "─── fan-in → ") + theme.fg("accent", aggregator.agent)} ${rIcon} ${coloredResultStatus(theme, aggregator, aggregatorRunning)}`,
|
|
554
580
|
0,
|
|
555
581
|
0,
|
|
556
582
|
),
|
|
@@ -558,6 +584,11 @@ export function renderSubagentResult(
|
|
|
558
584
|
container.addChild(
|
|
559
585
|
new Text(theme.fg("muted", "Task: ") + theme.fg("dim", aggregator.task), 0, 0),
|
|
560
586
|
);
|
|
587
|
+
const aggregatorPolicy = formatResultPolicy(aggregator);
|
|
588
|
+
if (aggregatorPolicy)
|
|
589
|
+
container.addChild(
|
|
590
|
+
new Text(theme.fg("muted", "Policy:\n") + theme.fg("dim", aggregatorPolicy), 0, 0),
|
|
591
|
+
);
|
|
561
592
|
if (aggregatorFailed && aggregator.errorMessage)
|
|
562
593
|
container.addChild(
|
|
563
594
|
new Text(theme.fg("error", `Error: ${aggregator.errorMessage}`), 0, 0),
|
|
@@ -592,7 +623,7 @@ export function renderSubagentResult(
|
|
|
592
623
|
}
|
|
593
624
|
|
|
594
625
|
// Collapsed view (or still running)
|
|
595
|
-
let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)}`;
|
|
626
|
+
let text = `${icon} ${theme.fg("toolTitle", theme.bold("parallel "))}${theme.fg("accent", status)} · ${coloredStatus(theme, overallStatus)}`;
|
|
596
627
|
for (const r of details.results) {
|
|
597
628
|
const rFailed = isResultError(r);
|
|
598
629
|
const rRunning = resultIsRunning(r);
|
|
@@ -603,7 +634,7 @@ export function renderSubagentResult(
|
|
|
603
634
|
: theme.fg("success", "✓");
|
|
604
635
|
const collapsed = getCollapsedDisplayItems(r);
|
|
605
636
|
const finalOutput = getResultFinalOutput(r).trim();
|
|
606
|
-
text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
637
|
+
text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon} ${coloredResultStatus(theme, r, rRunning)}`;
|
|
607
638
|
if (rFailed && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
608
639
|
if (collapsed.items.length > 0)
|
|
609
640
|
text += `\n${renderDisplayItems(collapsed.items, 5, collapsed.total)}`;
|
|
@@ -620,7 +651,7 @@ export function renderSubagentResult(
|
|
|
620
651
|
: theme.fg("success", "✓");
|
|
621
652
|
const collapsed = getCollapsedDisplayItems(aggregator);
|
|
622
653
|
const finalOutput = getResultFinalOutput(aggregator).trim();
|
|
623
|
-
text += `\n\n${theme.fg("muted", "─── fan-in → ")}${theme.fg("accent", aggregator.agent)} ${rIcon}`;
|
|
654
|
+
text += `\n\n${theme.fg("muted", "─── fan-in → ")}${theme.fg("accent", aggregator.agent)} ${rIcon} ${coloredResultStatus(theme, aggregator, aggregatorRunning)}`;
|
|
624
655
|
if (aggregatorFailed && aggregator.errorMessage)
|
|
625
656
|
text += `\n${theme.fg("error", `Error: ${aggregator.errorMessage}`)}`;
|
|
626
657
|
if (collapsed.items.length > 0)
|
|
@@ -636,10 +667,14 @@ export function renderSubagentResult(
|
|
|
636
667
|
const usageStr = formatUsageStats(aggregateUsage(usageResults));
|
|
637
668
|
if (usageStr) text += `\n\n${theme.fg("dim", `Total: ${usageStr}`)}`;
|
|
638
669
|
}
|
|
639
|
-
if (!expanded) text += `\n${
|
|
670
|
+
if (!expanded) text += `\n${expansionHint()}`;
|
|
640
671
|
return new Text(text, 0, 0);
|
|
641
672
|
}
|
|
642
673
|
|
|
643
674
|
const text = result.content[0];
|
|
644
|
-
return new Text(
|
|
675
|
+
return new Text(
|
|
676
|
+
text?.type === "text" ? safeBlock(text.text, "(no output)", 8 * 1024) : "(no output)",
|
|
677
|
+
0,
|
|
678
|
+
0,
|
|
679
|
+
);
|
|
645
680
|
}
|