@hheei/pi-ext-ui 0.0.0-stage → 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.
@@ -0,0 +1,991 @@
1
+ import { getLanguageFromPath, highlightCode, } from "@earendil-works/pi-coding-agent";
2
+ import { stripTerminalSequences } from "@earendil-works/pi-tui";
3
+ import { agentResultText, getRegisteredToolPresentationProvider, getToolPresentationProvider, } from "@hheei/pi-ext-core";
4
+ import { applyPatchGlyph } from "./apply-patch-glyph.js";
5
+ import { previewV4aOperations } from "./apply-patch-preview.js";
6
+ import { applyPatchFacts, formatApplyPatchOperationRow, } from "./apply-patch-view.js";
7
+ import { DiffView, diffStats } from "./diff-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";
11
+ /** Tools with a pi-ext-ui-owned renderer. Other tools stay on Pi's renderer. */
12
+ export const SPECIALIZED_TOOL_RENDERERS = [
13
+ "edit",
14
+ "write",
15
+ "apply_patch",
16
+ "python_eval",
17
+ "codemode",
18
+ ];
19
+ /** Web-access tools use the shared folding shell while retaining their native body renderer. */
20
+ export const WEB_ACCESS_TOOLS = ["web_search", "web_fetch", "web_open"];
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";
23
+ const APPLY_PATCH_PREVIEW_STATE_KEY = "__piExtUiApplyPatchPreview";
24
+ const PYTHON_EVAL_TOOL_TITLE = "python_eval";
25
+ const SPECIALIZED_TOOL_SET = new Set(SPECIALIZED_TOOL_RENDERERS);
26
+ const WEB_ACCESS_TOOL_SET = new Set(WEB_ACCESS_TOOLS);
27
+ const CALL_VIEW_STATE_KEY = "__piExtUiCallView";
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";
35
+ return undefined;
36
+ }
37
+ function createInputCodeSection(toolName, args, theme, railToken = "muted") {
38
+ const raw = extractHeavyInputCommand(toolName, args);
39
+ if (raw === undefined || raw === "")
40
+ return undefined;
41
+ const isShell = toolName === "bash";
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 };
57
+ }
58
+ function formatArgsPreview(args) {
59
+ if (args === undefined || args === null)
60
+ return "";
61
+ if (typeof args === "string")
62
+ return stripTerminalSequences(args);
63
+ try {
64
+ return JSON.stringify(args, null, 2);
65
+ }
66
+ catch {
67
+ return "";
68
+ }
69
+ }
70
+ /**
71
+ * The host stacks a tool execution's call and result views. Following the
72
+ * official edit renderer, the call view is stored in the shared renderer
73
+ * state so the result renderer can hide it in place within the same
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.
78
+ */
79
+ function rememberCallView(context, view) {
80
+ if (isRecord(context.state)) {
81
+ context.state[CALL_VIEW_STATE_KEY] = view;
82
+ }
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
+ */
89
+ function hideCallView(context, toolName, isPartial) {
90
+ if (toolName !== "codemode" &&
91
+ toolName !== PYTHON_EVAL_TOOL_NAME &&
92
+ toolName !== APPLY_PATCH_TOOL_NAME &&
93
+ isPartial)
94
+ return;
95
+ const view = isRecord(context.state) ? context.state[CALL_VIEW_STATE_KEY] : undefined;
96
+ if (view instanceof ToolView)
97
+ view.hide();
98
+ }
99
+ function formatNestedCallRow(call, theme) {
100
+ const inline = inlinePresentation(call.toolName, call.summary, undefined, call.isError === true);
101
+ const status = call.status === "running"
102
+ ? "running"
103
+ : call.isWarning === true
104
+ ? "warn"
105
+ : (inline?.status ?? (call.isError === true ? "error" : "ok"));
106
+ const cleanArgs = inline !== undefined || call.summary === undefined
107
+ ? ""
108
+ : stripTerminalSequences(call.summary.trim());
109
+ const truncatedArgs = cleanArgs.length > 72 ? `${cleanArgs.slice(0, 69)}…` : cleanArgs;
110
+ const costText = formatCostUsd(call.cost);
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);
118
+ }
119
+ function renderEditResult(params) {
120
+ const { theme, context, result, renderOptions, defaultLevel, options } = params;
121
+ const path = editPath(context.args);
122
+ const isError = context.isError || result.isError;
123
+ const source = isError ? undefined : editDiffSource(result.details);
124
+ const stats = source === undefined ? undefined : diffStats(source);
125
+ const summaryParts = [
126
+ stats === undefined ? path : `${path} (+${String(stats.added)} -${String(stats.removed)})`,
127
+ ];
128
+ const durText = formatDurationMs(context.durationMs);
129
+ if (durText !== "")
130
+ summaryParts.push(durText);
131
+ const summary = summaryParts.join(" · ");
132
+ const output = stripTerminalSequences(extractResultText(EDIT_TOOL_NAME, result).outputText);
133
+ const patch = patchTextForCopy(result.details);
134
+ // A successful edit hides the top-level check mark; the diff body carries the change.
135
+ const status = isError
136
+ ? "error"
137
+ : renderOptions.isPartial
138
+ ? "running"
139
+ : source !== undefined
140
+ ? "none"
141
+ : "ok";
142
+ const sections = source === undefined
143
+ ? output === ""
144
+ ? []
145
+ : [{ railToken: isError ? "error" : "dim", content: output }]
146
+ : [{ railToken: "dim", content: new DiffView({ theme, source }) }];
147
+ const copy = patch !== undefined ? { primary: patch } : output !== "" ? { primary: output } : undefined;
148
+ return new ToolView({
149
+ theme,
150
+ header: { title: EDIT_TOOL_NAME, status, summary },
151
+ sections,
152
+ copy,
153
+ outputPad: context.outputPad,
154
+ stateHolder: context.state,
155
+ defaultLevel,
156
+ globalExpanded: renderOptions.expanded,
157
+ invalidate: () => context.invalidate(),
158
+ copyToClipboard: options?.copyToClipboard,
159
+ });
160
+ }
161
+ function renderWriteResult(params) {
162
+ const { theme, context, result, renderOptions, defaultLevel, options } = params;
163
+ const path = toolPath(context.args);
164
+ const isError = context.isError || result.isError;
165
+ const presentation = isError ? undefined : writePresentation(context.args, result.details);
166
+ const output = stripTerminalSequences(extractResultText(WRITE_TOOL_NAME, result).outputText);
167
+ const baseSummary = presentation?.kind === "diff"
168
+ ? `${path} (+${String(presentation.added)} -${String(presentation.removed)})`
169
+ : presentation?.kind === "content"
170
+ ? `${path} (${presentation.label})`
171
+ : presentation?.kind === "noChange"
172
+ ? `${path} (no changes)`
173
+ : path;
174
+ const durText = formatDurationMs(context.durationMs);
175
+ const summaryParts = [baseSummary];
176
+ if (durText !== "")
177
+ summaryParts.push(durText);
178
+ const summary = summaryParts.join(" · ");
179
+ const sections = presentation?.kind === "diff"
180
+ ? [{ railToken: "dim", content: new DiffView({ theme, source: presentation.source }) }]
181
+ : presentation?.kind === "content"
182
+ ? [{ railToken: "dim", content: presentation.content }]
183
+ : output === ""
184
+ ? []
185
+ : [{ railToken: isError ? "error" : "dim", content: output }];
186
+ const copyText = presentation?.kind === "content"
187
+ ? presentation.content
188
+ : presentation?.kind === "diff"
189
+ ? undefined
190
+ : output;
191
+ return new ToolView({
192
+ theme,
193
+ header: {
194
+ title: WRITE_TOOL_NAME,
195
+ status: isError
196
+ ? "error"
197
+ : renderOptions.isPartial
198
+ ? "running"
199
+ : presentation?.kind === "diff"
200
+ ? "none"
201
+ : presentation?.kind === "noChange"
202
+ ? "none"
203
+ : "ok",
204
+ summary,
205
+ },
206
+ sections,
207
+ copy: copyText !== undefined && copyText !== "" ? { primary: copyText } : undefined,
208
+ outputPad: context.outputPad,
209
+ stateHolder: context.state,
210
+ defaultLevel,
211
+ globalExpanded: renderOptions.expanded,
212
+ invalidate: () => context.invalidate(),
213
+ copyToClipboard: options?.copyToClipboard,
214
+ });
215
+ }
216
+ /** A nested file-tool row keeps its tool name; other tools degrade to name plus bounded args. */
217
+ function formatEvalToolRow(row, theme) {
218
+ const isError = row.error !== undefined;
219
+ const inline = inlinePresentation(row.name, row.args, undefined, isError);
220
+ const summary = typeof row.args === "string" && row.args.trim() !== ""
221
+ ? stripTerminalSequences(row.args.trim())
222
+ : "";
223
+ const truncated = summary.length > 72 ? `${summary.slice(0, 69)}…` : summary;
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);
231
+ }
232
+ function renderPythonEvalResult(params) {
233
+ const { theme, context, result, renderOptions, defaultLevel, options } = params;
234
+ const facts = pythonEvalFacts(result.details);
235
+ const isError = (context.isError ?? false) || (result.isError ?? false);
236
+ const toolRows = facts?.rows.filter((row) => row.kind === "tool") ??
237
+ [];
238
+ const anyToolError = toolRows.some((row) => row.error !== undefined);
239
+ const failed = isError || facts?.error !== undefined || anyToolError;
240
+ const status = resolveContainerStatus({
241
+ isError,
242
+ isPartial: renderOptions.isPartial === true,
243
+ hasChildren: toolRows.length > 0,
244
+ selfFailed: failed && !anyToolError,
245
+ });
246
+ const parts = [];
247
+ if (isRecord(context.args)) {
248
+ if (context.args.reset === true)
249
+ parts.push("(reset)");
250
+ if (typeof context.args.timeout_ms === "number")
251
+ parts.push(formatTimeoutFact(context.args.timeout_ms));
252
+ }
253
+ if (facts !== undefined) {
254
+ if (toolRows.length > 0) {
255
+ const failCount = toolRows.filter((row) => row.error !== undefined).length;
256
+ parts.push(...callStatsParts(toolRows.length, failCount));
257
+ }
258
+ else if (facts.rows.length > 0) {
259
+ parts.push(`${String(facts.rows.length)} ${facts.rows.length === 1 ? "row" : "rows"}`);
260
+ }
261
+ }
262
+ const durText = formatDurationMs(context.durationMs);
263
+ if (durText !== "")
264
+ parts.push(durText);
265
+ const summary = parts.join(" · ");
266
+ const collapsedRows = toolRows.length > 0 ? toolRows.map((row) => formatEvalToolRow(row, theme)) : undefined;
267
+ const code = extractHeavyInputCommand(PYTHON_EVAL_TOOL_NAME, context.args);
268
+ const inputSection = createInputCodeSection(PYTHON_EVAL_TOOL_NAME, context.args, theme);
269
+ const sections = inputSection !== undefined ? [inputSection] : [];
270
+ if (facts !== undefined) {
271
+ const outputRows = [];
272
+ for (const row of facts.rows) {
273
+ if (row.kind === "tool") {
274
+ outputRows.push(formatEvalToolRow(row, theme));
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
+ }
280
+ else if (row.kind === "display" || row.kind === "result") {
281
+ const prefix = theme.fg("dim", `${row.kind}:`);
282
+ const lines = row.text.replace(/\r/g, "").split("\n");
283
+ for (let i = 0; i < lines.length; i++) {
284
+ const lineText = theme.fg("dim", lines[i] ?? "");
285
+ outputRows.push(i === 0 ? `${prefix} ${lineText}` : ` ${lineText}`);
286
+ }
287
+ }
288
+ else if (row.text !== "") {
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))));
291
+ }
292
+ }
293
+ if (outputRows.length > 0) {
294
+ sections.push({
295
+ railToken: isError ? "error" : "success",
296
+ content: outputRows,
297
+ });
298
+ }
299
+ }
300
+ else {
301
+ const output = stripTerminalSequences(extractResultText(PYTHON_EVAL_TOOL_NAME, result).outputText);
302
+ if (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 });
308
+ }
309
+ }
310
+ const output = stripTerminalSequences(extractResultText(PYTHON_EVAL_TOOL_NAME, result).outputText);
311
+ const copy = code !== undefined && code !== ""
312
+ ? { primary: code, full: output !== "" ? `${code}\n---\n${output}` : code }
313
+ : output !== ""
314
+ ? { primary: output }
315
+ : undefined;
316
+ return new ToolView({
317
+ theme,
318
+ header: {
319
+ title: PYTHON_EVAL_TOOL_TITLE,
320
+ status,
321
+ summary,
322
+ },
323
+ collapsedRows,
324
+ sections,
325
+ copy,
326
+ outputPad: context.outputPad,
327
+ stateHolder: context.state,
328
+ defaultLevel,
329
+ globalExpanded: renderOptions.expanded,
330
+ invalidate: () => context.invalidate(),
331
+ copyToClipboard: options?.copyToClipboard,
332
+ });
333
+ }
334
+ /**
335
+ * Streaming call view: operation headers recognized from the patch prefix
336
+ * preview as pending rows. `󰄰` only marks a recognized header — it promises
337
+ * neither validation nor application.
338
+ */
339
+ function renderApplyPatchCallView(params) {
340
+ const { theme, context, defaultLevel, options } = params;
341
+ const patch = isRecord(context.args) && typeof context.args.patch === "string" ? context.args.patch : "";
342
+ const host = isRecord(context.args) &&
343
+ typeof context.args.target === "string" &&
344
+ context.args.target !== "local"
345
+ ? context.args.target
346
+ : undefined;
347
+ // The preview cursor lives in the shared render state so each streamed update
348
+ // only processes new lines; an absent state degrades to a one-shot preview.
349
+ const state = isRecord(context.state) ? context.state : undefined;
350
+ const cursor = state?.[APPLY_PATCH_PREVIEW_STATE_KEY];
351
+ const stored = typeof cursor === "object" && cursor !== null ? cursor : undefined;
352
+ const operations = previewV4aOperations(patch, context.argsComplete === true, stored);
353
+ if (state !== undefined && stored !== undefined) {
354
+ state[APPLY_PATCH_PREVIEW_STATE_KEY] = stored;
355
+ }
356
+ const files = new Set(operations.map((operation) => operation.path)).size;
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
+ : "";
360
+ const rows = operations.map((operation) => formatApplyPatchOperationRow({
361
+ operationIndex: 0,
362
+ kind: operation.kind,
363
+ path: operation.path,
364
+ addedLines: operation.addedLines,
365
+ removedLines: operation.removedLines,
366
+ status: "pending",
367
+ appliedHunks: undefined,
368
+ totalHunks: undefined,
369
+ partialReason: undefined,
370
+ }, theme, host));
371
+ return new ToolView({
372
+ theme,
373
+ header: {
374
+ title: APPLY_PATCH_TOOL_NAME,
375
+ status: context.isError ? "error" : "none",
376
+ summary,
377
+ },
378
+ collapsedRows: rows,
379
+ sections: [{ railToken: "dim", content: rows }],
380
+ copy: patch !== "" ? { primary: patch } : undefined,
381
+ outputPad: context.outputPad,
382
+ stateHolder: context.state,
383
+ defaultLevel,
384
+ globalExpanded: context.expanded,
385
+ invalidate: () => context.invalidate(),
386
+ copyToClipboard: options?.copyToClipboard,
387
+ });
388
+ }
389
+ function renderApplyPatchResultView(params) {
390
+ const { theme, context, result, renderOptions, defaultLevel, options } = params;
391
+ const facts = applyPatchFacts(result.details);
392
+ const isError = context.isError || result.isError;
393
+ const host = facts?.target;
394
+ // The aggregated status is the executor's fact; success hides the top glyph.
395
+ const status = facts === undefined
396
+ ? isError
397
+ ? "error"
398
+ : renderOptions.isPartial
399
+ ? "running"
400
+ : "ok"
401
+ : facts.status === "failed"
402
+ ? "error"
403
+ : facts.status === "partial"
404
+ ? "warn"
405
+ : "none";
406
+ const output = stripTerminalSequences(extractResultText(APPLY_PATCH_TOOL_NAME, result).outputText);
407
+ const parts = [];
408
+ if (facts !== undefined) {
409
+ parts.push(patchSummary(new Set(facts.changedPaths).size, facts.addedLines, facts.removedLines));
410
+ }
411
+ const durText = formatDurationMs(context.durationMs);
412
+ if (durText !== "")
413
+ parts.push(durText);
414
+ const summary = parts.join(" · ");
415
+ const rows = facts === undefined
416
+ ? []
417
+ : facts.operations.map((operation) => formatApplyPatchOperationRow(operation, theme, host));
418
+ const sections = rows.length > 0 ? [{ railToken: "dim", content: rows }] : [];
419
+ if (facts === undefined && output !== "") {
420
+ sections.push({ railToken: isError ? "error" : "dim", content: output });
421
+ }
422
+ else if (facts !== undefined) {
423
+ for (const snapshot of facts.snapshots) {
424
+ const hunks = snapshotHunks(snapshot);
425
+ if (hunks !== undefined && hunks.length > 0) {
426
+ sections.push({
427
+ railToken: "dim",
428
+ content: new DiffView({ theme, source: { kind: "hunks", hunks } }),
429
+ });
430
+ }
431
+ }
432
+ const diagnosis = (entries, glyphStatus) => entries.flatMap((entry) => [
433
+ `${applyPatchGlyph(glyphStatus, theme)} ${entry.paths[0] ?? "<unknown>"} · ${entry.error}`,
434
+ ...entry.diagnostics.map((diagnostic) => `${applyPatchGlyph(glyphStatus, theme)} ${entry.paths[0] ?? "<unknown>"} · ${diagnostic.text}`),
435
+ ]);
436
+ const failureRows = [
437
+ ...diagnosis(facts.rejected, "rejected"),
438
+ ...diagnosis(facts.unconfirmed, "unconfirmed"),
439
+ ...diagnosis(facts.notApplied, "not_applied"),
440
+ ];
441
+ if (failureRows.length > 0) {
442
+ sections.push({ railToken: "error", content: failureRows });
443
+ }
444
+ }
445
+ const patch = isRecord(context.args) && typeof context.args.patch === "string" ? context.args.patch : "";
446
+ const copy = patch !== ""
447
+ ? { primary: patch, full: output !== "" ? `${patch}\n---\n${output}` : patch }
448
+ : output !== ""
449
+ ? { primary: output }
450
+ : undefined;
451
+ return new ToolView({
452
+ theme,
453
+ header: { title: APPLY_PATCH_TOOL_NAME, status, summary },
454
+ collapsedRows: rows.length > 0 ? rows : undefined,
455
+ sections,
456
+ copy,
457
+ outputPad: context.outputPad,
458
+ stateHolder: context.state,
459
+ defaultLevel,
460
+ globalExpanded: renderOptions.expanded,
461
+ invalidate: () => context.invalidate(),
462
+ copyToClipboard: options?.copyToClipboard,
463
+ });
464
+ }
465
+ /** Hunks of one stored before/after snapshot; no file context is invented. */
466
+ function snapshotHunks(snapshot) {
467
+ if (snapshot.before.length === 0 && snapshot.after.length === 0)
468
+ return undefined;
469
+ return lineHunks(snapshot.before.join("\n"), snapshot.after.join("\n"), snapshot.startLine);
470
+ }
471
+ function shouldInterceptTool(toolName, options) {
472
+ if (options?.catchAll === true) {
473
+ return options.match !== undefined ? options.match(toolName) : true;
474
+ }
475
+ if (options?.match?.(toolName)) {
476
+ return true;
477
+ }
478
+ const toolList = options?.tools;
479
+ if (toolList !== undefined) {
480
+ return toolList.includes(toolName);
481
+ }
482
+ return SPECIALIZED_TOOL_SET.has(toolName) || WEB_ACCESS_TOOL_SET.has(toolName);
483
+ }
484
+ function renderSpecializedResult(params) {
485
+ const { toolName } = params;
486
+ if (toolName === EDIT_TOOL_NAME)
487
+ return renderEditResult(params);
488
+ if (toolName === WRITE_TOOL_NAME)
489
+ return renderWriteResult(params);
490
+ if (toolName === PYTHON_EVAL_TOOL_NAME)
491
+ return renderPythonEvalResult(params);
492
+ if (toolName === APPLY_PATCH_TOOL_NAME)
493
+ return renderApplyPatchResultView(params);
494
+ return undefined;
495
+ }
496
+ function renderGenericResult(params) {
497
+ const { toolName, theme, context, result, renderOptions, defaultLevel, originalRenderCall, originalRenderResult, resolvePresentation, options, } = params;
498
+ const heavyCmd = extractHeavyInputCommand(toolName, context.args);
499
+ const nestedCalls = extractNestedCalls(result);
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;
510
+ const { outputText: rawOutput, wallTimeSeconds } = extractResultText(toolName, result);
511
+ const isError = (context.isError ?? false) || (result.isError ?? false);
512
+ const status = toolName === "codemode"
513
+ ? childCalls.length === 0 && isError
514
+ ? "error"
515
+ : "none"
516
+ : isMetaTool
517
+ ? resolveContainerStatus({
518
+ isError,
519
+ isPartial: renderOptions.isPartial === true,
520
+ hasChildren: childCalls.length > 0,
521
+ selfFailed: childCalls.length === 0 && isError,
522
+ })
523
+ : isError
524
+ ? "error"
525
+ : renderOptions.isPartial
526
+ ? "running"
527
+ : "ok";
528
+ const summaryParts = [];
529
+ const argSummary = extractSummaryArg(toolName, context.args);
530
+ if (argSummary !== "") {
531
+ summaryParts.push(argSummary);
532
+ }
533
+ else if (originalRenderCall !== undefined && toolName !== "codemode") {
534
+ const upstreamLines = originalRenderCall(context.args, theme, context).render(120);
535
+ const firstLine = stripTerminalSequences(upstreamLines[0] ?? "").trim();
536
+ const stripped = firstLine.startsWith(toolName)
537
+ ? firstLine.slice(toolName.length).trim()
538
+ : firstLine;
539
+ if (stripped !== "")
540
+ summaryParts.push(stripped);
541
+ }
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));
545
+ }
546
+ if (context.durationMs !== undefined) {
547
+ summaryParts.push(formatDurationMs(context.durationMs));
548
+ }
549
+ else if (wallTimeSeconds !== undefined && Number.isFinite(wallTimeSeconds)) {
550
+ summaryParts.push(formatDurationMs(wallTimeSeconds * 1000));
551
+ }
552
+ const totalCost = childCalls.reduce((sum, c) => sum + (c.cost ?? 0), 0);
553
+ if (totalCost > 0) {
554
+ summaryParts.push(formatCostUsd(totalCost));
555
+ }
556
+ const summary = summaryParts.join(" · ");
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
+ })
608
+ : undefined;
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");
621
+ const sections = inputSection !== undefined ? [inputSection] : [];
622
+ if (toolName === "codemode") {
623
+ // Nested tool calls belong only to collapsedRows (Level 0), hidden when expanded
624
+ if (rawOutput !== "") {
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));
629
+ sections.push({
630
+ railToken: toolRailToken,
631
+ content: outputLines,
632
+ });
633
+ }
634
+ }
635
+ else if (originalRenderResult !== undefined) {
636
+ const viewProbe = new ToolView({
637
+ theme,
638
+ header: { title: toolName },
639
+ stateHolder: context.state,
640
+ defaultLevel,
641
+ globalExpanded: renderOptions.expanded,
642
+ });
643
+ const effectiveExpanded = viewProbe.effectiveLevel === 2;
644
+ const effectiveOptions = {
645
+ ...renderOptions,
646
+ expanded: effectiveExpanded,
647
+ };
648
+ const lazyUpstreamComponent = {
649
+ render(w) {
650
+ return originalRenderResult(result, effectiveOptions, theme, context).render(w);
651
+ },
652
+ invalidate() { },
653
+ };
654
+ sections.push({
655
+ railToken: toolRailToken,
656
+ content: lazyUpstreamComponent,
657
+ maxLines: effectiveExpanded ? Number.POSITIVE_INFINITY : 9,
658
+ });
659
+ }
660
+ else if (rawOutput !== "") {
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
+ }
687
+ sections.push({
688
+ railToken: toolRailToken,
689
+ content: outputLines,
690
+ });
691
+ }
692
+ let copy;
693
+ if (toolName === "codemode") {
694
+ copy = undefined;
695
+ }
696
+ else if (heavyCmd !== undefined && heavyCmd !== "") {
697
+ const cleanOutput = stripTerminalSequences(rawOutput);
698
+ copy = {
699
+ primary: heavyCmd,
700
+ full: rawOutput !== "" ? `${heavyCmd}\n---\n${cleanOutput}` : heavyCmd,
701
+ };
702
+ }
703
+ else if (rawOutput !== "") {
704
+ const cleanOutput = stripTerminalSequences(rawOutput);
705
+ const argsFormatted = formatArgsPreview(context.args);
706
+ copy = {
707
+ primary: cleanOutput,
708
+ full: argsFormatted !== "" ? `${argsFormatted}\n\n${cleanOutput}` : cleanOutput,
709
+ };
710
+ }
711
+ return new ToolView({
712
+ theme,
713
+ header: {
714
+ title: toolName,
715
+ status,
716
+ summary,
717
+ },
718
+ railToken: toolRailToken,
719
+ collapsedRows,
720
+ sections,
721
+ copy,
722
+ outputPad: context.outputPad,
723
+ stateHolder: context.state,
724
+ defaultLevel,
725
+ globalExpanded: renderOptions.expanded,
726
+ invalidate: () => context.invalidate(),
727
+ copyToClipboard: options?.copyToClipboard,
728
+ });
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
+ }
805
+ export function createCollapsibleToolRendererResolver(options) {
806
+ return createRendererResolver(options);
807
+ }
808
+ function createRendererResolver(options, resolvePresentation) {
809
+ const defaultLevel = options?.defaultCollapsed === false ? 1 : 0;
810
+ return (toolName, next) => {
811
+ const upstream = next();
812
+ const provider = getToolPresentationProvider(upstream) ?? resolvePresentation?.(toolName);
813
+ if (provider === undefined && !shouldInterceptTool(toolName, options)) {
814
+ return upstream;
815
+ }
816
+ const originalRenderCall = upstream?.renderCall;
817
+ const originalRenderResult = upstream?.renderResult;
818
+ return {
819
+ renderShell: "self",
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
+ }
833
+ if (toolName === APPLY_PATCH_TOOL_NAME) {
834
+ const view = renderApplyPatchCallView({
835
+ theme,
836
+ context: { ...context, args },
837
+ defaultLevel,
838
+ options,
839
+ });
840
+ rememberCallView(context, view);
841
+ return view;
842
+ }
843
+ if (toolName === EDIT_TOOL_NAME) {
844
+ const path = editPath(args);
845
+ const view = new ToolView({
846
+ theme,
847
+ header: {
848
+ title: EDIT_TOOL_NAME,
849
+ status: context.isError ? "error" : "running",
850
+ summary: path,
851
+ },
852
+ outputPad: context.outputPad,
853
+ stateHolder: context.state,
854
+ defaultLevel,
855
+ globalExpanded: context.expanded,
856
+ invalidate: () => context.invalidate(),
857
+ copyToClipboard: options?.copyToClipboard,
858
+ });
859
+ rememberCallView(context, view);
860
+ return view;
861
+ }
862
+ const isStreamingArgs = context.isPartial && !context.argsComplete;
863
+ const status = context.isError
864
+ ? "error"
865
+ : toolName === "codemode"
866
+ ? "none"
867
+ : isStreamingArgs
868
+ ? "streaming"
869
+ : "running";
870
+ let summary = "";
871
+ if (isStreamingArgs) {
872
+ const tokens = estimateStreamingTokens(args);
873
+ summary = tokens > 0 ? `· ${String(tokens)} tokens` : "";
874
+ }
875
+ else {
876
+ summary = extractSummaryArg(toolName, args);
877
+ if (summary === "" && originalRenderCall !== undefined && toolName !== "codemode") {
878
+ const upstreamLines = originalRenderCall(args, theme, context).render(120);
879
+ const firstLine = stripTerminalSequences(upstreamLines[0] ?? "").trim();
880
+ summary = firstLine.startsWith(toolName)
881
+ ? firstLine.slice(toolName.length).trim()
882
+ : firstLine;
883
+ }
884
+ }
885
+ const heavyCmd = extractHeavyInputCommand(toolName, args);
886
+ const sections = [];
887
+ const inputSection = createInputCodeSection(toolName, args, theme);
888
+ if (inputSection !== undefined) {
889
+ sections.push(inputSection);
890
+ }
891
+ else {
892
+ const preview = formatArgsPreview(args);
893
+ if (preview !== "" && preview !== "{}") {
894
+ sections.push({
895
+ railToken: "muted",
896
+ content: preview,
897
+ });
898
+ }
899
+ }
900
+ const copy = heavyCmd !== undefined && heavyCmd !== ""
901
+ ? { primary: heavyCmd }
902
+ : formatArgsPreview(args) !== ""
903
+ ? { primary: formatArgsPreview(args) }
904
+ : undefined;
905
+ const view = new ToolView({
906
+ theme,
907
+ header: {
908
+ title: toolName,
909
+ status,
910
+ summary,
911
+ },
912
+ sections,
913
+ copy,
914
+ outputPad: context.outputPad,
915
+ stateHolder: context.state,
916
+ defaultLevel,
917
+ globalExpanded: context.expanded,
918
+ invalidate: () => context.invalidate(),
919
+ copyToClipboard: options?.copyToClipboard,
920
+ });
921
+ rememberCallView(context, view);
922
+ return view;
923
+ },
924
+ renderResult(result, renderOptions, theme, context) {
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
+ }
941
+ const specialized = renderSpecializedResult({
942
+ toolName,
943
+ theme,
944
+ context,
945
+ result,
946
+ renderOptions,
947
+ defaultLevel,
948
+ options,
949
+ });
950
+ if (specialized !== undefined)
951
+ return specialized;
952
+ return renderGenericResult({
953
+ toolName,
954
+ theme,
955
+ context,
956
+ result,
957
+ renderOptions,
958
+ defaultLevel,
959
+ originalRenderCall,
960
+ originalRenderResult,
961
+ resolvePresentation,
962
+ options,
963
+ });
964
+ },
965
+ };
966
+ };
967
+ }
968
+ export function registerCollapsibleToolRenderer(pi, 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)));
991
+ }