@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
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
import { stripTerminalSequences } from "@earendil-works/pi-tui";
|
|
2
|
+
import { previewV4aOperations } from "./apply-patch-preview.js";
|
|
3
|
+
import { applyPatchFacts } from "./apply-patch-view.js";
|
|
4
|
+
import { diffStats } from "./diff-view.js";
|
|
5
|
+
export const APPLY_PATCH_TOOL_NAME = "apply_patch";
|
|
6
|
+
export const PYTHON_EVAL_TOOL_NAME = "python_eval";
|
|
7
|
+
export const PYTHON_EVAL_DETAILS_FORMAT = "pi-ext-tools-python-eval";
|
|
8
|
+
export const EDIT_TOOL_NAME = "edit";
|
|
9
|
+
export const WRITE_TOOL_NAME = "write";
|
|
10
|
+
export const EDIT_VIEW_KEY = "__piExtToolsEditView";
|
|
11
|
+
export const WRITE_VIEW_KEY = "__piExtToolsWriteView";
|
|
12
|
+
export const CODEMODE_OPTIONS_PREFIX = "// @options:";
|
|
13
|
+
export const CODEMODE_SCRIPT_HEADER = /^Script (?:completed|failed)\r?\nWall time ([\d.]+) seconds\r?\nOutput:\r?\n/;
|
|
14
|
+
export function isRecord(value) {
|
|
15
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
export function estimateStreamingTokens(args) {
|
|
18
|
+
if (args === undefined || args === null)
|
|
19
|
+
return 0;
|
|
20
|
+
try {
|
|
21
|
+
const serialized = typeof args === "string" ? args : JSON.stringify(args);
|
|
22
|
+
if (!serialized || serialized === "{}")
|
|
23
|
+
return 0;
|
|
24
|
+
return Math.max(1, Math.ceil(serialized.length / 4));
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function formatDurationMs(ms) {
|
|
31
|
+
if (ms === undefined || !Number.isFinite(ms))
|
|
32
|
+
return "";
|
|
33
|
+
return ms < 1000 ? `${String(Math.round(ms))}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
34
|
+
}
|
|
35
|
+
export function formatCostUsd(cost) {
|
|
36
|
+
if (cost === undefined || !Number.isFinite(cost) || cost <= 0)
|
|
37
|
+
return "";
|
|
38
|
+
return `$${cost >= 0.01 ? cost.toFixed(2) : cost.toPrecision(2)}`;
|
|
39
|
+
}
|
|
40
|
+
/** One header fact for a deadline, in the same shape codemode's `timeout_ms` renders as. */
|
|
41
|
+
export function formatTimeoutFact(timeoutMs) {
|
|
42
|
+
const label = timeoutMs >= 1000 ? `${String(Math.round(timeoutMs / 1000))}s` : `${String(timeoutMs)}ms`;
|
|
43
|
+
return `(timeout ${label})`;
|
|
44
|
+
}
|
|
45
|
+
export function extractCodemodeHeaderFacts(args) {
|
|
46
|
+
if (!isRecord(args))
|
|
47
|
+
return "";
|
|
48
|
+
const options = {};
|
|
49
|
+
for (const [key, value] of Object.entries(args)) {
|
|
50
|
+
if (key !== "code" && value !== undefined && value !== null) {
|
|
51
|
+
options[key] = value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (typeof args.code === "string") {
|
|
55
|
+
const firstLine = args.code.split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
56
|
+
if (firstLine.startsWith(CODEMODE_OPTIONS_PREFIX)) {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(firstLine.slice(CODEMODE_OPTIONS_PREFIX.length).trim());
|
|
59
|
+
if (isRecord(parsed)) {
|
|
60
|
+
Object.assign(options, parsed);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch { }
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const facts = [];
|
|
67
|
+
const timeoutMs = typeof options.timeout_ms === "number" ? options.timeout_ms : undefined;
|
|
68
|
+
if (timeoutMs !== undefined)
|
|
69
|
+
facts.push(formatTimeoutFact(timeoutMs));
|
|
70
|
+
if (typeof options.max_output_tokens === "number") {
|
|
71
|
+
facts.push(`(tokens ${String(options.max_output_tokens)})`);
|
|
72
|
+
}
|
|
73
|
+
if (typeof options.target === "string" && options.target !== "local") {
|
|
74
|
+
facts.push(`(${options.target})`);
|
|
75
|
+
}
|
|
76
|
+
return facts.join(" ");
|
|
77
|
+
}
|
|
78
|
+
export function parseCallArgs(raw) {
|
|
79
|
+
if (isRecord(raw))
|
|
80
|
+
return raw;
|
|
81
|
+
if (typeof raw !== "string")
|
|
82
|
+
return undefined;
|
|
83
|
+
const text = raw.trim();
|
|
84
|
+
if (text === "")
|
|
85
|
+
return undefined;
|
|
86
|
+
if (text.startsWith("{")) {
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(text);
|
|
89
|
+
if (isRecord(parsed))
|
|
90
|
+
return parsed;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
const record = {};
|
|
94
|
+
const extractString = (key) => {
|
|
95
|
+
const regex = new RegExp(`"${key}"\\s*:\\s*"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"`);
|
|
96
|
+
const match = regex.exec(text);
|
|
97
|
+
if (match?.[1]) {
|
|
98
|
+
record[key] = match[1].replace(/\\"/g, '"');
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const extractNumber = (key) => {
|
|
102
|
+
const regex = new RegExp(`"${key}"\\s*:\\s*(-?\\d+(?:\\.\\d+)?)`);
|
|
103
|
+
const match = regex.exec(text);
|
|
104
|
+
if (match?.[1]) {
|
|
105
|
+
record[key] = Number(match[1]);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
extractString("path");
|
|
109
|
+
extractString("file_path");
|
|
110
|
+
extractString("command");
|
|
111
|
+
extractString("pattern");
|
|
112
|
+
extractString("glob");
|
|
113
|
+
extractString("query");
|
|
114
|
+
extractString("url");
|
|
115
|
+
extractString("id");
|
|
116
|
+
extractString("agent");
|
|
117
|
+
extractString("topic");
|
|
118
|
+
extractString("source");
|
|
119
|
+
extractNumber("offset");
|
|
120
|
+
extractNumber("limit");
|
|
121
|
+
if (Object.keys(record).length > 0)
|
|
122
|
+
return record;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
export function extractSummaryArg(toolName, args) {
|
|
128
|
+
if (toolName === "codemode") {
|
|
129
|
+
return extractCodemodeHeaderFacts(args);
|
|
130
|
+
}
|
|
131
|
+
const parsed = parseCallArgs(args);
|
|
132
|
+
if (!parsed)
|
|
133
|
+
return "";
|
|
134
|
+
for (const key of [
|
|
135
|
+
"command",
|
|
136
|
+
"query",
|
|
137
|
+
"queries",
|
|
138
|
+
"url",
|
|
139
|
+
"urls",
|
|
140
|
+
"source",
|
|
141
|
+
"claim",
|
|
142
|
+
"id",
|
|
143
|
+
"search_id",
|
|
144
|
+
"topic",
|
|
145
|
+
"path",
|
|
146
|
+
"file_path",
|
|
147
|
+
"pattern",
|
|
148
|
+
"agent",
|
|
149
|
+
]) {
|
|
150
|
+
const val = parsed[key];
|
|
151
|
+
if (typeof val === "string" && val.trim() !== "") {
|
|
152
|
+
return stripTerminalSequences(val.trim());
|
|
153
|
+
}
|
|
154
|
+
if (Array.isArray(val) && val.length > 0 && typeof val[0] === "string") {
|
|
155
|
+
const first = stripTerminalSequences(val[0].trim());
|
|
156
|
+
return val.length > 1 ? `${first} (+${String(val.length - 1)})` : first;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return "";
|
|
160
|
+
}
|
|
161
|
+
export function extractHeavyInputCommand(toolName, args) {
|
|
162
|
+
if (!isRecord(args))
|
|
163
|
+
return undefined;
|
|
164
|
+
if (toolName === "bash" && typeof args.command === "string") {
|
|
165
|
+
return args.command.trim();
|
|
166
|
+
}
|
|
167
|
+
if (toolName === "python_eval" && typeof args.code === "string") {
|
|
168
|
+
return args.code.trim();
|
|
169
|
+
}
|
|
170
|
+
if (toolName === "codemode" && typeof args.code === "string") {
|
|
171
|
+
return args.code.trim();
|
|
172
|
+
}
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
export function extractNestedCalls(result) {
|
|
176
|
+
if (result === undefined || !isRecord(result.details))
|
|
177
|
+
return [];
|
|
178
|
+
const rawList = Array.isArray(result.details.calls)
|
|
179
|
+
? result.details.calls
|
|
180
|
+
: Array.isArray(result.details.nestedCalls)
|
|
181
|
+
? result.details.nestedCalls
|
|
182
|
+
: undefined;
|
|
183
|
+
if (rawList === undefined)
|
|
184
|
+
return [];
|
|
185
|
+
const items = [];
|
|
186
|
+
for (const entry of rawList) {
|
|
187
|
+
if (!isRecord(entry))
|
|
188
|
+
continue;
|
|
189
|
+
const name = typeof entry.name === "string"
|
|
190
|
+
? entry.name
|
|
191
|
+
: typeof entry.toolName === "string"
|
|
192
|
+
? entry.toolName
|
|
193
|
+
: undefined;
|
|
194
|
+
if (name === undefined)
|
|
195
|
+
continue;
|
|
196
|
+
const rawStatus = entry.status;
|
|
197
|
+
const status = rawStatus === "running" ||
|
|
198
|
+
rawStatus === "ok" ||
|
|
199
|
+
rawStatus === "error" ||
|
|
200
|
+
rawStatus === "cancelled"
|
|
201
|
+
? rawStatus
|
|
202
|
+
: undefined;
|
|
203
|
+
const summary = typeof entry.args === "string" && entry.args.trim() !== ""
|
|
204
|
+
? entry.args
|
|
205
|
+
: typeof entry.summary === "string"
|
|
206
|
+
? entry.summary
|
|
207
|
+
: undefined;
|
|
208
|
+
items.push({
|
|
209
|
+
toolName: name,
|
|
210
|
+
summary,
|
|
211
|
+
status,
|
|
212
|
+
isError: entry.isError === true || status === "error",
|
|
213
|
+
isWarning: entry.isWarning === true || status === "cancelled",
|
|
214
|
+
durationMs: typeof entry.durationMs === "number" ? entry.durationMs : undefined,
|
|
215
|
+
cost: typeof entry.cost === "number" ? entry.cost : undefined,
|
|
216
|
+
error: typeof entry.error === "string" ? entry.error : undefined,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return items;
|
|
220
|
+
}
|
|
221
|
+
export function extractResultText(toolName, result) {
|
|
222
|
+
const parts = [];
|
|
223
|
+
for (const block of result.content) {
|
|
224
|
+
if (block.type === "text" && block.text.trim() !== "") {
|
|
225
|
+
parts.push(block.text);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const joined = parts.join("\n");
|
|
229
|
+
if (toolName === "codemode") {
|
|
230
|
+
const stripped = stripTerminalSequences(joined);
|
|
231
|
+
const match = CODEMODE_SCRIPT_HEADER.exec(stripped);
|
|
232
|
+
const wallTimeSeconds = match?.[1] !== undefined ? Number.parseFloat(match[1]) : undefined;
|
|
233
|
+
const cleaned = joined.replace(CODEMODE_SCRIPT_HEADER, "").trim();
|
|
234
|
+
return { outputText: cleaned, wallTimeSeconds };
|
|
235
|
+
}
|
|
236
|
+
return { outputText: joined, wallTimeSeconds: undefined };
|
|
237
|
+
}
|
|
238
|
+
export function callStatsParts(total, failCount) {
|
|
239
|
+
if (failCount > 0) {
|
|
240
|
+
return [`${String(total - failCount)} ok`, `${String(failCount)} failed`];
|
|
241
|
+
}
|
|
242
|
+
return [`${String(total)} ${total === 1 ? "call" : "calls"}`];
|
|
243
|
+
}
|
|
244
|
+
export function resolveContainerStatus(params) {
|
|
245
|
+
if (params.selfFailed)
|
|
246
|
+
return "error";
|
|
247
|
+
if (params.hasChildren)
|
|
248
|
+
return "none";
|
|
249
|
+
if (params.isPartial)
|
|
250
|
+
return "running";
|
|
251
|
+
return params.isError ? "error" : "ok";
|
|
252
|
+
}
|
|
253
|
+
export function toolPath(args) {
|
|
254
|
+
const parsed = parseCallArgs(args);
|
|
255
|
+
if (!parsed)
|
|
256
|
+
return "";
|
|
257
|
+
const val = parsed.path ?? parsed.file_path;
|
|
258
|
+
return typeof val === "string" ? val.trim() : "";
|
|
259
|
+
}
|
|
260
|
+
export function editPath(args) {
|
|
261
|
+
return toolPath(args);
|
|
262
|
+
}
|
|
263
|
+
export function numericField(value, key) {
|
|
264
|
+
return isRecord(value) && typeof value[key] === "number" ? value[key] : undefined;
|
|
265
|
+
}
|
|
266
|
+
export function writeStoredDiff(lines) {
|
|
267
|
+
const hunks = [];
|
|
268
|
+
let hunk = [];
|
|
269
|
+
const finish = () => {
|
|
270
|
+
if (hunk.length > 0)
|
|
271
|
+
hunks.push(hunk);
|
|
272
|
+
hunk = [];
|
|
273
|
+
};
|
|
274
|
+
for (const entry of lines) {
|
|
275
|
+
if (!isRecord(entry))
|
|
276
|
+
return undefined;
|
|
277
|
+
if (entry.type === "sep") {
|
|
278
|
+
finish();
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (entry.type !== "add" && entry.type !== "del" && entry.type !== "ctx")
|
|
282
|
+
return undefined;
|
|
283
|
+
if (typeof entry.content !== "string")
|
|
284
|
+
return undefined;
|
|
285
|
+
const line = entry.type === "del" ? entry.oldNum : entry.newNum;
|
|
286
|
+
if (typeof line !== "number")
|
|
287
|
+
return undefined;
|
|
288
|
+
hunk.push({ kind: entry.type, line, text: entry.content });
|
|
289
|
+
}
|
|
290
|
+
finish();
|
|
291
|
+
return hunks.length === 0
|
|
292
|
+
? undefined
|
|
293
|
+
: { kind: "hunks", hunks: hunks.map((lines) => ({ lines })) };
|
|
294
|
+
}
|
|
295
|
+
/** Uses only the baseline and content already stored by the write tool. */
|
|
296
|
+
export function writePresentation(args, details) {
|
|
297
|
+
const view = isRecord(details) ? details[WRITE_VIEW_KEY] : undefined;
|
|
298
|
+
if (!isRecord(view))
|
|
299
|
+
return undefined;
|
|
300
|
+
if (view.kind === "noChange")
|
|
301
|
+
return { kind: "noChange" };
|
|
302
|
+
if (view.kind === "diff") {
|
|
303
|
+
const stored = Array.isArray(view.lines) ? writeStoredDiff(view.lines) : undefined;
|
|
304
|
+
if (stored === undefined)
|
|
305
|
+
return undefined;
|
|
306
|
+
const source = stored;
|
|
307
|
+
const stats = diffStats(source);
|
|
308
|
+
return stats === undefined
|
|
309
|
+
? undefined
|
|
310
|
+
: {
|
|
311
|
+
kind: "diff",
|
|
312
|
+
source,
|
|
313
|
+
added: numericField(view, "added") ?? stats.added,
|
|
314
|
+
removed: numericField(view, "removed") ?? stats.removed,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
if (view.kind !== "new" && view.kind !== "replace")
|
|
318
|
+
return undefined;
|
|
319
|
+
const content = typeof view.content === "string"
|
|
320
|
+
? view.content
|
|
321
|
+
: isRecord(args) && typeof args.content === "string"
|
|
322
|
+
? args.content
|
|
323
|
+
: undefined;
|
|
324
|
+
if (content === undefined)
|
|
325
|
+
return undefined;
|
|
326
|
+
const lines = numericField(view, "lines") ?? content.split("\n").length;
|
|
327
|
+
return {
|
|
328
|
+
kind: "content",
|
|
329
|
+
label: view.kind === "new" ? `new file (${String(lines)} lines)` : `wrote (${String(lines)} lines)`,
|
|
330
|
+
content,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
export function fileToolPresentation(toolName, args, details, isError) {
|
|
334
|
+
const path = toolPath(args);
|
|
335
|
+
if (toolName === "read") {
|
|
336
|
+
if (path === "")
|
|
337
|
+
return undefined;
|
|
338
|
+
const parsed = parseCallArgs(args);
|
|
339
|
+
let range = "";
|
|
340
|
+
if (typeof parsed?.offset === "number" || typeof parsed?.limit === "number") {
|
|
341
|
+
const start = typeof parsed.offset === "number" ? parsed.offset : 1;
|
|
342
|
+
const end = typeof parsed.limit === "number" ? start + parsed.limit - 1 : "";
|
|
343
|
+
range = `:${String(start)}${end ? `-${String(end)}` : ""}`;
|
|
344
|
+
}
|
|
345
|
+
return { summary: `${path}${range}`, status: isError ? "error" : "ok" };
|
|
346
|
+
}
|
|
347
|
+
if (toolName === WRITE_TOOL_NAME) {
|
|
348
|
+
const presentation = isError ? undefined : writePresentation(args, details);
|
|
349
|
+
const delta = presentation?.kind === "diff"
|
|
350
|
+
? ` (+${String(presentation.added)} -${String(presentation.removed)})`
|
|
351
|
+
: presentation?.kind === "content"
|
|
352
|
+
? ` (${presentation.label})`
|
|
353
|
+
: presentation?.kind === "noChange"
|
|
354
|
+
? " (no changes)"
|
|
355
|
+
: "";
|
|
356
|
+
if (path === "" && delta === "")
|
|
357
|
+
return undefined;
|
|
358
|
+
return {
|
|
359
|
+
summary: `${path}${delta}`.trim(),
|
|
360
|
+
status: isError ? "error" : presentation?.kind === "noChange" ? "none" : "ok",
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
/** Pi native edit persists a unified patch plus the same change as a numbered display diff. */
|
|
366
|
+
export function editDiffSource(details) {
|
|
367
|
+
if (!isRecord(details))
|
|
368
|
+
return undefined;
|
|
369
|
+
if (typeof details.patch === "string" && details.patch.trim() !== "") {
|
|
370
|
+
return { kind: "unifiedPatch", text: details.patch };
|
|
371
|
+
}
|
|
372
|
+
if (typeof details.diff === "string" && details.diff.trim() !== "") {
|
|
373
|
+
return displayDiffSource(details.diff);
|
|
374
|
+
}
|
|
375
|
+
return editViewSource(details[EDIT_VIEW_KEY]);
|
|
376
|
+
}
|
|
377
|
+
export function displayDiffSource(diff) {
|
|
378
|
+
const hunks = [];
|
|
379
|
+
let current = [];
|
|
380
|
+
let pendingBreak = false;
|
|
381
|
+
for (const raw of diff.split(/\r?\n/)) {
|
|
382
|
+
if (raw === "")
|
|
383
|
+
continue;
|
|
384
|
+
const sign = raw.charAt(0);
|
|
385
|
+
const match = /^ *(\d+)? ?(.*)$/.exec(raw.slice(1));
|
|
386
|
+
const lineNumber = match?.[1];
|
|
387
|
+
if (lineNumber === undefined) {
|
|
388
|
+
pendingBreak = true;
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
if (pendingBreak && current.length > 0) {
|
|
392
|
+
hunks.push(current);
|
|
393
|
+
current = [];
|
|
394
|
+
}
|
|
395
|
+
pendingBreak = false;
|
|
396
|
+
const kind = sign === "+" ? "add" : sign === "-" ? "del" : "ctx";
|
|
397
|
+
current.push({ kind, line: Number(lineNumber), text: match?.[2] ?? "" });
|
|
398
|
+
}
|
|
399
|
+
if (current.length === 0)
|
|
400
|
+
return undefined;
|
|
401
|
+
hunks.push(current);
|
|
402
|
+
return { kind: "hunks", hunks: hunks.map((lines) => ({ lines })) };
|
|
403
|
+
}
|
|
404
|
+
export function editViewSource(view) {
|
|
405
|
+
const hunks = editViewHunks(view);
|
|
406
|
+
return hunks.length === 0 ? undefined : { kind: "hunks", hunks };
|
|
407
|
+
}
|
|
408
|
+
export function editViewHunks(view) {
|
|
409
|
+
if (!isRecord(view))
|
|
410
|
+
return [];
|
|
411
|
+
const ops = view.kind === "single" && isRecord(view.op)
|
|
412
|
+
? [view.op]
|
|
413
|
+
: view.kind === "multi" && Array.isArray(view.ops)
|
|
414
|
+
? view.ops.filter(isRecord)
|
|
415
|
+
: [];
|
|
416
|
+
return ops.flatMap((op) => hunksForEditOp(op));
|
|
417
|
+
}
|
|
418
|
+
export function hunksForEditOp(op) {
|
|
419
|
+
if (typeof op.oldContent !== "string" || typeof op.newContent !== "string")
|
|
420
|
+
return [];
|
|
421
|
+
const startLine = typeof op.startLine === "number" && op.startLine > 0 ? op.startLine : 1;
|
|
422
|
+
return lineHunks(op.oldContent.replace(/\r\n/g, "\n"), op.newContent.replace(/\r\n/g, "\n"), startLine);
|
|
423
|
+
}
|
|
424
|
+
export function splitFragment(text) {
|
|
425
|
+
if (text === "")
|
|
426
|
+
return [];
|
|
427
|
+
const lines = text.split("\n");
|
|
428
|
+
if (lines.at(-1) === "")
|
|
429
|
+
lines.pop();
|
|
430
|
+
return lines;
|
|
431
|
+
}
|
|
432
|
+
/** Line LCS of one stored before/after fragment. No file context is invented. */
|
|
433
|
+
export function lineHunks(before, after, startLine) {
|
|
434
|
+
const oldLines = splitFragment(before);
|
|
435
|
+
const newLines = splitFragment(after);
|
|
436
|
+
const oldCount = oldLines.length;
|
|
437
|
+
const newCount = newLines.length;
|
|
438
|
+
const lengths = Array.from({ length: oldCount + 1 }, () => Array(newCount + 1).fill(0));
|
|
439
|
+
for (let oldIndex = oldCount - 1; oldIndex >= 0; oldIndex -= 1) {
|
|
440
|
+
const row = lengths[oldIndex];
|
|
441
|
+
const next = lengths[oldIndex + 1];
|
|
442
|
+
if (row === undefined || next === undefined)
|
|
443
|
+
continue;
|
|
444
|
+
for (let newIndex = newCount - 1; newIndex >= 0; newIndex -= 1) {
|
|
445
|
+
row[newIndex] =
|
|
446
|
+
oldLines[oldIndex] === newLines[newIndex]
|
|
447
|
+
? (next[newIndex + 1] ?? 0) + 1
|
|
448
|
+
: Math.max(next[newIndex] ?? 0, row[newIndex + 1] ?? 0);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const lines = [];
|
|
452
|
+
let oldIndex = 0;
|
|
453
|
+
let newIndex = 0;
|
|
454
|
+
let oldLine = startLine;
|
|
455
|
+
let newLine = startLine;
|
|
456
|
+
while (oldIndex < oldCount || newIndex < newCount) {
|
|
457
|
+
const row = lengths[oldIndex];
|
|
458
|
+
const next = lengths[oldIndex + 1];
|
|
459
|
+
if (oldIndex < oldCount && newIndex < newCount && oldLines[oldIndex] === newLines[newIndex]) {
|
|
460
|
+
lines.push({ kind: "ctx", line: newLine, text: oldLines[oldIndex] ?? "" });
|
|
461
|
+
oldIndex += 1;
|
|
462
|
+
newIndex += 1;
|
|
463
|
+
oldLine += 1;
|
|
464
|
+
newLine += 1;
|
|
465
|
+
}
|
|
466
|
+
else if (newIndex < newCount &&
|
|
467
|
+
(oldIndex === oldCount || (row?.[newIndex + 1] ?? 0) > (next?.[newIndex] ?? 0))) {
|
|
468
|
+
lines.push({ kind: "add", line: newLine, text: newLines[newIndex] ?? "" });
|
|
469
|
+
newIndex += 1;
|
|
470
|
+
newLine += 1;
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
lines.push({ kind: "del", line: oldLine, text: oldLines[oldIndex] ?? "" });
|
|
474
|
+
oldIndex += 1;
|
|
475
|
+
oldLine += 1;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const changed = lines.some((line) => line.kind !== "ctx");
|
|
479
|
+
return changed ? [{ lines }] : [];
|
|
480
|
+
}
|
|
481
|
+
export function patchTextForCopy(details) {
|
|
482
|
+
if (isRecord(details) && typeof details.patch === "string")
|
|
483
|
+
return details.patch;
|
|
484
|
+
const hunks = editViewHunks(isRecord(details) ? details[EDIT_VIEW_KEY] : undefined);
|
|
485
|
+
if (hunks.length === 0)
|
|
486
|
+
return undefined;
|
|
487
|
+
return (hunks
|
|
488
|
+
.map((hunk) => {
|
|
489
|
+
let oldStart = 0;
|
|
490
|
+
let newStart = 0;
|
|
491
|
+
let oldCount = 0;
|
|
492
|
+
let newCount = 0;
|
|
493
|
+
const body = hunk.lines.map((line) => {
|
|
494
|
+
if (line.kind !== "add") {
|
|
495
|
+
oldCount += 1;
|
|
496
|
+
if (oldStart === 0)
|
|
497
|
+
oldStart = line.line;
|
|
498
|
+
}
|
|
499
|
+
if (line.kind !== "del") {
|
|
500
|
+
newCount += 1;
|
|
501
|
+
if (newStart === 0)
|
|
502
|
+
newStart = line.line;
|
|
503
|
+
}
|
|
504
|
+
const sign = line.kind === "add" ? "+" : line.kind === "del" ? "-" : " ";
|
|
505
|
+
return sign + line.text;
|
|
506
|
+
});
|
|
507
|
+
if (body.length === 0)
|
|
508
|
+
return "";
|
|
509
|
+
const header = "@@ -" +
|
|
510
|
+
String(oldStart || 1) +
|
|
511
|
+
"," +
|
|
512
|
+
String(oldCount) +
|
|
513
|
+
" +" +
|
|
514
|
+
String(newStart || 1) +
|
|
515
|
+
"," +
|
|
516
|
+
String(newCount) +
|
|
517
|
+
" @@";
|
|
518
|
+
return [header, ...body].join("\n");
|
|
519
|
+
})
|
|
520
|
+
.filter((hunk) => hunk !== "")
|
|
521
|
+
.join("\n") || undefined);
|
|
522
|
+
}
|
|
523
|
+
export function patchSummary(files, added, removed) {
|
|
524
|
+
return `modify ${files}${files === 1 ? " file" : " files"} (+${added} -${removed})`;
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* The name-free part of a summary for tools whose call arguments read well on their own. The tool
|
|
528
|
+
* name is the row's title, so repeating it here would print it twice.
|
|
529
|
+
*/
|
|
530
|
+
function formatArgumentSummary(toolName, parsed) {
|
|
531
|
+
const path = typeof parsed.path === "string"
|
|
532
|
+
? parsed.path.trim()
|
|
533
|
+
: typeof parsed.file_path === "string"
|
|
534
|
+
? parsed.file_path.trim()
|
|
535
|
+
: "";
|
|
536
|
+
switch (toolName) {
|
|
537
|
+
case "bash": {
|
|
538
|
+
const cmd = typeof parsed.command === "string" ? parsed.command.trim() : "";
|
|
539
|
+
if (!cmd)
|
|
540
|
+
return undefined;
|
|
541
|
+
return cmd.length > 70 ? `${cmd.slice(0, 67)}…` : cmd;
|
|
542
|
+
}
|
|
543
|
+
case "grep":
|
|
544
|
+
case "find": {
|
|
545
|
+
const pattern = typeof parsed.pattern === "string" ? parsed.pattern.trim() : "";
|
|
546
|
+
if (!pattern)
|
|
547
|
+
return undefined;
|
|
548
|
+
const target = path && path !== "." ? ` ${path}` : "";
|
|
549
|
+
return `${pattern}${target}`;
|
|
550
|
+
}
|
|
551
|
+
default: {
|
|
552
|
+
const keyArg = extractSummaryArg(toolName, parsed);
|
|
553
|
+
return keyArg ? keyArg : undefined;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* One compact nested summary for a tool call. Adopts the tool's standard collapsed
|
|
559
|
+
* single-line format when recognized, degrading to undefined so callers can use
|
|
560
|
+
* the default argument rendering.
|
|
561
|
+
*/
|
|
562
|
+
export function inlinePresentation(toolName, args, details, isError) {
|
|
563
|
+
const fileTool = fileToolPresentation(toolName, args, details, isError);
|
|
564
|
+
if (fileTool !== undefined)
|
|
565
|
+
return fileTool;
|
|
566
|
+
const parsed = parseCallArgs(args);
|
|
567
|
+
if (toolName === APPLY_PATCH_TOOL_NAME) {
|
|
568
|
+
const facts = applyPatchFacts(details);
|
|
569
|
+
if (facts !== undefined) {
|
|
570
|
+
return {
|
|
571
|
+
summary: patchSummary(new Set(facts.changedPaths).size, facts.addedLines, facts.removedLines),
|
|
572
|
+
status: isError ? "error" : "ok",
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
if (parsed === undefined || typeof parsed.patch !== "string")
|
|
576
|
+
return undefined;
|
|
577
|
+
const operations = previewV4aOperations(parsed.patch, true, undefined);
|
|
578
|
+
return {
|
|
579
|
+
summary: patchSummary(new Set(operations.map((operation) => operation.path)).size, operations.reduce((sum, operation) => sum + operation.addedLines, 0), operations.reduce((sum, operation) => sum + operation.removedLines, 0)),
|
|
580
|
+
status: isError ? "error" : "ok",
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
if (parsed === undefined)
|
|
584
|
+
return undefined;
|
|
585
|
+
if (toolName === EDIT_TOOL_NAME) {
|
|
586
|
+
const path = toolPath(parsed);
|
|
587
|
+
const source = isError ? undefined : editDiffSource(details);
|
|
588
|
+
const stats = source === undefined ? undefined : diffStats(source);
|
|
589
|
+
if (path === "" && stats === undefined)
|
|
590
|
+
return undefined;
|
|
591
|
+
return {
|
|
592
|
+
summary: stats === undefined
|
|
593
|
+
? path
|
|
594
|
+
: `${path} (+${String(stats.added)} -${String(stats.removed)})`.trim(),
|
|
595
|
+
status: isError ? "error" : "ok",
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
const baseSummary = formatArgumentSummary(toolName, parsed);
|
|
599
|
+
if (baseSummary === undefined)
|
|
600
|
+
return undefined;
|
|
601
|
+
return {
|
|
602
|
+
summary: baseSummary,
|
|
603
|
+
status: isError ? "error" : "ok",
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
/** Reads only the persisted `pi-ext-tools-eval` details; other shapes degrade to plain output. */
|
|
607
|
+
export function pythonEvalFacts(details) {
|
|
608
|
+
if (!isRecord(details) || details.format !== PYTHON_EVAL_DETAILS_FORMAT)
|
|
609
|
+
return undefined;
|
|
610
|
+
const rawRows = Array.isArray(details.rows) ? details.rows : [];
|
|
611
|
+
const rows = [];
|
|
612
|
+
for (const entry of rawRows) {
|
|
613
|
+
if (!isRecord(entry))
|
|
614
|
+
continue;
|
|
615
|
+
if (entry.kind === "text" && typeof entry.text === "string") {
|
|
616
|
+
rows.push({ kind: "text", text: entry.text });
|
|
617
|
+
}
|
|
618
|
+
else if (entry.kind === "display" && typeof entry.text === "string") {
|
|
619
|
+
rows.push({ kind: "display", text: entry.text });
|
|
620
|
+
}
|
|
621
|
+
else if (entry.kind === "result" && typeof entry.text === "string") {
|
|
622
|
+
rows.push({ kind: "result", text: entry.text });
|
|
623
|
+
}
|
|
624
|
+
else if (entry.kind === "state" && typeof entry.text === "string") {
|
|
625
|
+
rows.push({ kind: "state", text: entry.text });
|
|
626
|
+
}
|
|
627
|
+
else if (entry.kind === "tool" && isRecord(entry.trace)) {
|
|
628
|
+
const trace = entry.trace;
|
|
629
|
+
if (typeof trace.name !== "string")
|
|
630
|
+
continue;
|
|
631
|
+
rows.push({
|
|
632
|
+
kind: "tool",
|
|
633
|
+
name: trace.name,
|
|
634
|
+
args: trace.args,
|
|
635
|
+
durationMs: typeof trace.durationMs === "number" ? trace.durationMs : undefined,
|
|
636
|
+
error: typeof trace.error === "string" ? trace.error : undefined,
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return {
|
|
641
|
+
rows,
|
|
642
|
+
error: typeof details.error === "string" ? details.error : undefined,
|
|
643
|
+
};
|
|
644
|
+
}
|
package/dist/renderer.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { type ToolStatusKind } from "./tool-view.js";
|
|
1
|
+
import { type ExtensionAPI, type ToolRendererResolver, type ToolRenderers } from "@earendil-works/pi-coding-agent";
|
|
3
2
|
export type ToolRenderContext = Parameters<NonNullable<ToolRenderers["renderCall"]>>[2];
|
|
4
3
|
/** Tools with a pi-ext-ui-owned renderer. Other tools stay on Pi's renderer. */
|
|
5
4
|
export declare const SPECIALIZED_TOOL_RENDERERS: readonly ["edit", "write", "apply_patch", "python_eval", "codemode"];
|
|
@@ -7,6 +6,7 @@ export type SpecializedToolRendererName = (typeof SPECIALIZED_TOOL_RENDERERS)[nu
|
|
|
7
6
|
/** Web-access tools use the shared folding shell while retaining their native body renderer. */
|
|
8
7
|
export declare const WEB_ACCESS_TOOLS: readonly ["web_search", "web_fetch", "web_open"];
|
|
9
8
|
export type WebAccessToolName = (typeof WEB_ACCESS_TOOLS)[number];
|
|
9
|
+
export { estimateStreamingTokens, inlinePresentation, type NestedCallRecord, } from "./payload-parser.js";
|
|
10
10
|
export interface CollapsibleToolRendererOptions {
|
|
11
11
|
readonly tools?: readonly string[] | undefined;
|
|
12
12
|
readonly match?: ((toolName: string) => boolean) | undefined;
|
|
@@ -14,29 +14,5 @@ export interface CollapsibleToolRendererOptions {
|
|
|
14
14
|
readonly defaultCollapsed?: boolean | undefined;
|
|
15
15
|
readonly copyToClipboard?: ((text: string) => Promise<void>) | undefined;
|
|
16
16
|
}
|
|
17
|
-
export interface NestedCallRecord {
|
|
18
|
-
readonly toolName: string;
|
|
19
|
-
readonly summary?: string | undefined;
|
|
20
|
-
readonly status?: "running" | "ok" | "error" | "cancelled" | undefined;
|
|
21
|
-
readonly isError?: boolean | undefined;
|
|
22
|
-
readonly isWarning?: boolean | undefined;
|
|
23
|
-
readonly durationMs?: number | undefined;
|
|
24
|
-
readonly cost?: number | undefined;
|
|
25
|
-
readonly error?: string | undefined;
|
|
26
|
-
}
|
|
27
|
-
export declare function estimateStreamingTokens(args: unknown): number;
|
|
28
|
-
export declare function extractCodemodeHeaderFacts(args: unknown): string;
|
|
29
|
-
/** Compact nested-row result: a one-line summary plus the status it reports. */
|
|
30
|
-
interface InlinePresentation {
|
|
31
|
-
readonly summary: string;
|
|
32
|
-
readonly status: ToolStatusKind;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* One compact nested summary for a file tool. Reports the delta only when the
|
|
36
|
-
* caller actually has result details; a codemode nested record keeps arguments
|
|
37
|
-
* alone, so it degrades to `<tool> <path>` instead of inventing counts.
|
|
38
|
-
*/
|
|
39
|
-
export declare function inlinePresentation(toolName: string, args: unknown, details: unknown, isError: boolean): InlinePresentation | undefined;
|
|
40
17
|
export declare function createCollapsibleToolRendererResolver(options?: CollapsibleToolRendererOptions): ToolRendererResolver;
|
|
41
18
|
export declare function registerCollapsibleToolRenderer(pi: ExtensionAPI, options?: CollapsibleToolRendererOptions): void;
|
|
42
|
-
export {};
|