@hheei/pi-ext-ui 0.1.0 → 0.2.1
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/extension.js +12 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -2
- package/dist/message-view.d.ts +15 -0
- package/dist/message-view.js +130 -0
- package/dist/nested-calls-view.d.ts +28 -0
- package/dist/nested-calls-view.js +405 -0
- package/dist/payload-parser.d.ts +55 -0
- package/dist/payload-parser.js +370 -0
- package/dist/renderer.d.ts +10 -28
- package/dist/renderer.js +322 -1005
- 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/apply-patch-glyph.d.ts +0 -10
- package/dist/apply-patch-glyph.js +0 -29
- package/dist/apply-patch-preview.d.ts +0 -30
- package/dist/apply-patch-preview.js +0 -89
- package/dist/apply-patch-view.d.ts +0 -47
- package/dist/apply-patch-view.js +0 -198
- package/dist/collapsible-tool-frame.d.ts +0 -1
- package/dist/collapsible-tool-frame.js +0 -1
package/dist/renderer.js
CHANGED
|
@@ -1,230 +1,50 @@
|
|
|
1
|
+
import { getLanguageFromPath, highlightCode, } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import { stripTerminalSequences } from "@earendil-works/pi-tui";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"codemode",
|
|
14
|
-
];
|
|
3
|
+
import { agentResultText, getRegisteredToolPresentationProvider, getToolPresentationProvider, hasOptionalToolRenderers, } from "@hheei/pi-ext-core";
|
|
4
|
+
import { DiffView } from "./diff-view.js";
|
|
5
|
+
import { NestedCallsView } from "./nested-calls-view.js";
|
|
6
|
+
import { formatToolHeaderText, ToolView, } from "./tool-view.js";
|
|
7
|
+
import { getCapturedNestedCalls, recordToolExecutionEnd, recordToolExecutionStart, } from "./tracker.js";
|
|
8
|
+
/**
|
|
9
|
+
* Upstream tools whose call and result views pi-ext-ui still frames itself. Concrete tool packages
|
|
10
|
+
* own their own presentation through ext-core's optional renderer point, so their names never
|
|
11
|
+
* appear here.
|
|
12
|
+
*/
|
|
13
|
+
export const SPECIALIZED_TOOL_RENDERERS = ["codemode"];
|
|
15
14
|
/** Web-access tools use the shared folding shell while retaining their native body renderer. */
|
|
16
15
|
export const WEB_ACCESS_TOOLS = ["web_search", "web_fetch", "web_open"];
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const PYTHON_EVAL_TOOL_NAME = "python_eval";
|
|
20
|
-
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";
|
|
16
|
+
import { callStatsParts, estimateStreamingTokens, extractHeavyInputCommand, extractNestedCalls, extractResultText, extractSummaryArg, formatCostUsd, formatDurationMs, inlinePresentation, isRecord, resolveContainerStatus, toolPath, } from "./payload-parser.js";
|
|
17
|
+
export { estimateStreamingTokens, inlinePresentation, } from "./payload-parser.js";
|
|
26
18
|
const SPECIALIZED_TOOL_SET = new Set(SPECIALIZED_TOOL_RENDERERS);
|
|
27
19
|
const WEB_ACCESS_TOOL_SET = new Set(WEB_ACCESS_TOOLS);
|
|
28
20
|
const CALL_VIEW_STATE_KEY = "__piExtUiCallView";
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
export function estimateStreamingTokens(args) {
|
|
35
|
-
if (args === undefined || args === null)
|
|
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
|
-
}
|
|
21
|
+
function detectCodeLanguage(toolName) {
|
|
22
|
+
if (toolName === "bash")
|
|
23
|
+
return "bash";
|
|
24
|
+
if (toolName === "codemode")
|
|
25
|
+
return "javascript";
|
|
138
26
|
return undefined;
|
|
139
27
|
}
|
|
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) {
|
|
28
|
+
function createInputCodeSection(toolName, args, theme, railToken = "muted") {
|
|
218
29
|
const raw = extractHeavyInputCommand(toolName, args);
|
|
219
30
|
if (raw === undefined || raw === "")
|
|
220
31
|
return undefined;
|
|
221
32
|
const isShell = toolName === "bash";
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
.map((line, i) =>
|
|
227
|
-
|
|
33
|
+
const clean = raw.replace(/\r/g, "").replace(/\t/g, " ");
|
|
34
|
+
const lang = detectCodeLanguage(toolName);
|
|
35
|
+
let lines;
|
|
36
|
+
if (isShell) {
|
|
37
|
+
lines = clean.split("\n").map((line, i) => (i === 0 ? `$ ${line}` : ` ${line}`));
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
try {
|
|
41
|
+
lines = highlightCode(clean, lang);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
lines = clean.split("\n").map((line) => theme.fg("mdCode", line));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { railToken, content: lines };
|
|
228
48
|
}
|
|
229
49
|
function formatArgsPreview(args) {
|
|
230
50
|
if (args === undefined || args === null)
|
|
@@ -238,343 +58,32 @@ function formatArgsPreview(args) {
|
|
|
238
58
|
return "";
|
|
239
59
|
}
|
|
240
60
|
}
|
|
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
61
|
/**
|
|
258
62
|
* The host stacks a tool execution's call and result views. Following the
|
|
259
63
|
* official edit renderer, the call view is stored in the shared renderer
|
|
260
64
|
* state so the result renderer can hide it in place within the same
|
|
261
65
|
* updateDisplay pass — no draw-time flags needed.
|
|
66
|
+
*
|
|
67
|
+
* The call row is left visible until a result view exists: hiding it here would
|
|
68
|
+
* blank the whole execution for as long as the tool produces no result yet.
|
|
262
69
|
*/
|
|
263
|
-
function rememberCallView(context, view
|
|
70
|
+
function rememberCallView(context, view) {
|
|
264
71
|
if (isRecord(context.state)) {
|
|
265
72
|
context.state[CALL_VIEW_STATE_KEY] = view;
|
|
266
73
|
}
|
|
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
74
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
75
|
+
/**
|
|
76
|
+
* A result view takes over from the call view. A container tool yields on any result, because its
|
|
77
|
+
* own row would duplicate the nested rows and output it now carries; every other tool keeps its
|
|
78
|
+
* call row until the final result. A provider states that for its own tool, so no name is involved.
|
|
79
|
+
*/
|
|
80
|
+
function hideCallView(context, replaceCallOnPartial, isPartial) {
|
|
81
|
+
if (!replaceCallOnPartial && isPartial)
|
|
281
82
|
return;
|
|
282
83
|
const view = isRecord(context.state) ? context.state[CALL_VIEW_STATE_KEY] : undefined;
|
|
283
84
|
if (view instanceof ToolView)
|
|
284
85
|
view.hide();
|
|
285
86
|
}
|
|
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
87
|
function formatNestedCallRow(call, theme) {
|
|
579
88
|
const inline = inlinePresentation(call.toolName, call.summary, undefined, call.isError === true);
|
|
580
89
|
const status = call.status === "running"
|
|
@@ -582,399 +91,18 @@ function formatNestedCallRow(call, theme) {
|
|
|
582
91
|
: call.isWarning === true
|
|
583
92
|
? "warn"
|
|
584
93
|
: (inline?.status ?? (call.isError === true ? "error" : "ok"));
|
|
585
|
-
const glyph = nestedStatusGlyph(status, theme);
|
|
586
|
-
const name = theme.fg("toolTitle", inline?.summary ?? call.toolName);
|
|
587
94
|
const cleanArgs = inline !== undefined || call.summary === undefined
|
|
588
95
|
? ""
|
|
589
96
|
: stripTerminalSequences(call.summary.trim());
|
|
590
97
|
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
98
|
const costText = formatCostUsd(call.cost);
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
const source = isError ? undefined : editDiffSource(result.details);
|
|
603
|
-
const stats = source === undefined ? undefined : diffStats(source);
|
|
604
|
-
const summaryParts = [
|
|
605
|
-
stats === undefined ? path : `${path} (+${String(stats.added)} -${String(stats.removed)})`,
|
|
606
|
-
];
|
|
607
|
-
const durText = formatDurationMs(context.durationMs);
|
|
608
|
-
if (durText !== "")
|
|
609
|
-
summaryParts.push(durText);
|
|
610
|
-
const summary = summaryParts.join(" · ");
|
|
611
|
-
const output = stripTerminalSequences(extractResultText(EDIT_TOOL_NAME, result).outputText);
|
|
612
|
-
const patch = patchTextForCopy(result.details);
|
|
613
|
-
// A successful edit hides the top-level check mark; the diff body carries the change.
|
|
614
|
-
const status = isError
|
|
615
|
-
? "error"
|
|
616
|
-
: renderOptions.isPartial
|
|
617
|
-
? "running"
|
|
618
|
-
: source !== undefined
|
|
619
|
-
? "none"
|
|
620
|
-
: "ok";
|
|
621
|
-
const sections = source === undefined
|
|
622
|
-
? output === ""
|
|
623
|
-
? []
|
|
624
|
-
: [{ railToken: isError ? "error" : "dim", content: output }]
|
|
625
|
-
: [{ railToken: "dim", content: new DiffView({ theme, source }) }];
|
|
626
|
-
const copy = patch !== undefined ? { primary: patch } : output !== "" ? { primary: output } : undefined;
|
|
627
|
-
return new ToolView({
|
|
628
|
-
theme,
|
|
629
|
-
header: { title: EDIT_TOOL_NAME, status, summary },
|
|
630
|
-
sections,
|
|
631
|
-
copy,
|
|
632
|
-
outputPad: context.outputPad,
|
|
633
|
-
stateHolder: context.state,
|
|
634
|
-
defaultLevel,
|
|
635
|
-
globalExpanded: renderOptions.expanded,
|
|
636
|
-
invalidate: () => context.invalidate(),
|
|
637
|
-
copyToClipboard: options?.copyToClipboard,
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
function renderWriteResult(params) {
|
|
641
|
-
const { theme, context, result, renderOptions, defaultLevel, options } = params;
|
|
642
|
-
const path = toolPath(context.args);
|
|
643
|
-
const isError = context.isError || result.isError;
|
|
644
|
-
const presentation = isError ? undefined : writePresentation(context.args, result.details);
|
|
645
|
-
const output = stripTerminalSequences(extractResultText(WRITE_TOOL_NAME, result).outputText);
|
|
646
|
-
const baseSummary = presentation?.kind === "diff"
|
|
647
|
-
? `${path} (+${String(presentation.added)} -${String(presentation.removed)})`
|
|
648
|
-
: presentation?.kind === "content"
|
|
649
|
-
? `${path} (${presentation.label})`
|
|
650
|
-
: presentation?.kind === "noChange"
|
|
651
|
-
? `${path} (no changes)`
|
|
652
|
-
: path;
|
|
653
|
-
const durText = formatDurationMs(context.durationMs);
|
|
654
|
-
const summaryParts = [baseSummary];
|
|
655
|
-
if (durText !== "")
|
|
656
|
-
summaryParts.push(durText);
|
|
657
|
-
const summary = summaryParts.join(" · ");
|
|
658
|
-
const sections = presentation?.kind === "diff"
|
|
659
|
-
? [{ railToken: "dim", content: new DiffView({ theme, source: presentation.source }) }]
|
|
660
|
-
: presentation?.kind === "content"
|
|
661
|
-
? [{ railToken: "dim", content: presentation.content }]
|
|
662
|
-
: output === ""
|
|
663
|
-
? []
|
|
664
|
-
: [{ railToken: isError ? "error" : "dim", content: output }];
|
|
665
|
-
const copyText = presentation?.kind === "content"
|
|
666
|
-
? presentation.content
|
|
667
|
-
: presentation?.kind === "diff"
|
|
668
|
-
? undefined
|
|
669
|
-
: output;
|
|
670
|
-
return new ToolView({
|
|
671
|
-
theme,
|
|
672
|
-
header: {
|
|
673
|
-
title: WRITE_TOOL_NAME,
|
|
674
|
-
status: isError
|
|
675
|
-
? "error"
|
|
676
|
-
: renderOptions.isPartial
|
|
677
|
-
? "running"
|
|
678
|
-
: presentation?.kind === "diff"
|
|
679
|
-
? "none"
|
|
680
|
-
: presentation?.kind === "noChange"
|
|
681
|
-
? "none"
|
|
682
|
-
: "ok",
|
|
683
|
-
summary,
|
|
684
|
-
},
|
|
685
|
-
sections,
|
|
686
|
-
copy: copyText !== undefined && copyText !== "" ? { primary: copyText } : undefined,
|
|
687
|
-
outputPad: context.outputPad,
|
|
688
|
-
stateHolder: context.state,
|
|
689
|
-
defaultLevel,
|
|
690
|
-
globalExpanded: renderOptions.expanded,
|
|
691
|
-
invalidate: () => context.invalidate(),
|
|
692
|
-
copyToClipboard: options?.copyToClipboard,
|
|
693
|
-
});
|
|
694
|
-
}
|
|
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
|
-
/** A nested file-tool row keeps its tool name; other tools degrade to name plus bounded args. */
|
|
732
|
-
function formatEvalToolRow(row, theme) {
|
|
733
|
-
const isError = row.error !== undefined;
|
|
734
|
-
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
|
-
const summary = typeof row.args === "string" && row.args.trim() !== ""
|
|
742
|
-
? stripTerminalSequences(row.args.trim())
|
|
743
|
-
: "";
|
|
744
|
-
const truncated = summary.length > 72 ? `${summary.slice(0, 69)}…` : summary;
|
|
745
|
-
const summaryPart = truncated !== "" ? ` ${theme.fg("muted", truncated)}` : "";
|
|
746
|
-
return `${glyph} ${theme.fg("toolTitle", row.name)}${summaryPart}`;
|
|
747
|
-
}
|
|
748
|
-
function renderPythonEvalResult(params) {
|
|
749
|
-
const { theme, context, result, renderOptions, defaultLevel, options } = params;
|
|
750
|
-
const facts = pythonEvalFacts(result.details);
|
|
751
|
-
const isError = (context.isError ?? false) || (result.isError ?? false);
|
|
752
|
-
const toolRows = facts?.rows.filter((row) => row.kind === "tool") ??
|
|
753
|
-
[];
|
|
754
|
-
const anyToolError = toolRows.some((row) => row.error !== undefined);
|
|
755
|
-
const failed = isError || facts?.error !== undefined || anyToolError;
|
|
756
|
-
const status = resolveContainerStatus({
|
|
757
|
-
isError,
|
|
758
|
-
isPartial: renderOptions.isPartial === true,
|
|
759
|
-
hasChildren: toolRows.length > 0,
|
|
760
|
-
selfFailed: failed && !anyToolError,
|
|
761
|
-
});
|
|
762
|
-
const parts = [];
|
|
763
|
-
if (isRecord(context.args)) {
|
|
764
|
-
if (context.args.reset === true)
|
|
765
|
-
parts.push("(reset)");
|
|
766
|
-
if (typeof context.args.timeout === "number")
|
|
767
|
-
parts.push(`(timeout ${String(context.args.timeout)}s)`);
|
|
768
|
-
}
|
|
769
|
-
if (facts !== undefined) {
|
|
770
|
-
if (toolRows.length > 0) {
|
|
771
|
-
const failCount = toolRows.filter((row) => row.error !== undefined).length;
|
|
772
|
-
parts.push(...callStatsParts(toolRows.length, failCount));
|
|
773
|
-
}
|
|
774
|
-
else if (facts.rows.length > 0) {
|
|
775
|
-
parts.push(`${String(facts.rows.length)} ${facts.rows.length === 1 ? "row" : "rows"}`);
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
const durText = formatDurationMs(context.durationMs);
|
|
779
|
-
if (durText !== "")
|
|
780
|
-
parts.push(durText);
|
|
781
|
-
const summary = parts.join(" · ");
|
|
782
|
-
const collapsedRows = toolRows.length > 0 ? toolRows.map((row) => formatEvalToolRow(row, theme)) : undefined;
|
|
783
|
-
const code = extractHeavyInputCommand(PYTHON_EVAL_TOOL_NAME, context.args);
|
|
784
|
-
const inputSection = createInputCodeSection(PYTHON_EVAL_TOOL_NAME, context.args, theme);
|
|
785
|
-
const sections = inputSection !== undefined ? [inputSection] : [];
|
|
786
|
-
if (facts !== undefined) {
|
|
787
|
-
const outputRows = [];
|
|
788
|
-
for (const row of facts.rows) {
|
|
789
|
-
if (row.kind === "tool") {
|
|
790
|
-
outputRows.push(formatEvalToolRow(row, theme));
|
|
791
|
-
}
|
|
792
|
-
else if (row.kind === "display" || row.kind === "result") {
|
|
793
|
-
const prefix = theme.fg("dim", `${row.kind}:`);
|
|
794
|
-
const lines = row.text.replace(/\r/g, "").split("\n");
|
|
795
|
-
for (let i = 0; i < lines.length; i++) {
|
|
796
|
-
outputRows.push(i === 0 ? `${prefix} ${lines[i]}` : ` ${lines[i]}`);
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
else if (row.text !== "") {
|
|
800
|
-
outputRows.push(...row.text.replace(/\r/g, "").split("\n"));
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
if (outputRows.length > 0) {
|
|
804
|
-
sections.push({
|
|
805
|
-
railToken: isError ? "error" : "dim",
|
|
806
|
-
content: outputRows,
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
else {
|
|
811
|
-
const output = stripTerminalSequences(extractResultText(PYTHON_EVAL_TOOL_NAME, result).outputText);
|
|
812
|
-
if (output !== "") {
|
|
813
|
-
sections.push({ railToken: isError ? "error" : "dim", content: output });
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
const output = stripTerminalSequences(extractResultText(PYTHON_EVAL_TOOL_NAME, result).outputText);
|
|
817
|
-
const copy = code !== undefined && code !== ""
|
|
818
|
-
? { primary: code, full: output !== "" ? `${code}\n---\n${output}` : code }
|
|
819
|
-
: output !== ""
|
|
820
|
-
? { primary: output }
|
|
821
|
-
: undefined;
|
|
822
|
-
return new ToolView({
|
|
823
|
-
theme,
|
|
824
|
-
header: {
|
|
825
|
-
title: PYTHON_EVAL_TOOL_TITLE,
|
|
826
|
-
status,
|
|
827
|
-
summary,
|
|
828
|
-
},
|
|
829
|
-
collapsedRows,
|
|
830
|
-
sections,
|
|
831
|
-
copy,
|
|
832
|
-
outputPad: context.outputPad,
|
|
833
|
-
stateHolder: context.state,
|
|
834
|
-
defaultLevel,
|
|
835
|
-
globalExpanded: renderOptions.expanded,
|
|
836
|
-
invalidate: () => context.invalidate(),
|
|
837
|
-
copyToClipboard: options?.copyToClipboard,
|
|
838
|
-
});
|
|
839
|
-
}
|
|
840
|
-
/**
|
|
841
|
-
* Streaming call view: operation headers recognized from the patch prefix
|
|
842
|
-
* preview as pending rows. `` only marks a recognized header — it promises
|
|
843
|
-
* neither validation nor application.
|
|
844
|
-
*/
|
|
845
|
-
function renderApplyPatchCallView(params) {
|
|
846
|
-
const { theme, context, defaultLevel, options } = params;
|
|
847
|
-
const patch = isRecord(context.args) && typeof context.args.patch === "string" ? context.args.patch : "";
|
|
848
|
-
const host = isRecord(context.args) &&
|
|
849
|
-
typeof context.args.target === "string" &&
|
|
850
|
-
context.args.target !== "local"
|
|
851
|
-
? context.args.target
|
|
852
|
-
: undefined;
|
|
853
|
-
// The preview cursor lives in the shared render state so each streamed update
|
|
854
|
-
// only processes new lines; an absent state degrades to a one-shot preview.
|
|
855
|
-
const state = isRecord(context.state) ? context.state : undefined;
|
|
856
|
-
const cursor = state?.[APPLY_PATCH_PREVIEW_STATE_KEY];
|
|
857
|
-
const stored = typeof cursor === "object" && cursor !== null ? cursor : undefined;
|
|
858
|
-
const operations = previewV4aOperations(patch, context.argsComplete === true, stored);
|
|
859
|
-
if (state !== undefined && stored !== undefined) {
|
|
860
|
-
state[APPLY_PATCH_PREVIEW_STATE_KEY] = stored;
|
|
861
|
-
}
|
|
862
|
-
const files = new Set(operations.map((operation) => operation.path)).size;
|
|
863
|
-
const summary = files > 0 ? `${String(files)} ${files === 1 ? "file" : "files"}` : "";
|
|
864
|
-
const rows = operations.map((operation) => formatApplyPatchOperationRow({
|
|
865
|
-
operationIndex: 0,
|
|
866
|
-
kind: operation.kind,
|
|
867
|
-
path: operation.path,
|
|
868
|
-
addedLines: operation.addedLines,
|
|
869
|
-
removedLines: operation.removedLines,
|
|
870
|
-
status: "pending",
|
|
871
|
-
appliedHunks: undefined,
|
|
872
|
-
totalHunks: undefined,
|
|
873
|
-
partialReason: undefined,
|
|
874
|
-
}, theme, host));
|
|
875
|
-
return new ToolView({
|
|
876
|
-
theme,
|
|
877
|
-
header: {
|
|
878
|
-
title: APPLY_PATCH_TOOL_NAME,
|
|
879
|
-
status: context.isError ? "error" : "none",
|
|
880
|
-
summary,
|
|
881
|
-
},
|
|
882
|
-
collapsedRows: rows,
|
|
883
|
-
sections: [{ railToken: "dim", content: rows }],
|
|
884
|
-
copy: patch !== "" ? { primary: patch } : undefined,
|
|
885
|
-
outputPad: context.outputPad,
|
|
886
|
-
stateHolder: context.state,
|
|
887
|
-
defaultLevel,
|
|
888
|
-
globalExpanded: context.expanded,
|
|
889
|
-
invalidate: () => context.invalidate(),
|
|
890
|
-
copyToClipboard: options?.copyToClipboard,
|
|
891
|
-
});
|
|
892
|
-
}
|
|
893
|
-
function renderApplyPatchResultView(params) {
|
|
894
|
-
const { theme, context, result, renderOptions, defaultLevel, options } = params;
|
|
895
|
-
const facts = applyPatchFacts(result.details);
|
|
896
|
-
const isError = context.isError || result.isError;
|
|
897
|
-
const host = facts?.target;
|
|
898
|
-
// The aggregated status is the executor's fact; success hides the top glyph.
|
|
899
|
-
const status = facts === undefined
|
|
900
|
-
? isError
|
|
901
|
-
? "error"
|
|
902
|
-
: renderOptions.isPartial
|
|
903
|
-
? "running"
|
|
904
|
-
: "ok"
|
|
905
|
-
: facts.status === "failed"
|
|
906
|
-
? "error"
|
|
907
|
-
: facts.status === "partial"
|
|
908
|
-
? "warn"
|
|
909
|
-
: "none";
|
|
910
|
-
const output = stripTerminalSequences(extractResultText(APPLY_PATCH_TOOL_NAME, result).outputText);
|
|
911
|
-
const parts = [];
|
|
912
|
-
if (facts !== undefined) {
|
|
913
|
-
const files = facts.operations.length;
|
|
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)}`);
|
|
918
|
-
}
|
|
919
|
-
const durText = formatDurationMs(context.durationMs);
|
|
920
|
-
if (durText !== "")
|
|
921
|
-
parts.push(durText);
|
|
922
|
-
const summary = parts.join(" · ");
|
|
923
|
-
const rows = facts === undefined
|
|
924
|
-
? []
|
|
925
|
-
: facts.operations.map((operation) => formatApplyPatchOperationRow(operation, theme, host));
|
|
926
|
-
const sections = rows.length > 0 ? [{ railToken: "dim", content: rows }] : [];
|
|
927
|
-
if (facts === undefined && output !== "") {
|
|
928
|
-
sections.push({ railToken: isError ? "error" : "dim", content: output });
|
|
929
|
-
}
|
|
930
|
-
else if (facts !== undefined) {
|
|
931
|
-
for (const snapshot of facts.snapshots) {
|
|
932
|
-
const hunks = snapshotHunks(snapshot);
|
|
933
|
-
if (hunks !== undefined && hunks.length > 0) {
|
|
934
|
-
sections.push({
|
|
935
|
-
railToken: "dim",
|
|
936
|
-
content: new DiffView({ theme, source: { kind: "hunks", hunks } }),
|
|
937
|
-
});
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
const diagnosis = (entries, glyphStatus) => entries.flatMap((entry) => [
|
|
941
|
-
`${applyPatchGlyph(glyphStatus, theme)} ${entry.paths[0] ?? "<unknown>"} · ${entry.error}`,
|
|
942
|
-
...entry.diagnostics.map((diagnostic) => `${applyPatchGlyph(glyphStatus, theme)} ${entry.paths[0] ?? "<unknown>"} · ${diagnostic.text}`),
|
|
943
|
-
]);
|
|
944
|
-
const failureRows = [
|
|
945
|
-
...diagnosis(facts.rejected, "rejected"),
|
|
946
|
-
...diagnosis(facts.unconfirmed, "unconfirmed"),
|
|
947
|
-
...diagnosis(facts.notApplied, "not_applied"),
|
|
948
|
-
];
|
|
949
|
-
if (failureRows.length > 0) {
|
|
950
|
-
sections.push({ railToken: "error", content: failureRows });
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
const patch = isRecord(context.args) && typeof context.args.patch === "string" ? context.args.patch : "";
|
|
954
|
-
const copy = patch !== ""
|
|
955
|
-
? { primary: patch, full: output !== "" ? `${patch}\n---\n${output}` : patch }
|
|
956
|
-
: output !== ""
|
|
957
|
-
? { primary: output }
|
|
958
|
-
: undefined;
|
|
959
|
-
return new ToolView({
|
|
960
|
-
theme,
|
|
961
|
-
header: { title: APPLY_PATCH_TOOL_NAME, status, summary },
|
|
962
|
-
collapsedRows: rows.length > 0 ? rows : undefined,
|
|
963
|
-
sections,
|
|
964
|
-
copy,
|
|
965
|
-
outputPad: context.outputPad,
|
|
966
|
-
stateHolder: context.state,
|
|
967
|
-
defaultLevel,
|
|
968
|
-
globalExpanded: renderOptions.expanded,
|
|
969
|
-
invalidate: () => context.invalidate(),
|
|
970
|
-
copyToClipboard: options?.copyToClipboard,
|
|
971
|
-
});
|
|
972
|
-
}
|
|
973
|
-
/** Hunks of one stored before/after snapshot; no file context is invented. */
|
|
974
|
-
function snapshotHunks(snapshot) {
|
|
975
|
-
if (snapshot.before.length === 0 && snapshot.after.length === 0)
|
|
976
|
-
return undefined;
|
|
977
|
-
return lineHunks(snapshot.before.join("\n"), snapshot.after.join("\n"), snapshot.startLine);
|
|
99
|
+
return formatToolHeaderText({
|
|
100
|
+
status,
|
|
101
|
+
title: call.toolName,
|
|
102
|
+
summary: [inline?.summary ?? truncatedArgs, formatDurationMs(call.durationMs), costText]
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.join(" · "),
|
|
105
|
+
}, theme);
|
|
978
106
|
}
|
|
979
107
|
function shouldInterceptTool(toolName, options) {
|
|
980
108
|
if (options?.catchAll === true) {
|
|
@@ -989,35 +117,32 @@ function shouldInterceptTool(toolName, options) {
|
|
|
989
117
|
}
|
|
990
118
|
return SPECIALIZED_TOOL_SET.has(toolName) || WEB_ACCESS_TOOL_SET.has(toolName);
|
|
991
119
|
}
|
|
992
|
-
function renderSpecializedResult(params) {
|
|
993
|
-
const { toolName } = params;
|
|
994
|
-
if (toolName === EDIT_TOOL_NAME)
|
|
995
|
-
return renderEditResult(params);
|
|
996
|
-
if (toolName === WRITE_TOOL_NAME)
|
|
997
|
-
return renderWriteResult(params);
|
|
998
|
-
if (toolName === PYTHON_EVAL_TOOL_NAME)
|
|
999
|
-
return renderPythonEvalResult(params);
|
|
1000
|
-
if (toolName === APPLY_PATCH_TOOL_NAME)
|
|
1001
|
-
return renderApplyPatchResultView(params);
|
|
1002
|
-
return undefined;
|
|
1003
|
-
}
|
|
1004
120
|
function renderGenericResult(params) {
|
|
1005
|
-
const { toolName, theme, context, result, renderOptions, defaultLevel, originalRenderCall, originalRenderResult, options, } = params;
|
|
121
|
+
const { toolName, theme, context, result, renderOptions, defaultLevel, originalRenderCall, originalRenderResult, resolvePresentation, options, } = params;
|
|
1006
122
|
const heavyCmd = extractHeavyInputCommand(toolName, context.args);
|
|
1007
123
|
const nestedCalls = extractNestedCalls(result);
|
|
1008
|
-
const
|
|
124
|
+
const captured = toolName === "codemode"
|
|
125
|
+
? (getCapturedNestedCalls(context.toolCallId) ??
|
|
126
|
+
(isRecord(result.details) &&
|
|
127
|
+
Array.isArray(result.details.__piExtUiCapturedCalls)
|
|
128
|
+
? result.details
|
|
129
|
+
.__piExtUiCapturedCalls
|
|
130
|
+
: undefined))
|
|
131
|
+
: undefined;
|
|
132
|
+
const childCalls = captured ?? nestedCalls;
|
|
133
|
+
const isMetaTool = toolName === "codemode" || toolName === "subagent" || childCalls.length > 0;
|
|
1009
134
|
const { outputText: rawOutput, wallTimeSeconds } = extractResultText(toolName, result);
|
|
1010
135
|
const isError = (context.isError ?? false) || (result.isError ?? false);
|
|
1011
136
|
const status = toolName === "codemode"
|
|
1012
|
-
?
|
|
137
|
+
? childCalls.length === 0 && isError
|
|
1013
138
|
? "error"
|
|
1014
139
|
: "none"
|
|
1015
140
|
: isMetaTool
|
|
1016
141
|
? resolveContainerStatus({
|
|
1017
142
|
isError,
|
|
1018
143
|
isPartial: renderOptions.isPartial === true,
|
|
1019
|
-
hasChildren:
|
|
1020
|
-
selfFailed:
|
|
144
|
+
hasChildren: childCalls.length > 0,
|
|
145
|
+
selfFailed: childCalls.length === 0 && isError,
|
|
1021
146
|
})
|
|
1022
147
|
: isError
|
|
1023
148
|
? "error"
|
|
@@ -1038,9 +163,9 @@ function renderGenericResult(params) {
|
|
|
1038
163
|
if (stripped !== "")
|
|
1039
164
|
summaryParts.push(stripped);
|
|
1040
165
|
}
|
|
1041
|
-
if (isMetaTool &&
|
|
1042
|
-
const failCount =
|
|
1043
|
-
summaryParts.push(...callStatsParts(
|
|
166
|
+
if (isMetaTool && childCalls.length > 0) {
|
|
167
|
+
const failCount = childCalls.filter((c) => Boolean(c.isError || ("status" in c && c.status === "error"))).length;
|
|
168
|
+
summaryParts.push(...callStatsParts(childCalls.length, failCount));
|
|
1044
169
|
}
|
|
1045
170
|
if (context.durationMs !== undefined) {
|
|
1046
171
|
summaryParts.push(formatDurationMs(context.durationMs));
|
|
@@ -1048,28 +173,75 @@ function renderGenericResult(params) {
|
|
|
1048
173
|
else if (wallTimeSeconds !== undefined && Number.isFinite(wallTimeSeconds)) {
|
|
1049
174
|
summaryParts.push(formatDurationMs(wallTimeSeconds * 1000));
|
|
1050
175
|
}
|
|
1051
|
-
const totalCost =
|
|
176
|
+
const totalCost = childCalls.reduce((sum, c) => sum + (c.cost ?? 0), 0);
|
|
1052
177
|
if (totalCost > 0) {
|
|
1053
178
|
summaryParts.push(formatCostUsd(totalCost));
|
|
1054
179
|
}
|
|
1055
180
|
const summary = summaryParts.join(" · ");
|
|
1056
|
-
const
|
|
1057
|
-
?
|
|
181
|
+
const codemodeNestedView = toolName === "codemode" && childCalls.length > 0
|
|
182
|
+
? new NestedCallsView({
|
|
183
|
+
calls: childCalls,
|
|
184
|
+
theme,
|
|
185
|
+
context,
|
|
186
|
+
resolveChildRenderer: (childName) => {
|
|
187
|
+
return {
|
|
188
|
+
renderResult: (childResult, childOptions, childTheme, childCtx) => {
|
|
189
|
+
const provider = resolvePresentation?.(childName);
|
|
190
|
+
if (provider !== undefined) {
|
|
191
|
+
return renderOwnedPresentation({
|
|
192
|
+
toolName: childName,
|
|
193
|
+
presentation: provider(childCtx.args, childResult, childTheme, childCtx),
|
|
194
|
+
result: childResult,
|
|
195
|
+
theme: childTheme,
|
|
196
|
+
context: childCtx,
|
|
197
|
+
defaultLevel: childOptions.expanded ? 2 : 1,
|
|
198
|
+
options,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return renderGenericResult({
|
|
202
|
+
toolName: childName,
|
|
203
|
+
theme: childTheme,
|
|
204
|
+
context: childCtx,
|
|
205
|
+
result: childResult,
|
|
206
|
+
renderOptions: childOptions,
|
|
207
|
+
defaultLevel: childOptions.expanded ? 2 : 1,
|
|
208
|
+
originalRenderCall: undefined,
|
|
209
|
+
originalRenderResult: undefined,
|
|
210
|
+
resolvePresentation,
|
|
211
|
+
options,
|
|
212
|
+
});
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
},
|
|
216
|
+
copyToClipboard: options?.copyToClipboard,
|
|
217
|
+
stateHolder: context.state,
|
|
218
|
+
resolvePresentation,
|
|
219
|
+
invalidate: () => context.invalidate(),
|
|
220
|
+
})
|
|
1058
221
|
: undefined;
|
|
1059
|
-
const
|
|
222
|
+
const collapsedRows = codemodeNestedView !== undefined
|
|
223
|
+
? codemodeNestedView
|
|
224
|
+
: isMetaTool && nestedCalls.length > 0
|
|
225
|
+
? nestedCalls.map((call) => formatNestedCallRow(call, theme))
|
|
226
|
+
: undefined;
|
|
227
|
+
const toolRailToken = isError
|
|
228
|
+
? "error"
|
|
229
|
+
: renderOptions.isPartial
|
|
230
|
+
? "accent"
|
|
231
|
+
: "success";
|
|
232
|
+
// Upper room (input code) uses neutral/muted rail; lower room (output) uses toolRailToken (success/error)
|
|
233
|
+
const inputSection = createInputCodeSection(toolName, context.args, theme, "muted");
|
|
1060
234
|
const sections = inputSection !== undefined ? [inputSection] : [];
|
|
1061
|
-
const outputRailToken = isError ? "error" : renderOptions.isPartial ? "accent" : "dim";
|
|
1062
235
|
if (toolName === "codemode") {
|
|
236
|
+
// Nested tool calls belong only to collapsedRows (Level 0), hidden when expanded
|
|
1063
237
|
if (rawOutput !== "") {
|
|
1064
|
-
const
|
|
1065
|
-
const
|
|
1066
|
-
.
|
|
1067
|
-
.
|
|
1068
|
-
.split("\n")
|
|
1069
|
-
.map((line) => theme.fg(outputColor, line));
|
|
238
|
+
const rawLines = rawOutput.replace(/\r/g, "").replace(/\t/g, " ").split("\n");
|
|
239
|
+
const outputLines = isError
|
|
240
|
+
? rawLines.map((line) => theme.fg("error", line))
|
|
241
|
+
: rawLines.map((line) => theme.fg("dim", line));
|
|
1070
242
|
sections.push({
|
|
1071
|
-
railToken:
|
|
1072
|
-
content:
|
|
243
|
+
railToken: toolRailToken,
|
|
244
|
+
content: outputLines,
|
|
1073
245
|
});
|
|
1074
246
|
}
|
|
1075
247
|
}
|
|
@@ -1093,31 +265,60 @@ function renderGenericResult(params) {
|
|
|
1093
265
|
invalidate() { },
|
|
1094
266
|
};
|
|
1095
267
|
sections.push({
|
|
1096
|
-
railToken:
|
|
268
|
+
railToken: toolRailToken,
|
|
1097
269
|
content: lazyUpstreamComponent,
|
|
1098
270
|
maxLines: effectiveExpanded ? Number.POSITIVE_INFINITY : 9,
|
|
1099
271
|
});
|
|
1100
272
|
}
|
|
1101
273
|
else if (rawOutput !== "") {
|
|
1102
|
-
const
|
|
1103
|
-
|
|
274
|
+
const rawLines = rawOutput.split("\n");
|
|
275
|
+
let outputLines;
|
|
276
|
+
if (isError) {
|
|
277
|
+
outputLines = rawLines.map((line) => theme.fg("error", line));
|
|
278
|
+
}
|
|
279
|
+
else if (toolName === "read" || toolPath(context.args) !== "") {
|
|
280
|
+
const filePath = toolPath(context.args);
|
|
281
|
+
const lang = filePath !== "" ? getLanguageFromPath(filePath) : undefined;
|
|
282
|
+
if (lang !== undefined) {
|
|
283
|
+
try {
|
|
284
|
+
outputLines = highlightCode(rawOutput, lang);
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
outputLines = rawLines;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
outputLines = rawLines;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
else if (toolName === "bash") {
|
|
295
|
+
outputLines = rawLines.map((line) => theme.fg("dim", line));
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
outputLines = rawLines;
|
|
299
|
+
}
|
|
1104
300
|
sections.push({
|
|
1105
|
-
railToken:
|
|
1106
|
-
content:
|
|
301
|
+
railToken: toolRailToken,
|
|
302
|
+
content: outputLines,
|
|
1107
303
|
});
|
|
1108
304
|
}
|
|
1109
305
|
let copy;
|
|
1110
|
-
if (
|
|
306
|
+
if (toolName === "codemode") {
|
|
307
|
+
copy = undefined;
|
|
308
|
+
}
|
|
309
|
+
else if (heavyCmd !== undefined && heavyCmd !== "") {
|
|
310
|
+
const cleanOutput = stripTerminalSequences(rawOutput);
|
|
1111
311
|
copy = {
|
|
1112
312
|
primary: heavyCmd,
|
|
1113
|
-
full: rawOutput !== "" ? `${heavyCmd}\n---\n${
|
|
313
|
+
full: rawOutput !== "" ? `${heavyCmd}\n---\n${cleanOutput}` : heavyCmd,
|
|
1114
314
|
};
|
|
1115
315
|
}
|
|
1116
316
|
else if (rawOutput !== "") {
|
|
317
|
+
const cleanOutput = stripTerminalSequences(rawOutput);
|
|
1117
318
|
const argsFormatted = formatArgsPreview(context.args);
|
|
1118
319
|
copy = {
|
|
1119
|
-
primary:
|
|
1120
|
-
full: argsFormatted !== "" ? `${argsFormatted}\n\n${
|
|
320
|
+
primary: cleanOutput,
|
|
321
|
+
full: argsFormatted !== "" ? `${argsFormatted}\n\n${cleanOutput}` : cleanOutput,
|
|
1121
322
|
};
|
|
1122
323
|
}
|
|
1123
324
|
return new ToolView({
|
|
@@ -1127,6 +328,7 @@ function renderGenericResult(params) {
|
|
|
1127
328
|
status,
|
|
1128
329
|
summary,
|
|
1129
330
|
},
|
|
331
|
+
railToken: toolRailToken,
|
|
1130
332
|
collapsedRows,
|
|
1131
333
|
sections,
|
|
1132
334
|
copy,
|
|
@@ -1138,11 +340,104 @@ function renderGenericResult(params) {
|
|
|
1138
340
|
copyToClipboard: options?.copyToClipboard,
|
|
1139
341
|
});
|
|
1140
342
|
}
|
|
343
|
+
/**
|
|
344
|
+
* The generic frame around one owner's presentation. The caller already resolved it, so a frame
|
|
345
|
+
* pass never asks the tool to rebuild its rows.
|
|
346
|
+
*/
|
|
347
|
+
function renderOwnedPresentation(params) {
|
|
348
|
+
const { toolName, presentation, result, theme, context, defaultLevel, options } = params;
|
|
349
|
+
const isError = context.isError || result?.isError === true;
|
|
350
|
+
const pending = result === undefined || context.isPartial;
|
|
351
|
+
const isDiff = !isError && !pending && presentation.kind === "diff";
|
|
352
|
+
// The tool owns its status: a container says "none" for a child's failure and a partial patch
|
|
353
|
+
// says "warn" even though the host marks the result failed. Only a provider without an opinion
|
|
354
|
+
// falls back to the frame's own reading of the execution.
|
|
355
|
+
const status = presentation.status ??
|
|
356
|
+
(isError
|
|
357
|
+
? "error"
|
|
358
|
+
: pending
|
|
359
|
+
? context.argsComplete
|
|
360
|
+
? "running"
|
|
361
|
+
: "streaming"
|
|
362
|
+
: isDiff
|
|
363
|
+
? "none"
|
|
364
|
+
: "ok");
|
|
365
|
+
const railToken = isError ? "error" : pending ? "accent" : "success";
|
|
366
|
+
const output = isError && result !== undefined
|
|
367
|
+
? agentResultText(result)
|
|
368
|
+
: (presentation.output ?? (result === undefined ? "" : agentResultText(result)));
|
|
369
|
+
const sections = [...(presentation.sections ?? [])];
|
|
370
|
+
if (presentation.sections === undefined && presentation.request) {
|
|
371
|
+
sections.push({ railToken: "muted", content: stripTerminalSequences(presentation.request) });
|
|
372
|
+
}
|
|
373
|
+
if (presentation.sections !== undefined) {
|
|
374
|
+
// Tool owns domain rows; this factory owns the frame and interaction only.
|
|
375
|
+
}
|
|
376
|
+
else if (isDiff) {
|
|
377
|
+
if (presentation.diff.trim() !== "") {
|
|
378
|
+
sections.push({
|
|
379
|
+
railToken: "dim",
|
|
380
|
+
content: new DiffView({
|
|
381
|
+
theme,
|
|
382
|
+
source: { kind: "unifiedPatch", text: presentation.diff },
|
|
383
|
+
}),
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
else if (output !== "") {
|
|
388
|
+
const lines = stripTerminalSequences(output).split("\n");
|
|
389
|
+
sections.push({
|
|
390
|
+
railToken,
|
|
391
|
+
content: isError ? lines.map((line) => theme.fg("error", line)) : lines,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
const duration = result === undefined ? "" : formatDurationMs(context.durationMs);
|
|
395
|
+
const summary = [stripTerminalSequences(presentation.summary), duration]
|
|
396
|
+
.filter(Boolean)
|
|
397
|
+
.join(" · ");
|
|
398
|
+
const copy = presentation.copy === undefined
|
|
399
|
+
? isDiff && presentation.diff.trim() !== ""
|
|
400
|
+
? { primary: presentation.diff }
|
|
401
|
+
: undefined
|
|
402
|
+
: {
|
|
403
|
+
primary: stripTerminalSequences(presentation.copy.primary),
|
|
404
|
+
...(presentation.copy.full === undefined
|
|
405
|
+
? {}
|
|
406
|
+
: { full: stripTerminalSequences(presentation.copy.full) }),
|
|
407
|
+
};
|
|
408
|
+
return new ToolView({
|
|
409
|
+
theme,
|
|
410
|
+
header: { title: toolName, status, summary },
|
|
411
|
+
railToken,
|
|
412
|
+
// The tool owns the folded rows and their tones; the frame only sanitizes them for the host.
|
|
413
|
+
collapsedRows: presentation.collapsedRows ??
|
|
414
|
+
(isError && output !== ""
|
|
415
|
+
? output
|
|
416
|
+
.split("\n")
|
|
417
|
+
.slice(0, 1)
|
|
418
|
+
.map((line) => theme.fg("error", stripTerminalSequences(line)))
|
|
419
|
+
: undefined),
|
|
420
|
+
sections,
|
|
421
|
+
copy,
|
|
422
|
+
outputPad: context.outputPad,
|
|
423
|
+
stateHolder: context.state,
|
|
424
|
+
defaultLevel,
|
|
425
|
+
globalExpanded: context.expanded,
|
|
426
|
+
invalidate: () => context.invalidate(),
|
|
427
|
+
copyToClipboard: options?.copyToClipboard,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
1141
430
|
export function createCollapsibleToolRendererResolver(options) {
|
|
431
|
+
return createRendererResolver(options);
|
|
432
|
+
}
|
|
433
|
+
function createRendererResolver(options, resolvePresentation) {
|
|
1142
434
|
const defaultLevel = options?.defaultCollapsed === false ? 1 : 0;
|
|
1143
435
|
return (toolName, next) => {
|
|
1144
436
|
const upstream = next();
|
|
1145
|
-
if (
|
|
437
|
+
if (hasOptionalToolRenderers(upstream))
|
|
438
|
+
return upstream;
|
|
439
|
+
const provider = getToolPresentationProvider(upstream) ?? resolvePresentation?.(toolName);
|
|
440
|
+
if (provider === undefined && !shouldInterceptTool(toolName, options)) {
|
|
1146
441
|
return upstream;
|
|
1147
442
|
}
|
|
1148
443
|
const originalRenderCall = upstream?.renderCall;
|
|
@@ -1150,33 +445,17 @@ export function createCollapsibleToolRendererResolver(options) {
|
|
|
1150
445
|
return {
|
|
1151
446
|
renderShell: "self",
|
|
1152
447
|
renderCall(args, theme, context) {
|
|
1153
|
-
if (
|
|
1154
|
-
const
|
|
448
|
+
if (provider !== undefined) {
|
|
449
|
+
const callContext = { ...context, args };
|
|
450
|
+
const view = renderOwnedPresentation({
|
|
451
|
+
toolName,
|
|
452
|
+
presentation: provider(args, undefined, theme, callContext),
|
|
1155
453
|
theme,
|
|
1156
|
-
context:
|
|
454
|
+
context: callContext,
|
|
1157
455
|
defaultLevel,
|
|
1158
456
|
options,
|
|
1159
457
|
});
|
|
1160
|
-
rememberCallView(context, view
|
|
1161
|
-
return view;
|
|
1162
|
-
}
|
|
1163
|
-
if (toolName === EDIT_TOOL_NAME) {
|
|
1164
|
-
const path = editPath(args);
|
|
1165
|
-
const view = new ToolView({
|
|
1166
|
-
theme,
|
|
1167
|
-
header: {
|
|
1168
|
-
title: EDIT_TOOL_NAME,
|
|
1169
|
-
status: context.isError ? "error" : "running",
|
|
1170
|
-
summary: path,
|
|
1171
|
-
},
|
|
1172
|
-
outputPad: context.outputPad,
|
|
1173
|
-
stateHolder: context.state,
|
|
1174
|
-
defaultLevel,
|
|
1175
|
-
globalExpanded: context.expanded,
|
|
1176
|
-
invalidate: () => context.invalidate(),
|
|
1177
|
-
copyToClipboard: options?.copyToClipboard,
|
|
1178
|
-
});
|
|
1179
|
-
rememberCallView(context, view, toolName);
|
|
458
|
+
rememberCallView(context, view);
|
|
1180
459
|
return view;
|
|
1181
460
|
}
|
|
1182
461
|
const isStreamingArgs = context.isPartial && !context.argsComplete;
|
|
@@ -1238,22 +517,34 @@ export function createCollapsibleToolRendererResolver(options) {
|
|
|
1238
517
|
invalidate: () => context.invalidate(),
|
|
1239
518
|
copyToClipboard: options?.copyToClipboard,
|
|
1240
519
|
});
|
|
1241
|
-
rememberCallView(context, view
|
|
520
|
+
rememberCallView(context, view);
|
|
1242
521
|
return view;
|
|
1243
522
|
},
|
|
1244
523
|
renderResult(result, renderOptions, theme, context) {
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
524
|
+
const resultContext = {
|
|
525
|
+
...context,
|
|
526
|
+
expanded: renderOptions.expanded,
|
|
527
|
+
isPartial: renderOptions.isPartial,
|
|
528
|
+
};
|
|
529
|
+
const presentation = provider === undefined
|
|
530
|
+
? undefined
|
|
531
|
+
: provider(resultContext.args, result, theme, resultContext);
|
|
532
|
+
// Without an owner, a container frame is the only name-based decision left, and it
|
|
533
|
+
// belongs to upstream codemode.
|
|
534
|
+
const replacesCallOnPartial = presentation?.replaceCallOnPartial === true ||
|
|
535
|
+
(presentation === undefined && SPECIALIZED_TOOL_SET.has(toolName));
|
|
536
|
+
hideCallView(context, replacesCallOnPartial, renderOptions.isPartial);
|
|
537
|
+
if (presentation !== undefined) {
|
|
538
|
+
return renderOwnedPresentation({
|
|
539
|
+
toolName,
|
|
540
|
+
presentation,
|
|
541
|
+
result,
|
|
542
|
+
theme,
|
|
543
|
+
context: resultContext,
|
|
544
|
+
defaultLevel,
|
|
545
|
+
options,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
1257
548
|
return renderGenericResult({
|
|
1258
549
|
toolName,
|
|
1259
550
|
theme,
|
|
@@ -1263,12 +554,38 @@ export function createCollapsibleToolRendererResolver(options) {
|
|
|
1263
554
|
defaultLevel,
|
|
1264
555
|
originalRenderCall,
|
|
1265
556
|
originalRenderResult,
|
|
557
|
+
resolvePresentation,
|
|
1266
558
|
options,
|
|
1267
559
|
});
|
|
1268
560
|
},
|
|
1269
561
|
};
|
|
1270
562
|
};
|
|
1271
563
|
}
|
|
564
|
+
/** UI factory consumed by package-owned direct renderers, independently of host resolver precedence. */
|
|
565
|
+
export function createOptionalToolRendererFactory(pi, options) {
|
|
566
|
+
return (name, native) => createRendererResolver({ ...options, tools: [name] }, (toolName) => getRegisteredToolPresentationProvider(pi, toolName))(name, () => native) ?? native;
|
|
567
|
+
}
|
|
1272
568
|
export function registerCollapsibleToolRenderer(pi, options) {
|
|
1273
|
-
pi.
|
|
569
|
+
if (typeof pi.on === "function") {
|
|
570
|
+
pi.on("tool_execution_start", (event) => {
|
|
571
|
+
recordToolExecutionStart(event);
|
|
572
|
+
});
|
|
573
|
+
pi.on("tool_execution_end", (event) => {
|
|
574
|
+
recordToolExecutionEnd(event);
|
|
575
|
+
});
|
|
576
|
+
pi.on("tool_result", (event) => {
|
|
577
|
+
const captured = getCapturedNestedCalls(event.toolCallId);
|
|
578
|
+
if (captured && captured.length > 0) {
|
|
579
|
+
const currentDetails = isRecord(event.details) ? event.details : {};
|
|
580
|
+
return {
|
|
581
|
+
details: {
|
|
582
|
+
...currentDetails,
|
|
583
|
+
__piExtUiCapturedCalls: captured,
|
|
584
|
+
},
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
return undefined;
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
pi.registerToolRenderer(createRendererResolver(options, (name) => getRegisteredToolPresentationProvider(pi, name)));
|
|
1274
591
|
}
|