@hheei/pi-ext-ui 0.0.0-stage → 0.1.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.
@@ -0,0 +1,1274 @@
1
+ import { stripTerminalSequences } from "@earendil-works/pi-tui";
2
+ import { applyPatchGlyph } from "./apply-patch-glyph.js";
3
+ import { previewV4aOperations } from "./apply-patch-preview.js";
4
+ import { applyPatchFacts, formatApplyPatchOperationRow, } from "./apply-patch-view.js";
5
+ import { DiffView, diffStats } from "./diff-view.js";
6
+ import { ToolView, } from "./tool-view.js";
7
+ /** Tools with a pi-ext-ui-owned renderer. Other tools stay on Pi's renderer. */
8
+ export const SPECIALIZED_TOOL_RENDERERS = [
9
+ "edit",
10
+ "write",
11
+ "apply_patch",
12
+ "python_eval",
13
+ "codemode",
14
+ ];
15
+ /** Web-access tools use the shared folding shell while retaining their native body renderer. */
16
+ export const WEB_ACCESS_TOOLS = ["web_search", "web_fetch", "web_open"];
17
+ const APPLY_PATCH_TOOL_NAME = "apply_patch";
18
+ const APPLY_PATCH_PREVIEW_STATE_KEY = "__piExtUiApplyPatchPreview";
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";
26
+ const SPECIALIZED_TOOL_SET = new Set(SPECIALIZED_TOOL_RENDERERS);
27
+ const WEB_ACCESS_TOOL_SET = new Set(WEB_ACCESS_TOOLS);
28
+ const CALL_VIEW_STATE_KEY = "__piExtUiCallView";
29
+ const CODEMODE_OPTIONS_PREFIX = "// @options:";
30
+ const CODEMODE_SCRIPT_HEADER = /^Script (?:completed|failed)\r?\nWall time ([\d.]+) seconds\r?\nOutput:\r?\n/;
31
+ function isRecord(value) {
32
+ return typeof value === "object" && value !== null && !Array.isArray(value);
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
+ }
138
+ return undefined;
139
+ }
140
+ function extractNestedCalls(result) {
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) {
218
+ const raw = extractHeavyInputCommand(toolName, args);
219
+ if (raw === undefined || raw === "")
220
+ return undefined;
221
+ const isShell = toolName === "bash";
222
+ const lines = raw
223
+ .replace(/\r/g, "")
224
+ .replace(/\t/g, " ")
225
+ .split("\n")
226
+ .map((line, i) => theme.fg("mdCode", isShell ? (i === 0 ? `$ ${line}` : ` ${line}`) : line));
227
+ return { railToken: "muted", content: lines };
228
+ }
229
+ function formatArgsPreview(args) {
230
+ if (args === undefined || args === null)
231
+ return "";
232
+ if (typeof args === "string")
233
+ return stripTerminalSequences(args);
234
+ try {
235
+ return JSON.stringify(args, null, 2);
236
+ }
237
+ catch {
238
+ return "";
239
+ }
240
+ }
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
+ /**
258
+ * The host stacks a tool execution's call and result views. Following the
259
+ * official edit renderer, the call view is stored in the shared renderer
260
+ * state so the result renderer can hide it in place within the same
261
+ * updateDisplay pass — no draw-time flags needed.
262
+ */
263
+ function rememberCallView(context, view, toolName) {
264
+ if (isRecord(context.state)) {
265
+ context.state[CALL_VIEW_STATE_KEY] = view;
266
+ }
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
+ }
276
+ function hideCallView(context, toolName, isPartial) {
277
+ if (toolName !== "codemode" &&
278
+ toolName !== PYTHON_EVAL_TOOL_NAME &&
279
+ toolName !== APPLY_PATCH_TOOL_NAME &&
280
+ isPartial)
281
+ return;
282
+ const view = isRecord(context.state) ? context.state[CALL_VIEW_STATE_KEY] : undefined;
283
+ if (view instanceof ToolView)
284
+ view.hide();
285
+ }
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
+ function formatNestedCallRow(call, theme) {
579
+ const inline = inlinePresentation(call.toolName, call.summary, undefined, call.isError === true);
580
+ const status = call.status === "running"
581
+ ? "running"
582
+ : call.isWarning === true
583
+ ? "warn"
584
+ : (inline?.status ?? (call.isError === true ? "error" : "ok"));
585
+ const glyph = nestedStatusGlyph(status, theme);
586
+ const name = theme.fg("toolTitle", inline?.summary ?? call.toolName);
587
+ const cleanArgs = inline !== undefined || call.summary === undefined
588
+ ? ""
589
+ : stripTerminalSequences(call.summary.trim());
590
+ 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
+ const costText = formatCostUsd(call.cost);
595
+ const costPart = costText !== "" ? ` ${theme.fg("dim", costText)}` : "";
596
+ return `${glyph} ${name}${summaryPart}${durationPart}${costPart}`;
597
+ }
598
+ function renderEditResult(params) {
599
+ const { theme, context, result, renderOptions, defaultLevel, options } = params;
600
+ const path = editPath(context.args);
601
+ const isError = context.isError || result.isError;
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);
978
+ }
979
+ function shouldInterceptTool(toolName, options) {
980
+ if (options?.catchAll === true) {
981
+ return options.match !== undefined ? options.match(toolName) : true;
982
+ }
983
+ if (options?.match?.(toolName)) {
984
+ return true;
985
+ }
986
+ const toolList = options?.tools;
987
+ if (toolList !== undefined) {
988
+ return toolList.includes(toolName);
989
+ }
990
+ return SPECIALIZED_TOOL_SET.has(toolName) || WEB_ACCESS_TOOL_SET.has(toolName);
991
+ }
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
+ function renderGenericResult(params) {
1005
+ const { toolName, theme, context, result, renderOptions, defaultLevel, originalRenderCall, originalRenderResult, options, } = params;
1006
+ const heavyCmd = extractHeavyInputCommand(toolName, context.args);
1007
+ const nestedCalls = extractNestedCalls(result);
1008
+ const isMetaTool = toolName === "codemode" || toolName === "subagent" || nestedCalls.length > 0;
1009
+ const { outputText: rawOutput, wallTimeSeconds } = extractResultText(toolName, result);
1010
+ const isError = (context.isError ?? false) || (result.isError ?? false);
1011
+ const status = toolName === "codemode"
1012
+ ? nestedCalls.length === 0 && isError
1013
+ ? "error"
1014
+ : "none"
1015
+ : isMetaTool
1016
+ ? resolveContainerStatus({
1017
+ isError,
1018
+ isPartial: renderOptions.isPartial === true,
1019
+ hasChildren: nestedCalls.length > 0,
1020
+ selfFailed: nestedCalls.length === 0 && isError,
1021
+ })
1022
+ : isError
1023
+ ? "error"
1024
+ : renderOptions.isPartial
1025
+ ? "running"
1026
+ : "ok";
1027
+ const summaryParts = [];
1028
+ const argSummary = extractSummaryArg(toolName, context.args);
1029
+ if (argSummary !== "") {
1030
+ summaryParts.push(argSummary);
1031
+ }
1032
+ else if (originalRenderCall !== undefined && toolName !== "codemode") {
1033
+ const upstreamLines = originalRenderCall(context.args, theme, context).render(120);
1034
+ const firstLine = stripTerminalSequences(upstreamLines[0] ?? "").trim();
1035
+ const stripped = firstLine.startsWith(toolName)
1036
+ ? firstLine.slice(toolName.length).trim()
1037
+ : firstLine;
1038
+ if (stripped !== "")
1039
+ summaryParts.push(stripped);
1040
+ }
1041
+ if (isMetaTool && nestedCalls.length > 0) {
1042
+ const failCount = nestedCalls.filter((c) => c.isError || c.isWarning).length;
1043
+ summaryParts.push(...callStatsParts(nestedCalls.length, failCount));
1044
+ }
1045
+ if (context.durationMs !== undefined) {
1046
+ summaryParts.push(formatDurationMs(context.durationMs));
1047
+ }
1048
+ else if (wallTimeSeconds !== undefined && Number.isFinite(wallTimeSeconds)) {
1049
+ summaryParts.push(formatDurationMs(wallTimeSeconds * 1000));
1050
+ }
1051
+ const totalCost = nestedCalls.reduce((sum, c) => sum + (c.cost ?? 0), 0);
1052
+ if (totalCost > 0) {
1053
+ summaryParts.push(formatCostUsd(totalCost));
1054
+ }
1055
+ const summary = summaryParts.join(" · ");
1056
+ const collapsedRows = isMetaTool && nestedCalls.length > 0
1057
+ ? nestedCalls.map((call) => formatNestedCallRow(call, theme))
1058
+ : undefined;
1059
+ const inputSection = createInputCodeSection(toolName, context.args, theme);
1060
+ const sections = inputSection !== undefined ? [inputSection] : [];
1061
+ const outputRailToken = isError ? "error" : renderOptions.isPartial ? "accent" : "dim";
1062
+ if (toolName === "codemode") {
1063
+ if (rawOutput !== "") {
1064
+ const outputColor = isError ? "error" : "toolOutput";
1065
+ const coloredLines = rawOutput
1066
+ .replace(/\r/g, "")
1067
+ .replace(/\t/g, " ")
1068
+ .split("\n")
1069
+ .map((line) => theme.fg(outputColor, line));
1070
+ sections.push({
1071
+ railToken: outputRailToken,
1072
+ content: coloredLines,
1073
+ });
1074
+ }
1075
+ }
1076
+ else if (originalRenderResult !== undefined) {
1077
+ const viewProbe = new ToolView({
1078
+ theme,
1079
+ header: { title: toolName },
1080
+ stateHolder: context.state,
1081
+ defaultLevel,
1082
+ globalExpanded: renderOptions.expanded,
1083
+ });
1084
+ const effectiveExpanded = viewProbe.effectiveLevel === 2;
1085
+ const effectiveOptions = {
1086
+ ...renderOptions,
1087
+ expanded: effectiveExpanded,
1088
+ };
1089
+ const lazyUpstreamComponent = {
1090
+ render(w) {
1091
+ return originalRenderResult(result, effectiveOptions, theme, context).render(w);
1092
+ },
1093
+ invalidate() { },
1094
+ };
1095
+ sections.push({
1096
+ railToken: outputRailToken,
1097
+ content: lazyUpstreamComponent,
1098
+ maxLines: effectiveExpanded ? Number.POSITIVE_INFINITY : 9,
1099
+ });
1100
+ }
1101
+ else if (rawOutput !== "") {
1102
+ const outputColor = isError ? "error" : "toolOutput";
1103
+ const coloredLines = rawOutput.split("\n").map((line) => theme.fg(outputColor, line));
1104
+ sections.push({
1105
+ railToken: outputRailToken,
1106
+ content: coloredLines,
1107
+ });
1108
+ }
1109
+ let copy;
1110
+ if (heavyCmd !== undefined && heavyCmd !== "") {
1111
+ copy = {
1112
+ primary: heavyCmd,
1113
+ full: rawOutput !== "" ? `${heavyCmd}\n---\n${rawOutput}` : heavyCmd,
1114
+ };
1115
+ }
1116
+ else if (rawOutput !== "") {
1117
+ const argsFormatted = formatArgsPreview(context.args);
1118
+ copy = {
1119
+ primary: rawOutput,
1120
+ full: argsFormatted !== "" ? `${argsFormatted}\n\n${rawOutput}` : rawOutput,
1121
+ };
1122
+ }
1123
+ return new ToolView({
1124
+ theme,
1125
+ header: {
1126
+ title: toolName,
1127
+ status,
1128
+ summary,
1129
+ },
1130
+ collapsedRows,
1131
+ sections,
1132
+ copy,
1133
+ outputPad: context.outputPad,
1134
+ stateHolder: context.state,
1135
+ defaultLevel,
1136
+ globalExpanded: renderOptions.expanded,
1137
+ invalidate: () => context.invalidate(),
1138
+ copyToClipboard: options?.copyToClipboard,
1139
+ });
1140
+ }
1141
+ export function createCollapsibleToolRendererResolver(options) {
1142
+ const defaultLevel = options?.defaultCollapsed === false ? 1 : 0;
1143
+ return (toolName, next) => {
1144
+ const upstream = next();
1145
+ if (!shouldInterceptTool(toolName, options)) {
1146
+ return upstream;
1147
+ }
1148
+ const originalRenderCall = upstream?.renderCall;
1149
+ const originalRenderResult = upstream?.renderResult;
1150
+ return {
1151
+ renderShell: "self",
1152
+ renderCall(args, theme, context) {
1153
+ if (toolName === APPLY_PATCH_TOOL_NAME) {
1154
+ const view = renderApplyPatchCallView({
1155
+ theme,
1156
+ context: { ...context, args },
1157
+ defaultLevel,
1158
+ options,
1159
+ });
1160
+ rememberCallView(context, view, toolName);
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);
1180
+ return view;
1181
+ }
1182
+ const isStreamingArgs = context.isPartial && !context.argsComplete;
1183
+ const status = context.isError
1184
+ ? "error"
1185
+ : toolName === "codemode"
1186
+ ? "none"
1187
+ : isStreamingArgs
1188
+ ? "streaming"
1189
+ : "running";
1190
+ let summary = "";
1191
+ if (isStreamingArgs) {
1192
+ const tokens = estimateStreamingTokens(args);
1193
+ summary = tokens > 0 ? `· ${String(tokens)} tokens` : "";
1194
+ }
1195
+ else {
1196
+ summary = extractSummaryArg(toolName, args);
1197
+ if (summary === "" && originalRenderCall !== undefined && toolName !== "codemode") {
1198
+ const upstreamLines = originalRenderCall(args, theme, context).render(120);
1199
+ const firstLine = stripTerminalSequences(upstreamLines[0] ?? "").trim();
1200
+ summary = firstLine.startsWith(toolName)
1201
+ ? firstLine.slice(toolName.length).trim()
1202
+ : firstLine;
1203
+ }
1204
+ }
1205
+ const heavyCmd = extractHeavyInputCommand(toolName, args);
1206
+ const sections = [];
1207
+ const inputSection = createInputCodeSection(toolName, args, theme);
1208
+ if (inputSection !== undefined) {
1209
+ sections.push(inputSection);
1210
+ }
1211
+ else {
1212
+ const preview = formatArgsPreview(args);
1213
+ if (preview !== "" && preview !== "{}") {
1214
+ sections.push({
1215
+ railToken: "muted",
1216
+ content: preview,
1217
+ });
1218
+ }
1219
+ }
1220
+ const copy = heavyCmd !== undefined && heavyCmd !== ""
1221
+ ? { primary: heavyCmd }
1222
+ : formatArgsPreview(args) !== ""
1223
+ ? { primary: formatArgsPreview(args) }
1224
+ : undefined;
1225
+ const view = new ToolView({
1226
+ theme,
1227
+ header: {
1228
+ title: toolName,
1229
+ status,
1230
+ summary,
1231
+ },
1232
+ sections,
1233
+ copy,
1234
+ outputPad: context.outputPad,
1235
+ stateHolder: context.state,
1236
+ defaultLevel,
1237
+ globalExpanded: context.expanded,
1238
+ invalidate: () => context.invalidate(),
1239
+ copyToClipboard: options?.copyToClipboard,
1240
+ });
1241
+ rememberCallView(context, view, toolName);
1242
+ return view;
1243
+ },
1244
+ renderResult(result, renderOptions, theme, context) {
1245
+ hideCallView(context, toolName, renderOptions.isPartial);
1246
+ const specialized = renderSpecializedResult({
1247
+ toolName,
1248
+ theme,
1249
+ context,
1250
+ result,
1251
+ renderOptions,
1252
+ defaultLevel,
1253
+ options,
1254
+ });
1255
+ if (specialized !== undefined)
1256
+ return specialized;
1257
+ return renderGenericResult({
1258
+ toolName,
1259
+ theme,
1260
+ context,
1261
+ result,
1262
+ renderOptions,
1263
+ defaultLevel,
1264
+ originalRenderCall,
1265
+ originalRenderResult,
1266
+ options,
1267
+ });
1268
+ },
1269
+ };
1270
+ };
1271
+ }
1272
+ export function registerCollapsibleToolRenderer(pi, options) {
1273
+ pi.registerToolRenderer(createCollapsibleToolRendererResolver(options));
1274
+ }