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