@hheei/pi-ext-ui 0.1.0 → 0.2.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 +2 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/nested-calls-view.d.ts +28 -0
- package/dist/nested-calls-view.js +406 -0
- package/dist/payload-parser.d.ts +120 -0
- package/dist/payload-parser.js +644 -0
- package/dist/renderer.d.ts +2 -26
- package/dist/renderer.js +333 -616
- package/dist/tool-view.d.ts +50 -3
- package/dist/tool-view.js +209 -55
- package/dist/tracker.d.ts +26 -0
- package/dist/tracker.js +81 -0
- package/package.json +51 -49
- package/dist/collapsible-tool-frame.d.ts +0 -1
- package/dist/collapsible-tool-frame.js +0 -1
package/dist/renderer.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { getLanguageFromPath, highlightCode, } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import { stripTerminalSequences } from "@earendil-works/pi-tui";
|
|
3
|
+
import { agentResultText, getRegisteredToolPresentationProvider, getToolPresentationProvider, } from "@hheei/pi-ext-core";
|
|
2
4
|
import { applyPatchGlyph } from "./apply-patch-glyph.js";
|
|
3
5
|
import { previewV4aOperations } from "./apply-patch-preview.js";
|
|
4
6
|
import { applyPatchFacts, formatApplyPatchOperationRow, } from "./apply-patch-view.js";
|
|
5
7
|
import { DiffView, diffStats } from "./diff-view.js";
|
|
6
|
-
import {
|
|
8
|
+
import { NestedCallsView } from "./nested-calls-view.js";
|
|
9
|
+
import { formatToolHeaderText, ToolView, } from "./tool-view.js";
|
|
10
|
+
import { getCapturedNestedCalls, recordToolExecutionEnd, recordToolExecutionStart, } from "./tracker.js";
|
|
7
11
|
/** Tools with a pi-ext-ui-owned renderer. Other tools stay on Pi's renderer. */
|
|
8
12
|
export const SPECIALIZED_TOOL_RENDERERS = [
|
|
9
13
|
"edit",
|
|
@@ -14,217 +18,42 @@ export const SPECIALIZED_TOOL_RENDERERS = [
|
|
|
14
18
|
];
|
|
15
19
|
/** Web-access tools use the shared folding shell while retaining their native body renderer. */
|
|
16
20
|
export const WEB_ACCESS_TOOLS = ["web_search", "web_fetch", "web_open"];
|
|
17
|
-
|
|
21
|
+
import { APPLY_PATCH_TOOL_NAME, callStatsParts, EDIT_TOOL_NAME, editDiffSource, editPath, estimateStreamingTokens, extractHeavyInputCommand, extractNestedCalls, extractResultText, extractSummaryArg, formatCostUsd, formatDurationMs, formatTimeoutFact, inlinePresentation, isRecord, lineHunks, PYTHON_EVAL_TOOL_NAME, patchSummary, patchTextForCopy, pythonEvalFacts, resolveContainerStatus, toolPath, WRITE_TOOL_NAME, writePresentation, } from "./payload-parser.js";
|
|
22
|
+
export { estimateStreamingTokens, inlinePresentation, } from "./payload-parser.js";
|
|
18
23
|
const APPLY_PATCH_PREVIEW_STATE_KEY = "__piExtUiApplyPatchPreview";
|
|
19
|
-
const PYTHON_EVAL_TOOL_NAME = "python_eval";
|
|
20
24
|
const PYTHON_EVAL_TOOL_TITLE = "python_eval";
|
|
21
|
-
const PYTHON_EVAL_DETAILS_FORMAT = "pi-ext-tools-python-eval";
|
|
22
|
-
const EDIT_TOOL_NAME = "edit";
|
|
23
|
-
const WRITE_TOOL_NAME = "write";
|
|
24
|
-
const EDIT_VIEW_KEY = "__piExtToolsEditView";
|
|
25
|
-
const WRITE_VIEW_KEY = "__piExtToolsWriteView";
|
|
26
25
|
const SPECIALIZED_TOOL_SET = new Set(SPECIALIZED_TOOL_RENDERERS);
|
|
27
26
|
const WEB_ACCESS_TOOL_SET = new Set(WEB_ACCESS_TOOLS);
|
|
28
27
|
const CALL_VIEW_STATE_KEY = "__piExtUiCallView";
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return 0;
|
|
37
|
-
try {
|
|
38
|
-
const serialized = typeof args === "string" ? args : JSON.stringify(args);
|
|
39
|
-
if (!serialized || serialized === "{}")
|
|
40
|
-
return 0;
|
|
41
|
-
return Math.max(1, Math.ceil(serialized.length / 4));
|
|
42
|
-
}
|
|
43
|
-
catch {
|
|
44
|
-
return 0;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
function formatDurationMs(ms) {
|
|
48
|
-
if (ms === undefined || !Number.isFinite(ms))
|
|
49
|
-
return "";
|
|
50
|
-
return ms < 1000 ? `${String(Math.round(ms))}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
51
|
-
}
|
|
52
|
-
function formatCostUsd(cost) {
|
|
53
|
-
if (cost === undefined || !Number.isFinite(cost) || cost <= 0)
|
|
54
|
-
return "";
|
|
55
|
-
return `$${cost >= 0.01 ? cost.toFixed(2) : cost.toPrecision(2)}`;
|
|
56
|
-
}
|
|
57
|
-
export function extractCodemodeHeaderFacts(args) {
|
|
58
|
-
if (!isRecord(args))
|
|
59
|
-
return "";
|
|
60
|
-
const options = {};
|
|
61
|
-
for (const [key, value] of Object.entries(args)) {
|
|
62
|
-
if (key !== "code" && value !== undefined && value !== null) {
|
|
63
|
-
options[key] = value;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
if (typeof args.code === "string") {
|
|
67
|
-
const firstLine = args.code.split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
68
|
-
if (firstLine.startsWith(CODEMODE_OPTIONS_PREFIX)) {
|
|
69
|
-
try {
|
|
70
|
-
const parsed = JSON.parse(firstLine.slice(CODEMODE_OPTIONS_PREFIX.length).trim());
|
|
71
|
-
if (isRecord(parsed)) {
|
|
72
|
-
Object.assign(options, parsed);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
catch { }
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
const facts = [];
|
|
79
|
-
const timeoutMs = typeof options.timeout_ms === "number"
|
|
80
|
-
? options.timeout_ms
|
|
81
|
-
: typeof options.timeout === "number"
|
|
82
|
-
? options.timeout * 1000
|
|
83
|
-
: undefined;
|
|
84
|
-
if (timeoutMs !== undefined) {
|
|
85
|
-
const label = timeoutMs >= 1000 ? `${String(Math.round(timeoutMs / 1000))}s` : `${String(timeoutMs)}ms`;
|
|
86
|
-
facts.push(`(timeout ${label})`);
|
|
87
|
-
}
|
|
88
|
-
if (typeof options.max_output_tokens === "number") {
|
|
89
|
-
facts.push(`(tokens ${String(options.max_output_tokens)})`);
|
|
90
|
-
}
|
|
91
|
-
if (typeof options.target === "string" && options.target !== "local") {
|
|
92
|
-
facts.push(`(${options.target})`);
|
|
93
|
-
}
|
|
94
|
-
return facts.join(" ");
|
|
95
|
-
}
|
|
96
|
-
function extractSummaryArg(toolName, args) {
|
|
97
|
-
if (toolName === "codemode") {
|
|
98
|
-
return extractCodemodeHeaderFacts(args);
|
|
99
|
-
}
|
|
100
|
-
if (!isRecord(args))
|
|
101
|
-
return "";
|
|
102
|
-
for (const key of [
|
|
103
|
-
"query",
|
|
104
|
-
"queries",
|
|
105
|
-
"url",
|
|
106
|
-
"urls",
|
|
107
|
-
"source",
|
|
108
|
-
"claim",
|
|
109
|
-
"id",
|
|
110
|
-
"search_id",
|
|
111
|
-
"topic",
|
|
112
|
-
"path",
|
|
113
|
-
"pattern",
|
|
114
|
-
]) {
|
|
115
|
-
const val = args[key];
|
|
116
|
-
if (typeof val === "string" && val.trim() !== "") {
|
|
117
|
-
return stripTerminalSequences(val.trim());
|
|
118
|
-
}
|
|
119
|
-
if (Array.isArray(val) && val.length > 0 && typeof val[0] === "string") {
|
|
120
|
-
const first = stripTerminalSequences(val[0].trim());
|
|
121
|
-
return val.length > 1 ? `${first} (+${String(val.length - 1)})` : first;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return "";
|
|
125
|
-
}
|
|
126
|
-
function extractHeavyInputCommand(toolName, args) {
|
|
127
|
-
if (!isRecord(args))
|
|
128
|
-
return undefined;
|
|
129
|
-
if (toolName === "bash" && typeof args.command === "string") {
|
|
130
|
-
return args.command.trim();
|
|
131
|
-
}
|
|
132
|
-
if (toolName === "python_eval" && typeof args.code === "string") {
|
|
133
|
-
return args.code.trim();
|
|
134
|
-
}
|
|
135
|
-
if (toolName === "codemode" && typeof args.code === "string") {
|
|
136
|
-
return args.code.trim();
|
|
137
|
-
}
|
|
28
|
+
function detectCodeLanguage(toolName) {
|
|
29
|
+
if (toolName === "bash")
|
|
30
|
+
return "bash";
|
|
31
|
+
if (toolName === "python_eval")
|
|
32
|
+
return "python";
|
|
33
|
+
if (toolName === "codemode")
|
|
34
|
+
return "javascript";
|
|
138
35
|
return undefined;
|
|
139
36
|
}
|
|
140
|
-
function
|
|
141
|
-
if (result === undefined || !isRecord(result.details))
|
|
142
|
-
return [];
|
|
143
|
-
const rawList = Array.isArray(result.details.calls)
|
|
144
|
-
? result.details.calls
|
|
145
|
-
: Array.isArray(result.details.nestedCalls)
|
|
146
|
-
? result.details.nestedCalls
|
|
147
|
-
: undefined;
|
|
148
|
-
if (rawList === undefined)
|
|
149
|
-
return [];
|
|
150
|
-
const items = [];
|
|
151
|
-
for (const entry of rawList) {
|
|
152
|
-
if (!isRecord(entry))
|
|
153
|
-
continue;
|
|
154
|
-
const name = typeof entry.name === "string"
|
|
155
|
-
? entry.name
|
|
156
|
-
: typeof entry.toolName === "string"
|
|
157
|
-
? entry.toolName
|
|
158
|
-
: undefined;
|
|
159
|
-
if (name === undefined)
|
|
160
|
-
continue;
|
|
161
|
-
const rawStatus = entry.status;
|
|
162
|
-
const status = rawStatus === "running" ||
|
|
163
|
-
rawStatus === "ok" ||
|
|
164
|
-
rawStatus === "error" ||
|
|
165
|
-
rawStatus === "cancelled"
|
|
166
|
-
? rawStatus
|
|
167
|
-
: undefined;
|
|
168
|
-
const summary = typeof entry.args === "string" && entry.args.trim() !== ""
|
|
169
|
-
? entry.args
|
|
170
|
-
: typeof entry.summary === "string"
|
|
171
|
-
? entry.summary
|
|
172
|
-
: undefined;
|
|
173
|
-
items.push({
|
|
174
|
-
toolName: name,
|
|
175
|
-
summary,
|
|
176
|
-
status,
|
|
177
|
-
isError: entry.isError === true || status === "error",
|
|
178
|
-
isWarning: entry.isWarning === true || status === "cancelled",
|
|
179
|
-
durationMs: typeof entry.durationMs === "number" ? entry.durationMs : undefined,
|
|
180
|
-
cost: typeof entry.cost === "number" ? entry.cost : undefined,
|
|
181
|
-
error: typeof entry.error === "string" ? entry.error : undefined,
|
|
182
|
-
});
|
|
183
|
-
}
|
|
184
|
-
return items;
|
|
185
|
-
}
|
|
186
|
-
function extractResultText(toolName, result) {
|
|
187
|
-
const parts = [];
|
|
188
|
-
for (const block of result.content) {
|
|
189
|
-
if (block.type === "text" && block.text.trim() !== "") {
|
|
190
|
-
parts.push(stripTerminalSequences(block.text));
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
const joined = parts.join("\n");
|
|
194
|
-
if (toolName === "codemode") {
|
|
195
|
-
const match = CODEMODE_SCRIPT_HEADER.exec(joined);
|
|
196
|
-
const wallTimeSeconds = match?.[1] !== undefined ? Number.parseFloat(match[1]) : undefined;
|
|
197
|
-
const cleaned = joined.replace(CODEMODE_SCRIPT_HEADER, "").trim();
|
|
198
|
-
return { outputText: cleaned, wallTimeSeconds };
|
|
199
|
-
}
|
|
200
|
-
return { outputText: joined, wallTimeSeconds: undefined };
|
|
201
|
-
}
|
|
202
|
-
function callStatsParts(total, failCount) {
|
|
203
|
-
if (failCount > 0) {
|
|
204
|
-
return [`${String(total - failCount)} ok`, `${String(failCount)} failed`];
|
|
205
|
-
}
|
|
206
|
-
return [`${String(total)} ${total === 1 ? "call" : "calls"}`];
|
|
207
|
-
}
|
|
208
|
-
function resolveContainerStatus(params) {
|
|
209
|
-
if (params.selfFailed)
|
|
210
|
-
return "error";
|
|
211
|
-
if (params.hasChildren)
|
|
212
|
-
return "none";
|
|
213
|
-
if (params.isPartial)
|
|
214
|
-
return "running";
|
|
215
|
-
return params.isError ? "error" : "ok";
|
|
216
|
-
}
|
|
217
|
-
function createInputCodeSection(toolName, args, theme) {
|
|
37
|
+
function createInputCodeSection(toolName, args, theme, railToken = "muted") {
|
|
218
38
|
const raw = extractHeavyInputCommand(toolName, args);
|
|
219
39
|
if (raw === undefined || raw === "")
|
|
220
40
|
return undefined;
|
|
221
41
|
const isShell = toolName === "bash";
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
.map((line, i) =>
|
|
227
|
-
|
|
42
|
+
const clean = raw.replace(/\r/g, "").replace(/\t/g, " ");
|
|
43
|
+
const lang = detectCodeLanguage(toolName);
|
|
44
|
+
let lines;
|
|
45
|
+
if (isShell) {
|
|
46
|
+
lines = clean.split("\n").map((line, i) => (i === 0 ? `$ ${line}` : ` ${line}`));
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
try {
|
|
50
|
+
lines = highlightCode(clean, lang);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
lines = clean.split("\n").map((line) => theme.fg("mdCode", line));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { railToken, content: lines };
|
|
228
57
|
}
|
|
229
58
|
function formatArgsPreview(args) {
|
|
230
59
|
if (args === undefined || args === null)
|
|
@@ -238,41 +67,25 @@ function formatArgsPreview(args) {
|
|
|
238
67
|
return "";
|
|
239
68
|
}
|
|
240
69
|
}
|
|
241
|
-
function nestedStatusGlyph(status, theme) {
|
|
242
|
-
switch (status) {
|
|
243
|
-
case "streaming":
|
|
244
|
-
return theme.fg("muted", "⋯");
|
|
245
|
-
case "running":
|
|
246
|
-
return theme.fg("accent", "◐");
|
|
247
|
-
case "ok":
|
|
248
|
-
return theme.fg("success", "✓");
|
|
249
|
-
case "warn":
|
|
250
|
-
return theme.fg("warning", "!");
|
|
251
|
-
case "error":
|
|
252
|
-
return theme.fg("error", "✗");
|
|
253
|
-
case "none":
|
|
254
|
-
return "";
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
70
|
/**
|
|
258
71
|
* The host stacks a tool execution's call and result views. Following the
|
|
259
72
|
* official edit renderer, the call view is stored in the shared renderer
|
|
260
73
|
* state so the result renderer can hide it in place within the same
|
|
261
74
|
* updateDisplay pass — no draw-time flags needed.
|
|
75
|
+
*
|
|
76
|
+
* The call row is left visible until a result view exists: hiding it here would
|
|
77
|
+
* blank the whole execution for as long as the tool produces no result yet.
|
|
262
78
|
*/
|
|
263
|
-
function rememberCallView(context, view
|
|
79
|
+
function rememberCallView(context, view) {
|
|
264
80
|
if (isRecord(context.state)) {
|
|
265
81
|
context.state[CALL_VIEW_STATE_KEY] = view;
|
|
266
82
|
}
|
|
267
|
-
// A container tool's own row yields as soon as execution starts: nested rows and output
|
|
268
|
-
// carry the progress, so a spinner row on top would only duplicate them.
|
|
269
|
-
if ((toolName === "codemode" ||
|
|
270
|
-
toolName === PYTHON_EVAL_TOOL_NAME ||
|
|
271
|
-
toolName === APPLY_PATCH_TOOL_NAME) &&
|
|
272
|
-
context.executionStarted) {
|
|
273
|
-
view.hide();
|
|
274
|
-
}
|
|
275
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* A result view takes over from the call view. A container tool yields on any result,
|
|
86
|
+
* because its own row would duplicate the nested rows and output it now carries; every
|
|
87
|
+
* other tool keeps its call row until the final result.
|
|
88
|
+
*/
|
|
276
89
|
function hideCallView(context, toolName, isPartial) {
|
|
277
90
|
if (toolName !== "codemode" &&
|
|
278
91
|
toolName !== PYTHON_EVAL_TOOL_NAME &&
|
|
@@ -283,298 +96,6 @@ function hideCallView(context, toolName, isPartial) {
|
|
|
283
96
|
if (view instanceof ToolView)
|
|
284
97
|
view.hide();
|
|
285
98
|
}
|
|
286
|
-
function toolPath(args) {
|
|
287
|
-
let value = args;
|
|
288
|
-
if (typeof value === "string") {
|
|
289
|
-
try {
|
|
290
|
-
value = JSON.parse(value);
|
|
291
|
-
}
|
|
292
|
-
catch {
|
|
293
|
-
return "";
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
if (!isRecord(value))
|
|
297
|
-
return "";
|
|
298
|
-
return typeof value.path === "string" ? value.path.trim() : "";
|
|
299
|
-
}
|
|
300
|
-
function editPath(args) {
|
|
301
|
-
return toolPath(args);
|
|
302
|
-
}
|
|
303
|
-
function numericField(value, key) {
|
|
304
|
-
return isRecord(value) && typeof value[key] === "number" ? value[key] : undefined;
|
|
305
|
-
}
|
|
306
|
-
function writeStoredDiff(lines) {
|
|
307
|
-
const hunks = [];
|
|
308
|
-
let hunk = [];
|
|
309
|
-
const finish = () => {
|
|
310
|
-
if (hunk.length > 0)
|
|
311
|
-
hunks.push(hunk);
|
|
312
|
-
hunk = [];
|
|
313
|
-
};
|
|
314
|
-
for (const entry of lines) {
|
|
315
|
-
if (!isRecord(entry))
|
|
316
|
-
return undefined;
|
|
317
|
-
if (entry.type === "sep") {
|
|
318
|
-
finish();
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
if (entry.type !== "add" && entry.type !== "del" && entry.type !== "ctx")
|
|
322
|
-
return undefined;
|
|
323
|
-
if (typeof entry.content !== "string")
|
|
324
|
-
return undefined;
|
|
325
|
-
const line = entry.type === "del" ? entry.oldNum : entry.newNum;
|
|
326
|
-
if (typeof line !== "number")
|
|
327
|
-
return undefined;
|
|
328
|
-
hunk.push({ kind: entry.type, line, text: entry.content });
|
|
329
|
-
}
|
|
330
|
-
finish();
|
|
331
|
-
return hunks.length === 0
|
|
332
|
-
? undefined
|
|
333
|
-
: { kind: "hunks", hunks: hunks.map((lines) => ({ lines })) };
|
|
334
|
-
}
|
|
335
|
-
/** Uses only the baseline and content already stored by the write tool. */
|
|
336
|
-
function writePresentation(args, details) {
|
|
337
|
-
const view = isRecord(details) ? details[WRITE_VIEW_KEY] : undefined;
|
|
338
|
-
if (!isRecord(view))
|
|
339
|
-
return undefined;
|
|
340
|
-
if (view.kind === "noChange")
|
|
341
|
-
return { kind: "noChange" };
|
|
342
|
-
if (view.kind === "diff") {
|
|
343
|
-
const stored = Array.isArray(view.lines) ? writeStoredDiff(view.lines) : undefined;
|
|
344
|
-
if (stored === undefined)
|
|
345
|
-
return undefined;
|
|
346
|
-
const source = stored;
|
|
347
|
-
const stats = diffStats(source);
|
|
348
|
-
return stats === undefined
|
|
349
|
-
? undefined
|
|
350
|
-
: {
|
|
351
|
-
kind: "diff",
|
|
352
|
-
source,
|
|
353
|
-
added: numericField(view, "added") ?? stats.added,
|
|
354
|
-
removed: numericField(view, "removed") ?? stats.removed,
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
if (view.kind !== "new" && view.kind !== "replace")
|
|
358
|
-
return undefined;
|
|
359
|
-
const content = typeof view.content === "string"
|
|
360
|
-
? view.content
|
|
361
|
-
: isRecord(args) && typeof args.content === "string"
|
|
362
|
-
? args.content
|
|
363
|
-
: undefined;
|
|
364
|
-
if (content === undefined)
|
|
365
|
-
return undefined;
|
|
366
|
-
const lines = numericField(view, "lines") ?? content.split("\n").length;
|
|
367
|
-
return {
|
|
368
|
-
kind: "content",
|
|
369
|
-
label: view.kind === "new" ? `new file (${String(lines)} lines)` : `wrote (${String(lines)} lines)`,
|
|
370
|
-
content,
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
function fileToolPresentation(toolName, args, details, isError) {
|
|
374
|
-
const path = toolPath(args);
|
|
375
|
-
if (toolName === "read") {
|
|
376
|
-
if (path === "")
|
|
377
|
-
return undefined;
|
|
378
|
-
return { summary: `read ${path}`, status: isError ? "error" : "ok" };
|
|
379
|
-
}
|
|
380
|
-
if (toolName === WRITE_TOOL_NAME) {
|
|
381
|
-
const presentation = isError ? undefined : writePresentation(args, details);
|
|
382
|
-
const delta = presentation?.kind === "diff"
|
|
383
|
-
? ` (+${String(presentation.added)} -${String(presentation.removed)})`
|
|
384
|
-
: presentation?.kind === "content"
|
|
385
|
-
? ` (${presentation.label})`
|
|
386
|
-
: presentation?.kind === "noChange"
|
|
387
|
-
? " (no changes)"
|
|
388
|
-
: "";
|
|
389
|
-
if (path === "" && delta === "")
|
|
390
|
-
return undefined;
|
|
391
|
-
return {
|
|
392
|
-
summary: `${WRITE_TOOL_NAME} ${path}${delta}`.trim(),
|
|
393
|
-
status: isError ? "error" : presentation?.kind === "noChange" ? "none" : "ok",
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
return undefined;
|
|
397
|
-
}
|
|
398
|
-
/** Pi native edit persists a unified patch plus the same change as a numbered display diff. */
|
|
399
|
-
function editDiffSource(details) {
|
|
400
|
-
if (!isRecord(details))
|
|
401
|
-
return undefined;
|
|
402
|
-
if (typeof details.patch === "string" && details.patch.trim() !== "") {
|
|
403
|
-
return { kind: "unifiedPatch", text: details.patch };
|
|
404
|
-
}
|
|
405
|
-
if (typeof details.diff === "string" && details.diff.trim() !== "") {
|
|
406
|
-
return displayDiffSource(details.diff);
|
|
407
|
-
}
|
|
408
|
-
return editViewSource(details[EDIT_VIEW_KEY]);
|
|
409
|
-
}
|
|
410
|
-
function displayDiffSource(diff) {
|
|
411
|
-
const hunks = [];
|
|
412
|
-
let current = [];
|
|
413
|
-
let pendingBreak = false;
|
|
414
|
-
for (const raw of diff.split(/\r?\n/)) {
|
|
415
|
-
if (raw === "")
|
|
416
|
-
continue;
|
|
417
|
-
const sign = raw.charAt(0);
|
|
418
|
-
const match = /^ *(\d+)? ?(.*)$/.exec(raw.slice(1));
|
|
419
|
-
const lineNumber = match?.[1];
|
|
420
|
-
if (lineNumber === undefined) {
|
|
421
|
-
pendingBreak = true;
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
if (pendingBreak && current.length > 0) {
|
|
425
|
-
hunks.push(current);
|
|
426
|
-
current = [];
|
|
427
|
-
}
|
|
428
|
-
pendingBreak = false;
|
|
429
|
-
const kind = sign === "+" ? "add" : sign === "-" ? "del" : "ctx";
|
|
430
|
-
current.push({ kind, line: Number(lineNumber), text: match?.[2] ?? "" });
|
|
431
|
-
}
|
|
432
|
-
if (current.length === 0)
|
|
433
|
-
return undefined;
|
|
434
|
-
hunks.push(current);
|
|
435
|
-
return { kind: "hunks", hunks: hunks.map((lines) => ({ lines })) };
|
|
436
|
-
}
|
|
437
|
-
function editViewSource(view) {
|
|
438
|
-
const hunks = editViewHunks(view);
|
|
439
|
-
return hunks.length === 0 ? undefined : { kind: "hunks", hunks };
|
|
440
|
-
}
|
|
441
|
-
function editViewHunks(view) {
|
|
442
|
-
if (!isRecord(view))
|
|
443
|
-
return [];
|
|
444
|
-
const ops = view.kind === "single" && isRecord(view.op)
|
|
445
|
-
? [view.op]
|
|
446
|
-
: view.kind === "multi" && Array.isArray(view.ops)
|
|
447
|
-
? view.ops.filter(isRecord)
|
|
448
|
-
: [];
|
|
449
|
-
return ops.flatMap((op) => hunksForEditOp(op));
|
|
450
|
-
}
|
|
451
|
-
function hunksForEditOp(op) {
|
|
452
|
-
if (typeof op.oldContent !== "string" || typeof op.newContent !== "string")
|
|
453
|
-
return [];
|
|
454
|
-
const startLine = typeof op.startLine === "number" && op.startLine > 0 ? op.startLine : 1;
|
|
455
|
-
return lineHunks(op.oldContent.replace(/\r\n/g, "\n"), op.newContent.replace(/\r\n/g, "\n"), startLine);
|
|
456
|
-
}
|
|
457
|
-
function splitFragment(text) {
|
|
458
|
-
if (text === "")
|
|
459
|
-
return [];
|
|
460
|
-
const lines = text.split("\n");
|
|
461
|
-
if (lines.at(-1) === "")
|
|
462
|
-
lines.pop();
|
|
463
|
-
return lines;
|
|
464
|
-
}
|
|
465
|
-
/** Line LCS of one stored before/after fragment. No file context is invented. */
|
|
466
|
-
function lineHunks(before, after, startLine) {
|
|
467
|
-
const oldLines = splitFragment(before);
|
|
468
|
-
const newLines = splitFragment(after);
|
|
469
|
-
const oldCount = oldLines.length;
|
|
470
|
-
const newCount = newLines.length;
|
|
471
|
-
const lengths = Array.from({ length: oldCount + 1 }, () => Array(newCount + 1).fill(0));
|
|
472
|
-
for (let oldIndex = oldCount - 1; oldIndex >= 0; oldIndex -= 1) {
|
|
473
|
-
const row = lengths[oldIndex];
|
|
474
|
-
const next = lengths[oldIndex + 1];
|
|
475
|
-
if (row === undefined || next === undefined)
|
|
476
|
-
continue;
|
|
477
|
-
for (let newIndex = newCount - 1; newIndex >= 0; newIndex -= 1) {
|
|
478
|
-
row[newIndex] =
|
|
479
|
-
oldLines[oldIndex] === newLines[newIndex]
|
|
480
|
-
? (next[newIndex + 1] ?? 0) + 1
|
|
481
|
-
: Math.max(next[newIndex] ?? 0, row[newIndex + 1] ?? 0);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
const lines = [];
|
|
485
|
-
let oldIndex = 0;
|
|
486
|
-
let newIndex = 0;
|
|
487
|
-
let oldLine = startLine;
|
|
488
|
-
let newLine = startLine;
|
|
489
|
-
while (oldIndex < oldCount || newIndex < newCount) {
|
|
490
|
-
const row = lengths[oldIndex];
|
|
491
|
-
const next = lengths[oldIndex + 1];
|
|
492
|
-
if (oldIndex < oldCount && newIndex < newCount && oldLines[oldIndex] === newLines[newIndex]) {
|
|
493
|
-
lines.push({ kind: "ctx", line: newLine, text: oldLines[oldIndex] ?? "" });
|
|
494
|
-
oldIndex += 1;
|
|
495
|
-
newIndex += 1;
|
|
496
|
-
oldLine += 1;
|
|
497
|
-
newLine += 1;
|
|
498
|
-
}
|
|
499
|
-
else if (newIndex < newCount &&
|
|
500
|
-
(oldIndex === oldCount || (row?.[newIndex + 1] ?? 0) > (next?.[newIndex] ?? 0))) {
|
|
501
|
-
lines.push({ kind: "add", line: newLine, text: newLines[newIndex] ?? "" });
|
|
502
|
-
newIndex += 1;
|
|
503
|
-
newLine += 1;
|
|
504
|
-
}
|
|
505
|
-
else {
|
|
506
|
-
lines.push({ kind: "del", line: oldLine, text: oldLines[oldIndex] ?? "" });
|
|
507
|
-
oldIndex += 1;
|
|
508
|
-
oldLine += 1;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
const changed = lines.some((line) => line.kind !== "ctx");
|
|
512
|
-
return changed ? [{ lines }] : [];
|
|
513
|
-
}
|
|
514
|
-
function patchTextForCopy(details) {
|
|
515
|
-
if (isRecord(details) && typeof details.patch === "string")
|
|
516
|
-
return details.patch;
|
|
517
|
-
const hunks = editViewHunks(isRecord(details) ? details[EDIT_VIEW_KEY] : undefined);
|
|
518
|
-
if (hunks.length === 0)
|
|
519
|
-
return undefined;
|
|
520
|
-
return (hunks
|
|
521
|
-
.map((hunk) => {
|
|
522
|
-
let oldStart = 0;
|
|
523
|
-
let newStart = 0;
|
|
524
|
-
let oldCount = 0;
|
|
525
|
-
let newCount = 0;
|
|
526
|
-
const body = hunk.lines.map((line) => {
|
|
527
|
-
if (line.kind !== "add") {
|
|
528
|
-
oldCount += 1;
|
|
529
|
-
if (oldStart === 0)
|
|
530
|
-
oldStart = line.line;
|
|
531
|
-
}
|
|
532
|
-
if (line.kind !== "del") {
|
|
533
|
-
newCount += 1;
|
|
534
|
-
if (newStart === 0)
|
|
535
|
-
newStart = line.line;
|
|
536
|
-
}
|
|
537
|
-
const sign = line.kind === "add" ? "+" : line.kind === "del" ? "-" : " ";
|
|
538
|
-
return sign + line.text;
|
|
539
|
-
});
|
|
540
|
-
if (body.length === 0)
|
|
541
|
-
return "";
|
|
542
|
-
const header = "@@ -" +
|
|
543
|
-
String(oldStart || 1) +
|
|
544
|
-
"," +
|
|
545
|
-
String(oldCount) +
|
|
546
|
-
" +" +
|
|
547
|
-
String(newStart || 1) +
|
|
548
|
-
"," +
|
|
549
|
-
String(newCount) +
|
|
550
|
-
" @@";
|
|
551
|
-
return [header, ...body].join("\n");
|
|
552
|
-
})
|
|
553
|
-
.filter((hunk) => hunk !== "")
|
|
554
|
-
.join("\n") || undefined);
|
|
555
|
-
}
|
|
556
|
-
/**
|
|
557
|
-
* One compact nested summary for a file tool. Reports the delta only when the
|
|
558
|
-
* caller actually has result details; a codemode nested record keeps arguments
|
|
559
|
-
* alone, so it degrades to `<tool> <path>` instead of inventing counts.
|
|
560
|
-
*/
|
|
561
|
-
export function inlinePresentation(toolName, args, details, isError) {
|
|
562
|
-
const fileTool = fileToolPresentation(toolName, args, details, isError);
|
|
563
|
-
if (fileTool !== undefined)
|
|
564
|
-
return fileTool;
|
|
565
|
-
if (toolName !== EDIT_TOOL_NAME)
|
|
566
|
-
return undefined;
|
|
567
|
-
const path = editPath(args);
|
|
568
|
-
const source = isError ? undefined : editDiffSource(details);
|
|
569
|
-
const stats = source === undefined ? undefined : diffStats(source);
|
|
570
|
-
if (path === "" && stats === undefined)
|
|
571
|
-
return undefined;
|
|
572
|
-
const label = path === "" ? EDIT_TOOL_NAME : `${EDIT_TOOL_NAME} ${path}`;
|
|
573
|
-
return {
|
|
574
|
-
summary: stats === undefined ? label : `${label} (+${String(stats.added)} -${String(stats.removed)})`,
|
|
575
|
-
status: isError ? "error" : "ok",
|
|
576
|
-
};
|
|
577
|
-
}
|
|
578
99
|
function formatNestedCallRow(call, theme) {
|
|
579
100
|
const inline = inlinePresentation(call.toolName, call.summary, undefined, call.isError === true);
|
|
580
101
|
const status = call.status === "running"
|
|
@@ -582,18 +103,18 @@ function formatNestedCallRow(call, theme) {
|
|
|
582
103
|
: call.isWarning === true
|
|
583
104
|
? "warn"
|
|
584
105
|
: (inline?.status ?? (call.isError === true ? "error" : "ok"));
|
|
585
|
-
const glyph = nestedStatusGlyph(status, theme);
|
|
586
|
-
const name = theme.fg("toolTitle", inline?.summary ?? call.toolName);
|
|
587
106
|
const cleanArgs = inline !== undefined || call.summary === undefined
|
|
588
107
|
? ""
|
|
589
108
|
: stripTerminalSequences(call.summary.trim());
|
|
590
109
|
const truncatedArgs = cleanArgs.length > 72 ? `${cleanArgs.slice(0, 69)}…` : cleanArgs;
|
|
591
|
-
const summaryPart = truncatedArgs !== "" ? ` ${theme.fg("muted", truncatedArgs)}` : "";
|
|
592
|
-
const durText = formatDurationMs(call.durationMs);
|
|
593
|
-
const durationPart = durText !== "" ? ` ${theme.fg("dim", durText)}` : "";
|
|
594
110
|
const costText = formatCostUsd(call.cost);
|
|
595
|
-
|
|
596
|
-
|
|
111
|
+
return formatToolHeaderText({
|
|
112
|
+
status,
|
|
113
|
+
title: call.toolName,
|
|
114
|
+
summary: [inline?.summary ?? truncatedArgs, formatDurationMs(call.durationMs), costText]
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.join(" · "),
|
|
117
|
+
}, theme);
|
|
597
118
|
}
|
|
598
119
|
function renderEditResult(params) {
|
|
599
120
|
const { theme, context, result, renderOptions, defaultLevel, options } = params;
|
|
@@ -692,58 +213,21 @@ function renderWriteResult(params) {
|
|
|
692
213
|
copyToClipboard: options?.copyToClipboard,
|
|
693
214
|
});
|
|
694
215
|
}
|
|
695
|
-
/** Reads only the persisted `pi-ext-tools-eval` details; other shapes degrade to plain output. */
|
|
696
|
-
function pythonEvalFacts(details) {
|
|
697
|
-
if (!isRecord(details) || details.format !== PYTHON_EVAL_DETAILS_FORMAT)
|
|
698
|
-
return undefined;
|
|
699
|
-
const rawRows = Array.isArray(details.rows) ? details.rows : [];
|
|
700
|
-
const rows = [];
|
|
701
|
-
for (const entry of rawRows) {
|
|
702
|
-
if (!isRecord(entry))
|
|
703
|
-
continue;
|
|
704
|
-
if (entry.kind === "text" && typeof entry.text === "string") {
|
|
705
|
-
rows.push({ kind: "text", text: entry.text });
|
|
706
|
-
}
|
|
707
|
-
else if (entry.kind === "display" && typeof entry.text === "string") {
|
|
708
|
-
rows.push({ kind: "display", text: entry.text });
|
|
709
|
-
}
|
|
710
|
-
else if (entry.kind === "result" && typeof entry.text === "string") {
|
|
711
|
-
rows.push({ kind: "result", text: entry.text });
|
|
712
|
-
}
|
|
713
|
-
else if (entry.kind === "tool" && isRecord(entry.trace)) {
|
|
714
|
-
const trace = entry.trace;
|
|
715
|
-
if (typeof trace.name !== "string")
|
|
716
|
-
continue;
|
|
717
|
-
rows.push({
|
|
718
|
-
kind: "tool",
|
|
719
|
-
name: trace.name,
|
|
720
|
-
args: trace.args,
|
|
721
|
-
durationMs: typeof trace.durationMs === "number" ? trace.durationMs : undefined,
|
|
722
|
-
error: typeof trace.error === "string" ? trace.error : undefined,
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
return {
|
|
727
|
-
rows,
|
|
728
|
-
error: typeof details.error === "string" ? details.error : undefined,
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
216
|
/** A nested file-tool row keeps its tool name; other tools degrade to name plus bounded args. */
|
|
732
217
|
function formatEvalToolRow(row, theme) {
|
|
733
218
|
const isError = row.error !== undefined;
|
|
734
219
|
const inline = inlinePresentation(row.name, row.args, undefined, isError);
|
|
735
|
-
const glyph = nestedStatusGlyph(isError ? "error" : "ok", theme);
|
|
736
|
-
if (inline !== undefined) {
|
|
737
|
-
const durText = formatDurationMs(row.durationMs);
|
|
738
|
-
const durationPart = durText !== "" ? ` ${theme.fg("dim", durText)}` : "";
|
|
739
|
-
return `${glyph} ${theme.fg("toolTitle", inline.summary)}${durationPart}`;
|
|
740
|
-
}
|
|
741
220
|
const summary = typeof row.args === "string" && row.args.trim() !== ""
|
|
742
221
|
? stripTerminalSequences(row.args.trim())
|
|
743
222
|
: "";
|
|
744
223
|
const truncated = summary.length > 72 ? `${summary.slice(0, 69)}…` : summary;
|
|
745
|
-
|
|
746
|
-
|
|
224
|
+
return formatToolHeaderText({
|
|
225
|
+
status: isError ? "error" : "ok",
|
|
226
|
+
title: row.name,
|
|
227
|
+
summary: [inline?.summary ?? truncated, formatDurationMs(row.durationMs)]
|
|
228
|
+
.filter(Boolean)
|
|
229
|
+
.join(" · "),
|
|
230
|
+
}, theme);
|
|
747
231
|
}
|
|
748
232
|
function renderPythonEvalResult(params) {
|
|
749
233
|
const { theme, context, result, renderOptions, defaultLevel, options } = params;
|
|
@@ -763,8 +247,8 @@ function renderPythonEvalResult(params) {
|
|
|
763
247
|
if (isRecord(context.args)) {
|
|
764
248
|
if (context.args.reset === true)
|
|
765
249
|
parts.push("(reset)");
|
|
766
|
-
if (typeof context.args.
|
|
767
|
-
parts.push(
|
|
250
|
+
if (typeof context.args.timeout_ms === "number")
|
|
251
|
+
parts.push(formatTimeoutFact(context.args.timeout_ms));
|
|
768
252
|
}
|
|
769
253
|
if (facts !== undefined) {
|
|
770
254
|
if (toolRows.length > 0) {
|
|
@@ -789,20 +273,26 @@ function renderPythonEvalResult(params) {
|
|
|
789
273
|
if (row.kind === "tool") {
|
|
790
274
|
outputRows.push(formatEvalToolRow(row, theme));
|
|
791
275
|
}
|
|
276
|
+
else if (row.kind === "state") {
|
|
277
|
+
// The namespace report is metadata about the cell, not its output, so it stays muted.
|
|
278
|
+
outputRows.push(theme.fg("muted", row.text));
|
|
279
|
+
}
|
|
792
280
|
else if (row.kind === "display" || row.kind === "result") {
|
|
793
281
|
const prefix = theme.fg("dim", `${row.kind}:`);
|
|
794
282
|
const lines = row.text.replace(/\r/g, "").split("\n");
|
|
795
283
|
for (let i = 0; i < lines.length; i++) {
|
|
796
|
-
|
|
284
|
+
const lineText = theme.fg("dim", lines[i] ?? "");
|
|
285
|
+
outputRows.push(i === 0 ? `${prefix} ${lineText}` : ` ${lineText}`);
|
|
797
286
|
}
|
|
798
287
|
}
|
|
799
288
|
else if (row.text !== "") {
|
|
800
|
-
|
|
289
|
+
const lines = row.text.replace(/\r/g, "").split("\n");
|
|
290
|
+
outputRows.push(...lines.map((l) => (isError ? theme.fg("error", l) : theme.fg("dim", l))));
|
|
801
291
|
}
|
|
802
292
|
}
|
|
803
293
|
if (outputRows.length > 0) {
|
|
804
294
|
sections.push({
|
|
805
|
-
railToken: isError ? "error" : "
|
|
295
|
+
railToken: isError ? "error" : "success",
|
|
806
296
|
content: outputRows,
|
|
807
297
|
});
|
|
808
298
|
}
|
|
@@ -810,7 +300,11 @@ function renderPythonEvalResult(params) {
|
|
|
810
300
|
else {
|
|
811
301
|
const output = stripTerminalSequences(extractResultText(PYTHON_EVAL_TOOL_NAME, result).outputText);
|
|
812
302
|
if (output !== "") {
|
|
813
|
-
|
|
303
|
+
const rawLines = output.split("\n");
|
|
304
|
+
const outputLines = isError
|
|
305
|
+
? rawLines.map((l) => theme.fg("error", l))
|
|
306
|
+
: rawLines.map((l) => theme.fg("dim", l));
|
|
307
|
+
sections.push({ railToken: isError ? "error" : "success", content: outputLines });
|
|
814
308
|
}
|
|
815
309
|
}
|
|
816
310
|
const output = stripTerminalSequences(extractResultText(PYTHON_EVAL_TOOL_NAME, result).outputText);
|
|
@@ -860,7 +354,9 @@ function renderApplyPatchCallView(params) {
|
|
|
860
354
|
state[APPLY_PATCH_PREVIEW_STATE_KEY] = stored;
|
|
861
355
|
}
|
|
862
356
|
const files = new Set(operations.map((operation) => operation.path)).size;
|
|
863
|
-
const summary = files > 0
|
|
357
|
+
const summary = files > 0
|
|
358
|
+
? patchSummary(files, operations.reduce((sum, operation) => sum + operation.addedLines, 0), operations.reduce((sum, operation) => sum + operation.removedLines, 0))
|
|
359
|
+
: "";
|
|
864
360
|
const rows = operations.map((operation) => formatApplyPatchOperationRow({
|
|
865
361
|
operationIndex: 0,
|
|
866
362
|
kind: operation.kind,
|
|
@@ -910,11 +406,7 @@ function renderApplyPatchResultView(params) {
|
|
|
910
406
|
const output = stripTerminalSequences(extractResultText(APPLY_PATCH_TOOL_NAME, result).outputText);
|
|
911
407
|
const parts = [];
|
|
912
408
|
if (facts !== undefined) {
|
|
913
|
-
|
|
914
|
-
if (files > 0)
|
|
915
|
-
parts.push(`${String(files)} ${files === 1 ? "file" : "files"}`);
|
|
916
|
-
if (facts.addedLines > 0 || facts.removedLines > 0)
|
|
917
|
-
parts.push(`+${String(facts.addedLines)} -${String(facts.removedLines)}`);
|
|
409
|
+
parts.push(patchSummary(new Set(facts.changedPaths).size, facts.addedLines, facts.removedLines));
|
|
918
410
|
}
|
|
919
411
|
const durText = formatDurationMs(context.durationMs);
|
|
920
412
|
if (durText !== "")
|
|
@@ -1002,22 +494,31 @@ function renderSpecializedResult(params) {
|
|
|
1002
494
|
return undefined;
|
|
1003
495
|
}
|
|
1004
496
|
function renderGenericResult(params) {
|
|
1005
|
-
const { toolName, theme, context, result, renderOptions, defaultLevel, originalRenderCall, originalRenderResult, options, } = params;
|
|
497
|
+
const { toolName, theme, context, result, renderOptions, defaultLevel, originalRenderCall, originalRenderResult, resolvePresentation, options, } = params;
|
|
1006
498
|
const heavyCmd = extractHeavyInputCommand(toolName, context.args);
|
|
1007
499
|
const nestedCalls = extractNestedCalls(result);
|
|
1008
|
-
const
|
|
500
|
+
const captured = toolName === "codemode"
|
|
501
|
+
? (getCapturedNestedCalls(context.toolCallId) ??
|
|
502
|
+
(isRecord(result.details) &&
|
|
503
|
+
Array.isArray(result.details.__piExtUiCapturedCalls)
|
|
504
|
+
? result.details
|
|
505
|
+
.__piExtUiCapturedCalls
|
|
506
|
+
: undefined))
|
|
507
|
+
: undefined;
|
|
508
|
+
const childCalls = captured ?? nestedCalls;
|
|
509
|
+
const isMetaTool = toolName === "codemode" || toolName === "subagent" || childCalls.length > 0;
|
|
1009
510
|
const { outputText: rawOutput, wallTimeSeconds } = extractResultText(toolName, result);
|
|
1010
511
|
const isError = (context.isError ?? false) || (result.isError ?? false);
|
|
1011
512
|
const status = toolName === "codemode"
|
|
1012
|
-
?
|
|
513
|
+
? childCalls.length === 0 && isError
|
|
1013
514
|
? "error"
|
|
1014
515
|
: "none"
|
|
1015
516
|
: isMetaTool
|
|
1016
517
|
? resolveContainerStatus({
|
|
1017
518
|
isError,
|
|
1018
519
|
isPartial: renderOptions.isPartial === true,
|
|
1019
|
-
hasChildren:
|
|
1020
|
-
selfFailed:
|
|
520
|
+
hasChildren: childCalls.length > 0,
|
|
521
|
+
selfFailed: childCalls.length === 0 && isError,
|
|
1021
522
|
})
|
|
1022
523
|
: isError
|
|
1023
524
|
? "error"
|
|
@@ -1038,9 +539,9 @@ function renderGenericResult(params) {
|
|
|
1038
539
|
if (stripped !== "")
|
|
1039
540
|
summaryParts.push(stripped);
|
|
1040
541
|
}
|
|
1041
|
-
if (isMetaTool &&
|
|
1042
|
-
const failCount =
|
|
1043
|
-
summaryParts.push(...callStatsParts(
|
|
542
|
+
if (isMetaTool && childCalls.length > 0) {
|
|
543
|
+
const failCount = childCalls.filter((c) => Boolean(c.isError || ("status" in c && c.status === "error"))).length;
|
|
544
|
+
summaryParts.push(...callStatsParts(childCalls.length, failCount));
|
|
1044
545
|
}
|
|
1045
546
|
if (context.durationMs !== undefined) {
|
|
1046
547
|
summaryParts.push(formatDurationMs(context.durationMs));
|
|
@@ -1048,28 +549,86 @@ function renderGenericResult(params) {
|
|
|
1048
549
|
else if (wallTimeSeconds !== undefined && Number.isFinite(wallTimeSeconds)) {
|
|
1049
550
|
summaryParts.push(formatDurationMs(wallTimeSeconds * 1000));
|
|
1050
551
|
}
|
|
1051
|
-
const totalCost =
|
|
552
|
+
const totalCost = childCalls.reduce((sum, c) => sum + (c.cost ?? 0), 0);
|
|
1052
553
|
if (totalCost > 0) {
|
|
1053
554
|
summaryParts.push(formatCostUsd(totalCost));
|
|
1054
555
|
}
|
|
1055
556
|
const summary = summaryParts.join(" · ");
|
|
1056
|
-
const
|
|
1057
|
-
?
|
|
557
|
+
const codemodeNestedView = toolName === "codemode" && childCalls.length > 0
|
|
558
|
+
? new NestedCallsView({
|
|
559
|
+
calls: childCalls,
|
|
560
|
+
theme,
|
|
561
|
+
context,
|
|
562
|
+
resolveChildRenderer: (childName) => {
|
|
563
|
+
return {
|
|
564
|
+
renderResult: (childResult, childOptions, childTheme, childCtx) => {
|
|
565
|
+
const provider = resolvePresentation?.(childName);
|
|
566
|
+
if (provider !== undefined) {
|
|
567
|
+
return renderOwnedPresentation({
|
|
568
|
+
toolName: childName,
|
|
569
|
+
provider,
|
|
570
|
+
result: childResult,
|
|
571
|
+
theme: childTheme,
|
|
572
|
+
context: childCtx,
|
|
573
|
+
defaultLevel: childOptions.expanded ? 2 : 1,
|
|
574
|
+
options,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
const specialized = renderSpecializedResult({
|
|
578
|
+
toolName: childName,
|
|
579
|
+
theme: childTheme,
|
|
580
|
+
context: childCtx,
|
|
581
|
+
result: childResult,
|
|
582
|
+
renderOptions: childOptions,
|
|
583
|
+
defaultLevel: childOptions.expanded ? 2 : 1,
|
|
584
|
+
options,
|
|
585
|
+
});
|
|
586
|
+
if (specialized !== undefined)
|
|
587
|
+
return specialized;
|
|
588
|
+
return renderGenericResult({
|
|
589
|
+
toolName: childName,
|
|
590
|
+
theme: childTheme,
|
|
591
|
+
context: childCtx,
|
|
592
|
+
result: childResult,
|
|
593
|
+
renderOptions: childOptions,
|
|
594
|
+
defaultLevel: childOptions.expanded ? 2 : 1,
|
|
595
|
+
originalRenderCall: undefined,
|
|
596
|
+
originalRenderResult: undefined,
|
|
597
|
+
resolvePresentation,
|
|
598
|
+
options,
|
|
599
|
+
});
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
},
|
|
603
|
+
copyToClipboard: options?.copyToClipboard,
|
|
604
|
+
stateHolder: context.state,
|
|
605
|
+
resolvePresentation,
|
|
606
|
+
invalidate: () => context.invalidate(),
|
|
607
|
+
})
|
|
1058
608
|
: undefined;
|
|
1059
|
-
const
|
|
609
|
+
const collapsedRows = codemodeNestedView !== undefined
|
|
610
|
+
? codemodeNestedView
|
|
611
|
+
: isMetaTool && nestedCalls.length > 0
|
|
612
|
+
? nestedCalls.map((call) => formatNestedCallRow(call, theme))
|
|
613
|
+
: undefined;
|
|
614
|
+
const toolRailToken = isError
|
|
615
|
+
? "error"
|
|
616
|
+
: renderOptions.isPartial
|
|
617
|
+
? "accent"
|
|
618
|
+
: "success";
|
|
619
|
+
// Upper room (input code) uses neutral/muted rail; lower room (output) uses toolRailToken (success/error)
|
|
620
|
+
const inputSection = createInputCodeSection(toolName, context.args, theme, "muted");
|
|
1060
621
|
const sections = inputSection !== undefined ? [inputSection] : [];
|
|
1061
|
-
const outputRailToken = isError ? "error" : renderOptions.isPartial ? "accent" : "dim";
|
|
1062
622
|
if (toolName === "codemode") {
|
|
623
|
+
// Nested tool calls belong only to collapsedRows (Level 0), hidden when expanded
|
|
1063
624
|
if (rawOutput !== "") {
|
|
1064
|
-
const
|
|
1065
|
-
const
|
|
1066
|
-
.
|
|
1067
|
-
.
|
|
1068
|
-
.split("\n")
|
|
1069
|
-
.map((line) => theme.fg(outputColor, line));
|
|
625
|
+
const rawLines = rawOutput.replace(/\r/g, "").replace(/\t/g, " ").split("\n");
|
|
626
|
+
const outputLines = isError
|
|
627
|
+
? rawLines.map((line) => theme.fg("error", line))
|
|
628
|
+
: rawLines.map((line) => theme.fg("dim", line));
|
|
1070
629
|
sections.push({
|
|
1071
|
-
railToken:
|
|
1072
|
-
content:
|
|
630
|
+
railToken: toolRailToken,
|
|
631
|
+
content: outputLines,
|
|
1073
632
|
});
|
|
1074
633
|
}
|
|
1075
634
|
}
|
|
@@ -1093,31 +652,60 @@ function renderGenericResult(params) {
|
|
|
1093
652
|
invalidate() { },
|
|
1094
653
|
};
|
|
1095
654
|
sections.push({
|
|
1096
|
-
railToken:
|
|
655
|
+
railToken: toolRailToken,
|
|
1097
656
|
content: lazyUpstreamComponent,
|
|
1098
657
|
maxLines: effectiveExpanded ? Number.POSITIVE_INFINITY : 9,
|
|
1099
658
|
});
|
|
1100
659
|
}
|
|
1101
660
|
else if (rawOutput !== "") {
|
|
1102
|
-
const
|
|
1103
|
-
|
|
661
|
+
const rawLines = rawOutput.split("\n");
|
|
662
|
+
let outputLines;
|
|
663
|
+
if (isError) {
|
|
664
|
+
outputLines = rawLines.map((line) => theme.fg("error", line));
|
|
665
|
+
}
|
|
666
|
+
else if (toolName === "read" || toolPath(context.args) !== "") {
|
|
667
|
+
const filePath = toolPath(context.args);
|
|
668
|
+
const lang = filePath !== "" ? getLanguageFromPath(filePath) : undefined;
|
|
669
|
+
if (lang !== undefined) {
|
|
670
|
+
try {
|
|
671
|
+
outputLines = highlightCode(rawOutput, lang);
|
|
672
|
+
}
|
|
673
|
+
catch {
|
|
674
|
+
outputLines = rawLines;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
outputLines = rawLines;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
else if (toolName === "bash") {
|
|
682
|
+
outputLines = rawLines.map((line) => theme.fg("dim", line));
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
outputLines = rawLines;
|
|
686
|
+
}
|
|
1104
687
|
sections.push({
|
|
1105
|
-
railToken:
|
|
1106
|
-
content:
|
|
688
|
+
railToken: toolRailToken,
|
|
689
|
+
content: outputLines,
|
|
1107
690
|
});
|
|
1108
691
|
}
|
|
1109
692
|
let copy;
|
|
1110
|
-
if (
|
|
693
|
+
if (toolName === "codemode") {
|
|
694
|
+
copy = undefined;
|
|
695
|
+
}
|
|
696
|
+
else if (heavyCmd !== undefined && heavyCmd !== "") {
|
|
697
|
+
const cleanOutput = stripTerminalSequences(rawOutput);
|
|
1111
698
|
copy = {
|
|
1112
699
|
primary: heavyCmd,
|
|
1113
|
-
full: rawOutput !== "" ? `${heavyCmd}\n---\n${
|
|
700
|
+
full: rawOutput !== "" ? `${heavyCmd}\n---\n${cleanOutput}` : heavyCmd,
|
|
1114
701
|
};
|
|
1115
702
|
}
|
|
1116
703
|
else if (rawOutput !== "") {
|
|
704
|
+
const cleanOutput = stripTerminalSequences(rawOutput);
|
|
1117
705
|
const argsFormatted = formatArgsPreview(context.args);
|
|
1118
706
|
copy = {
|
|
1119
|
-
primary:
|
|
1120
|
-
full: argsFormatted !== "" ? `${argsFormatted}\n\n${
|
|
707
|
+
primary: cleanOutput,
|
|
708
|
+
full: argsFormatted !== "" ? `${argsFormatted}\n\n${cleanOutput}` : cleanOutput,
|
|
1121
709
|
};
|
|
1122
710
|
}
|
|
1123
711
|
return new ToolView({
|
|
@@ -1127,6 +715,7 @@ function renderGenericResult(params) {
|
|
|
1127
715
|
status,
|
|
1128
716
|
summary,
|
|
1129
717
|
},
|
|
718
|
+
railToken: toolRailToken,
|
|
1130
719
|
collapsedRows,
|
|
1131
720
|
sections,
|
|
1132
721
|
copy,
|
|
@@ -1138,11 +727,90 @@ function renderGenericResult(params) {
|
|
|
1138
727
|
copyToClipboard: options?.copyToClipboard,
|
|
1139
728
|
});
|
|
1140
729
|
}
|
|
730
|
+
function renderOwnedPresentation(params) {
|
|
731
|
+
const { toolName, provider, result, theme, context, defaultLevel, options } = params;
|
|
732
|
+
const presentation = provider(context.args, result, theme);
|
|
733
|
+
const isError = context.isError || result?.isError === true;
|
|
734
|
+
const pending = result === undefined || context.isPartial;
|
|
735
|
+
const isDiff = !isError && !pending && presentation.kind === "diff";
|
|
736
|
+
const status = isError
|
|
737
|
+
? "error"
|
|
738
|
+
: pending
|
|
739
|
+
? context.argsComplete
|
|
740
|
+
? "running"
|
|
741
|
+
: "streaming"
|
|
742
|
+
: isDiff
|
|
743
|
+
? "none"
|
|
744
|
+
: "ok";
|
|
745
|
+
const railToken = isError ? "error" : pending ? "accent" : "success";
|
|
746
|
+
const output = isError && result !== undefined
|
|
747
|
+
? agentResultText(result)
|
|
748
|
+
: (presentation.output ?? (result === undefined ? "" : agentResultText(result)));
|
|
749
|
+
const sections = [];
|
|
750
|
+
if (presentation.request) {
|
|
751
|
+
sections.push({ railToken: "muted", content: stripTerminalSequences(presentation.request) });
|
|
752
|
+
}
|
|
753
|
+
if (isDiff) {
|
|
754
|
+
if (presentation.diff.trim() !== "") {
|
|
755
|
+
sections.push({
|
|
756
|
+
railToken: "dim",
|
|
757
|
+
content: new DiffView({
|
|
758
|
+
theme,
|
|
759
|
+
source: { kind: "unifiedPatch", text: presentation.diff },
|
|
760
|
+
}),
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
else if (output !== "") {
|
|
765
|
+
const lines = stripTerminalSequences(output).split("\n");
|
|
766
|
+
sections.push({
|
|
767
|
+
railToken,
|
|
768
|
+
content: isError ? lines.map((line) => theme.fg("error", line)) : lines,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
const duration = result === undefined ? "" : formatDurationMs(context.durationMs);
|
|
772
|
+
const summary = [stripTerminalSequences(presentation.summary), duration]
|
|
773
|
+
.filter(Boolean)
|
|
774
|
+
.join(" · ");
|
|
775
|
+
const copy = presentation.copy === undefined
|
|
776
|
+
? isDiff && presentation.diff.trim() !== ""
|
|
777
|
+
? { primary: presentation.diff }
|
|
778
|
+
: undefined
|
|
779
|
+
: {
|
|
780
|
+
primary: stripTerminalSequences(presentation.copy.primary),
|
|
781
|
+
...(presentation.copy.full === undefined
|
|
782
|
+
? {}
|
|
783
|
+
: { full: stripTerminalSequences(presentation.copy.full) }),
|
|
784
|
+
};
|
|
785
|
+
return new ToolView({
|
|
786
|
+
theme,
|
|
787
|
+
header: { title: toolName, status, summary },
|
|
788
|
+
railToken,
|
|
789
|
+
collapsedRows: isError && output !== ""
|
|
790
|
+
? output
|
|
791
|
+
.split("\n")
|
|
792
|
+
.slice(0, 1)
|
|
793
|
+
.map((line) => theme.fg("error", stripTerminalSequences(line)))
|
|
794
|
+
: presentation.collapsedRows?.map(stripTerminalSequences),
|
|
795
|
+
sections,
|
|
796
|
+
copy,
|
|
797
|
+
outputPad: context.outputPad,
|
|
798
|
+
stateHolder: context.state,
|
|
799
|
+
defaultLevel,
|
|
800
|
+
globalExpanded: context.expanded,
|
|
801
|
+
invalidate: () => context.invalidate(),
|
|
802
|
+
copyToClipboard: options?.copyToClipboard,
|
|
803
|
+
});
|
|
804
|
+
}
|
|
1141
805
|
export function createCollapsibleToolRendererResolver(options) {
|
|
806
|
+
return createRendererResolver(options);
|
|
807
|
+
}
|
|
808
|
+
function createRendererResolver(options, resolvePresentation) {
|
|
1142
809
|
const defaultLevel = options?.defaultCollapsed === false ? 1 : 0;
|
|
1143
810
|
return (toolName, next) => {
|
|
1144
811
|
const upstream = next();
|
|
1145
|
-
|
|
812
|
+
const provider = getToolPresentationProvider(upstream) ?? resolvePresentation?.(toolName);
|
|
813
|
+
if (provider === undefined && !shouldInterceptTool(toolName, options)) {
|
|
1146
814
|
return upstream;
|
|
1147
815
|
}
|
|
1148
816
|
const originalRenderCall = upstream?.renderCall;
|
|
@@ -1150,6 +818,18 @@ export function createCollapsibleToolRendererResolver(options) {
|
|
|
1150
818
|
return {
|
|
1151
819
|
renderShell: "self",
|
|
1152
820
|
renderCall(args, theme, context) {
|
|
821
|
+
if (provider !== undefined) {
|
|
822
|
+
const view = renderOwnedPresentation({
|
|
823
|
+
toolName,
|
|
824
|
+
provider,
|
|
825
|
+
theme,
|
|
826
|
+
context: { ...context, args },
|
|
827
|
+
defaultLevel,
|
|
828
|
+
options,
|
|
829
|
+
});
|
|
830
|
+
rememberCallView(context, view);
|
|
831
|
+
return view;
|
|
832
|
+
}
|
|
1153
833
|
if (toolName === APPLY_PATCH_TOOL_NAME) {
|
|
1154
834
|
const view = renderApplyPatchCallView({
|
|
1155
835
|
theme,
|
|
@@ -1157,7 +837,7 @@ export function createCollapsibleToolRendererResolver(options) {
|
|
|
1157
837
|
defaultLevel,
|
|
1158
838
|
options,
|
|
1159
839
|
});
|
|
1160
|
-
rememberCallView(context, view
|
|
840
|
+
rememberCallView(context, view);
|
|
1161
841
|
return view;
|
|
1162
842
|
}
|
|
1163
843
|
if (toolName === EDIT_TOOL_NAME) {
|
|
@@ -1176,7 +856,7 @@ export function createCollapsibleToolRendererResolver(options) {
|
|
|
1176
856
|
invalidate: () => context.invalidate(),
|
|
1177
857
|
copyToClipboard: options?.copyToClipboard,
|
|
1178
858
|
});
|
|
1179
|
-
rememberCallView(context, view
|
|
859
|
+
rememberCallView(context, view);
|
|
1180
860
|
return view;
|
|
1181
861
|
}
|
|
1182
862
|
const isStreamingArgs = context.isPartial && !context.argsComplete;
|
|
@@ -1238,11 +918,26 @@ export function createCollapsibleToolRendererResolver(options) {
|
|
|
1238
918
|
invalidate: () => context.invalidate(),
|
|
1239
919
|
copyToClipboard: options?.copyToClipboard,
|
|
1240
920
|
});
|
|
1241
|
-
rememberCallView(context, view
|
|
921
|
+
rememberCallView(context, view);
|
|
1242
922
|
return view;
|
|
1243
923
|
},
|
|
1244
924
|
renderResult(result, renderOptions, theme, context) {
|
|
1245
925
|
hideCallView(context, toolName, renderOptions.isPartial);
|
|
926
|
+
if (provider !== undefined) {
|
|
927
|
+
return renderOwnedPresentation({
|
|
928
|
+
toolName,
|
|
929
|
+
provider,
|
|
930
|
+
result,
|
|
931
|
+
theme,
|
|
932
|
+
context: {
|
|
933
|
+
...context,
|
|
934
|
+
expanded: renderOptions.expanded,
|
|
935
|
+
isPartial: renderOptions.isPartial,
|
|
936
|
+
},
|
|
937
|
+
defaultLevel,
|
|
938
|
+
options,
|
|
939
|
+
});
|
|
940
|
+
}
|
|
1246
941
|
const specialized = renderSpecializedResult({
|
|
1247
942
|
toolName,
|
|
1248
943
|
theme,
|
|
@@ -1263,6 +958,7 @@ export function createCollapsibleToolRendererResolver(options) {
|
|
|
1263
958
|
defaultLevel,
|
|
1264
959
|
originalRenderCall,
|
|
1265
960
|
originalRenderResult,
|
|
961
|
+
resolvePresentation,
|
|
1266
962
|
options,
|
|
1267
963
|
});
|
|
1268
964
|
},
|
|
@@ -1270,5 +966,26 @@ export function createCollapsibleToolRendererResolver(options) {
|
|
|
1270
966
|
};
|
|
1271
967
|
}
|
|
1272
968
|
export function registerCollapsibleToolRenderer(pi, options) {
|
|
1273
|
-
pi.
|
|
969
|
+
if (typeof pi.on === "function") {
|
|
970
|
+
pi.on("tool_execution_start", (event) => {
|
|
971
|
+
recordToolExecutionStart(event);
|
|
972
|
+
});
|
|
973
|
+
pi.on("tool_execution_end", (event) => {
|
|
974
|
+
recordToolExecutionEnd(event);
|
|
975
|
+
});
|
|
976
|
+
pi.on("tool_result", (event) => {
|
|
977
|
+
const captured = getCapturedNestedCalls(event.toolCallId);
|
|
978
|
+
if (captured && captured.length > 0) {
|
|
979
|
+
const currentDetails = isRecord(event.details) ? event.details : {};
|
|
980
|
+
return {
|
|
981
|
+
details: {
|
|
982
|
+
...currentDetails,
|
|
983
|
+
__piExtUiCapturedCalls: captured,
|
|
984
|
+
},
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
return undefined;
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
pi.registerToolRenderer(createRendererResolver(options, (name) => getRegisteredToolPresentationProvider(pi, name)));
|
|
1274
991
|
}
|