@hheei/pi-ext-ui 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/renderer.js CHANGED
@@ -1,35 +1,26 @@
1
1
  import { getLanguageFromPath, highlightCode, } from "@earendil-works/pi-coding-agent";
2
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";
3
+ import { agentResultText, getRegisteredToolPresentationProvider, getToolPresentationProvider, hasOptionalToolRenderers, } from "@hheei/pi-ext-core";
4
+ import { DiffView } from "./diff-view.js";
8
5
  import { NestedCallsView } from "./nested-calls-view.js";
9
6
  import { formatToolHeaderText, ToolView, } from "./tool-view.js";
10
7
  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
- ];
8
+ /**
9
+ * Upstream tools whose call and result views pi-ext-ui still frames itself. Concrete tool packages
10
+ * own their own presentation through ext-core's optional renderer point, so their names never
11
+ * appear here.
12
+ */
13
+ export const SPECIALIZED_TOOL_RENDERERS = ["codemode"];
19
14
  /** Web-access tools use the shared folding shell while retaining their native body renderer. */
20
15
  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";
16
+ import { callStatsParts, estimateStreamingTokens, extractHeavyInputCommand, extractNestedCalls, extractResultText, extractSummaryArg, formatCostUsd, formatDurationMs, inlinePresentation, isRecord, resolveContainerStatus, toolPath, } from "./payload-parser.js";
22
17
  export { estimateStreamingTokens, inlinePresentation, } from "./payload-parser.js";
23
- const APPLY_PATCH_PREVIEW_STATE_KEY = "__piExtUiApplyPatchPreview";
24
- const PYTHON_EVAL_TOOL_TITLE = "python_eval";
25
18
  const SPECIALIZED_TOOL_SET = new Set(SPECIALIZED_TOOL_RENDERERS);
26
19
  const WEB_ACCESS_TOOL_SET = new Set(WEB_ACCESS_TOOLS);
27
20
  const CALL_VIEW_STATE_KEY = "__piExtUiCallView";
28
21
  function detectCodeLanguage(toolName) {
29
22
  if (toolName === "bash")
30
23
  return "bash";
31
- if (toolName === "python_eval")
32
- return "python";
33
24
  if (toolName === "codemode")
34
25
  return "javascript";
35
26
  return undefined;
@@ -82,15 +73,12 @@ function rememberCallView(context, view) {
82
73
  }
83
74
  }
84
75
  /**
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.
76
+ * A result view takes over from the call view. A container tool yields on any result, because its
77
+ * own row would duplicate the nested rows and output it now carries; every other tool keeps its
78
+ * call row until the final result. A provider states that for its own tool, so no name is involved.
88
79
  */
89
- function hideCallView(context, toolName, isPartial) {
90
- if (toolName !== "codemode" &&
91
- toolName !== PYTHON_EVAL_TOOL_NAME &&
92
- toolName !== APPLY_PATCH_TOOL_NAME &&
93
- isPartial)
80
+ function hideCallView(context, replaceCallOnPartial, isPartial) {
81
+ if (!replaceCallOnPartial && isPartial)
94
82
  return;
95
83
  const view = isRecord(context.state) ? context.state[CALL_VIEW_STATE_KEY] : undefined;
96
84
  if (view instanceof ToolView)
@@ -116,358 +104,6 @@ function formatNestedCallRow(call, theme) {
116
104
  .join(" · "),
117
105
  }, theme);
118
106
  }
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
107
  function shouldInterceptTool(toolName, options) {
472
108
  if (options?.catchAll === true) {
473
109
  return options.match !== undefined ? options.match(toolName) : true;
@@ -481,18 +117,6 @@ function shouldInterceptTool(toolName, options) {
481
117
  }
482
118
  return SPECIALIZED_TOOL_SET.has(toolName) || WEB_ACCESS_TOOL_SET.has(toolName);
483
119
  }
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
120
  function renderGenericResult(params) {
497
121
  const { toolName, theme, context, result, renderOptions, defaultLevel, originalRenderCall, originalRenderResult, resolvePresentation, options, } = params;
498
122
  const heavyCmd = extractHeavyInputCommand(toolName, context.args);
@@ -566,7 +190,7 @@ function renderGenericResult(params) {
566
190
  if (provider !== undefined) {
567
191
  return renderOwnedPresentation({
568
192
  toolName: childName,
569
- provider,
193
+ presentation: provider(childCtx.args, childResult, childTheme, childCtx),
570
194
  result: childResult,
571
195
  theme: childTheme,
572
196
  context: childCtx,
@@ -574,17 +198,6 @@ function renderGenericResult(params) {
574
198
  options,
575
199
  });
576
200
  }
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
201
  return renderGenericResult({
589
202
  toolName: childName,
590
203
  theme: childTheme,
@@ -727,30 +340,40 @@ function renderGenericResult(params) {
727
340
  copyToClipboard: options?.copyToClipboard,
728
341
  });
729
342
  }
343
+ /**
344
+ * The generic frame around one owner's presentation. The caller already resolved it, so a frame
345
+ * pass never asks the tool to rebuild its rows.
346
+ */
730
347
  function renderOwnedPresentation(params) {
731
- const { toolName, provider, result, theme, context, defaultLevel, options } = params;
732
- const presentation = provider(context.args, result, theme);
348
+ const { toolName, presentation, result, theme, context, defaultLevel, options } = params;
733
349
  const isError = context.isError || result?.isError === true;
734
350
  const pending = result === undefined || context.isPartial;
735
351
  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";
352
+ // The tool owns its status: a container says "none" for a child's failure and a partial patch
353
+ // says "warn" even though the host marks the result failed. Only a provider without an opinion
354
+ // falls back to the frame's own reading of the execution.
355
+ const status = presentation.status ??
356
+ (isError
357
+ ? "error"
358
+ : pending
359
+ ? context.argsComplete
360
+ ? "running"
361
+ : "streaming"
362
+ : isDiff
363
+ ? "none"
364
+ : "ok");
745
365
  const railToken = isError ? "error" : pending ? "accent" : "success";
746
366
  const output = isError && result !== undefined
747
367
  ? agentResultText(result)
748
368
  : (presentation.output ?? (result === undefined ? "" : agentResultText(result)));
749
- const sections = [];
750
- if (presentation.request) {
369
+ const sections = [...(presentation.sections ?? [])];
370
+ if (presentation.sections === undefined && presentation.request) {
751
371
  sections.push({ railToken: "muted", content: stripTerminalSequences(presentation.request) });
752
372
  }
753
- if (isDiff) {
373
+ if (presentation.sections !== undefined) {
374
+ // Tool owns domain rows; this factory owns the frame and interaction only.
375
+ }
376
+ else if (isDiff) {
754
377
  if (presentation.diff.trim() !== "") {
755
378
  sections.push({
756
379
  railToken: "dim",
@@ -786,12 +409,14 @@ function renderOwnedPresentation(params) {
786
409
  theme,
787
410
  header: { title: toolName, status, summary },
788
411
  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),
412
+ // The tool owns the folded rows and their tones; the frame only sanitizes them for the host.
413
+ collapsedRows: presentation.collapsedRows ??
414
+ (isError && output !== ""
415
+ ? output
416
+ .split("\n")
417
+ .slice(0, 1)
418
+ .map((line) => theme.fg("error", stripTerminalSequences(line)))
419
+ : undefined),
795
420
  sections,
796
421
  copy,
797
422
  outputPad: context.outputPad,
@@ -809,6 +434,8 @@ function createRendererResolver(options, resolvePresentation) {
809
434
  const defaultLevel = options?.defaultCollapsed === false ? 1 : 0;
810
435
  return (toolName, next) => {
811
436
  const upstream = next();
437
+ if (hasOptionalToolRenderers(upstream))
438
+ return upstream;
812
439
  const provider = getToolPresentationProvider(upstream) ?? resolvePresentation?.(toolName);
813
440
  if (provider === undefined && !shouldInterceptTool(toolName, options)) {
814
441
  return upstream;
@@ -819,46 +446,18 @@ function createRendererResolver(options, resolvePresentation) {
819
446
  renderShell: "self",
820
447
  renderCall(args, theme, context) {
821
448
  if (provider !== undefined) {
449
+ const callContext = { ...context, args };
822
450
  const view = renderOwnedPresentation({
823
451
  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({
452
+ presentation: provider(args, undefined, theme, callContext),
835
453
  theme,
836
- context: { ...context, args },
454
+ context: callContext,
837
455
  defaultLevel,
838
456
  options,
839
457
  });
840
458
  rememberCallView(context, view);
841
459
  return view;
842
460
  }
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
461
  const isStreamingArgs = context.isPartial && !context.argsComplete;
863
462
  const status = context.isError
864
463
  ? "error"
@@ -922,33 +521,30 @@ function createRendererResolver(options, resolvePresentation) {
922
521
  return view;
923
522
  },
924
523
  renderResult(result, renderOptions, theme, context) {
925
- hideCallView(context, toolName, renderOptions.isPartial);
926
- if (provider !== undefined) {
524
+ const resultContext = {
525
+ ...context,
526
+ expanded: renderOptions.expanded,
527
+ isPartial: renderOptions.isPartial,
528
+ };
529
+ const presentation = provider === undefined
530
+ ? undefined
531
+ : provider(resultContext.args, result, theme, resultContext);
532
+ // Without an owner, a container frame is the only name-based decision left, and it
533
+ // belongs to upstream codemode.
534
+ const replacesCallOnPartial = presentation?.replaceCallOnPartial === true ||
535
+ (presentation === undefined && SPECIALIZED_TOOL_SET.has(toolName));
536
+ hideCallView(context, replacesCallOnPartial, renderOptions.isPartial);
537
+ if (presentation !== undefined) {
927
538
  return renderOwnedPresentation({
928
539
  toolName,
929
- provider,
540
+ presentation,
930
541
  result,
931
542
  theme,
932
- context: {
933
- ...context,
934
- expanded: renderOptions.expanded,
935
- isPartial: renderOptions.isPartial,
936
- },
543
+ context: resultContext,
937
544
  defaultLevel,
938
545
  options,
939
546
  });
940
547
  }
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
548
  return renderGenericResult({
953
549
  toolName,
954
550
  theme,
@@ -965,6 +561,10 @@ function createRendererResolver(options, resolvePresentation) {
965
561
  };
966
562
  };
967
563
  }
564
+ /** UI factory consumed by package-owned direct renderers, independently of host resolver precedence. */
565
+ export function createOptionalToolRendererFactory(pi, options) {
566
+ return (name, native) => createRendererResolver({ ...options, tools: [name] }, (toolName) => getRegisteredToolPresentationProvider(pi, toolName))(name, () => native) ?? native;
567
+ }
968
568
  export function registerCollapsibleToolRenderer(pi, options) {
969
569
  if (typeof pi.on === "function") {
970
570
  pi.on("tool_execution_start", (event) => {