@hheei/pi-ext-ui 0.6.0 → 0.7.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,43 +1,34 @@
1
- import { getLanguageFromPath, highlightCode, } from "@earendil-works/pi-coding-agent";
1
+ import { highlightCode, } from "@earendil-works/pi-coding-agent";
2
2
  import { stripTerminalSequences } from "@earendil-works/pi-tui";
3
3
  import { agentResultText, getRegisteredToolPresentationProvider, getRegisteredToolRenderers, getToolPresentationProvider, hasOptionalToolRenderers, } from "@hheei/pi-ext-core";
4
4
  import { DiffView } from "./diff-view.js";
5
5
  import { NestedCallsView } from "./nested-calls-view.js";
6
6
  import { formatToolHeaderText, ToolView, } from "./tool-view.js";
7
- import { getCapturedNestedCalls, recordToolExecutionEnd, recordToolExecutionStart, } from "./tracker.js";
8
7
  /**
9
8
  * Upstream tools whose call and result views pi-ext-ui still frames itself. Concrete tool packages
10
9
  * own their own presentation through ext-core's optional renderer point, so their names never
11
10
  * appear here.
12
11
  */
13
12
  export const SPECIALIZED_TOOL_RENDERERS = ["codemode"];
14
- import { callStatsParts, estimateStreamingTokens, extractCodemodeHeaderFacts, extractHeavyInputCommand, extractNestedCalls, extractResultText, extractSummaryArg, formatCostUsd, formatDurationMs, inlinePresentation, isRecord, resolveContainerStatus, } from "./payload-parser.js";
15
- export { estimateStreamingTokens, inlinePresentation, } from "./payload-parser.js";
13
+ import { inlinePresentation, nativeHeaderSummary, nativeHeavyRequest, nativeOutputLanguage, nativeRequestBlock, nativeResultText, nativeTimeoutFact, } from "./native-tools.js";
14
+ import { callStatsParts, estimateStreamingTokens, extractNestedCalls, formatCostUsd, formatDurationMs, isRecord, RESOLVED_CALLS_KEY, resolveContainerStatus, } from "./payload-parser.js";
15
+ export { extractChildToolCopyText, inlinePresentation } from "./native-tools.js";
16
+ export { estimateStreamingTokens, } from "./payload-parser.js";
16
17
  const SPECIALIZED_TOOL_SET = new Set(SPECIALIZED_TOOL_RENDERERS);
17
18
  const CALL_VIEW_STATE_KEY = "__piExtUiCallView";
18
- function detectCodeLanguage(toolName) {
19
- if (toolName === "bash")
20
- return "bash";
21
- if (toolName === "codemode")
22
- return "javascript";
23
- if (toolName === "python_eval")
24
- return "python";
25
- return undefined;
26
- }
27
19
  function createInputCodeSection(toolName, args, theme, railToken = "muted") {
28
- const raw = extractHeavyInputCommand(toolName, args);
29
- if (raw === undefined || raw === "")
20
+ const block = nativeRequestBlock(toolName, args);
21
+ if (block === undefined)
30
22
  return undefined;
31
- const isShell = toolName === "bash";
32
- const clean = raw.replace(/\r/g, "").replace(/\t/g, " ");
33
- const lang = detectCodeLanguage(toolName);
23
+ // Display only: the request is model-controlled input, while its copy stays byte-exact.
24
+ const clean = stripTerminalSequences(block.text.trim()).replace(/\r/g, "").replace(/\t/g, " ");
34
25
  let lines;
35
- if (isShell) {
26
+ if (block.shell) {
36
27
  lines = clean.split("\n").map((line, i) => (i === 0 ? `$ ${line}` : ` ${line}`));
37
28
  }
38
29
  else {
39
30
  try {
40
- lines = highlightCode(clean, lang);
31
+ lines = highlightCode(clean, block.language);
41
32
  }
42
33
  catch {
43
34
  lines = clean.split("\n").map((line) => theme.fg("mdCode", line));
@@ -45,44 +36,97 @@ function createInputCodeSection(toolName, args, theme, railToken = "muted") {
45
36
  }
46
37
  return { railToken, content: lines };
47
38
  }
48
- function resolveToolCallSummary(params) {
49
- const { toolName, args, originalRenderCall, theme, context } = params;
50
- if (toolName === "codemode") {
51
- return extractCodemodeHeaderFacts(args);
52
- }
53
- // Tier 1: Host Native RenderCall Delegation
54
- if (originalRenderCall !== undefined) {
55
- try {
56
- const upstreamContext = { ...context, lastComponent: undefined };
57
- const upstreamLines = originalRenderCall(args, theme, upstreamContext).render(120);
58
- const firstNonEmpty = upstreamLines
59
- .map((line) => stripTerminalSequences(line).trim())
60
- .find((line) => line !== "");
61
- if (firstNonEmpty !== undefined) {
62
- if (firstNonEmpty.startsWith("$ ")) {
63
- return firstNonEmpty.slice(2).trim();
64
- }
65
- if (firstNonEmpty === toolName) {
66
- return "";
67
- }
68
- if (firstNonEmpty.startsWith(toolName) &&
69
- /^[\s(:=/[]/.test(firstNonEmpty.slice(toolName.length))) {
70
- return firstNonEmpty.slice(toolName.length).trim();
71
- }
72
- return firstNonEmpty;
73
- }
74
- }
75
- catch {
76
- // Upstream threw, fall through to fallback
77
- }
39
+ /**
40
+ * The one-row argument line a native generic tool row shows. It states the arguments as given, so a
41
+ * tool with no vocabulary of its own keeps the information its native row carried without this
42
+ * frame rendering and parsing that row.
43
+ */
44
+ function genericArgsLine(args) {
45
+ if (typeof args === "string")
46
+ return stripTerminalSequences(args.trim());
47
+ if (!isRecord(args))
48
+ return "";
49
+ const pairs = Object.entries(args)
50
+ .map(([key, value]) => `${key}=${JSON.stringify(value) ?? String(value)}`)
51
+ .join(" ");
52
+ return pairs.length > 100 ? `${pairs.slice(0, 97)}...` : pairs;
53
+ }
54
+ /**
55
+ * The header facts of one tool row. The tool's own layer states its summary and whether it absorbs the
56
+ * secondary facts; a tool with no vocabulary of its own states its arguments as given.
57
+ */
58
+ function toolHeaderFacts(params) {
59
+ const { toolName, args, theme, cwd, facts, nativeArgsLine } = params;
60
+ const secondary = facts.filter(Boolean);
61
+ const meta = secondary.join(" · ");
62
+ const withMeta = (summary, styled) => ({
63
+ summary,
64
+ ...(styled ? { summaryStyled: true } : {}),
65
+ ...(meta === "" ? {} : { meta }),
66
+ });
67
+ const native = nativeHeaderSummary(toolName, args, theme, cwd);
68
+ if (native !== undefined) {
69
+ return native.factsInSummary
70
+ ? { summary: [native.summary, ...secondary].filter(Boolean).join(" · ") }
71
+ : withMeta(native.summary, native.styled);
78
72
  }
79
- // Tier 2: Contractual Fallback for built-in tools
80
- return extractSummaryArg(toolName, args);
73
+ const summary = nativeArgsLine === true ? genericArgsLine(args) : "";
74
+ return { summary: [summary, ...secondary].filter(Boolean).join(" · ") };
81
75
  }
82
76
  function isCapturedCall(call) {
83
77
  return "args" in call || "result" in call;
84
78
  }
85
- /** Nested facts for a consumer that lists children outside the container's own body. */
79
+ /**
80
+ * The request a persisted child row can still show. A live capture already carries it; otherwise the
81
+ * host's saved nested-call record for the same id does, as long as it kept the arguments at all.
82
+ */
83
+ function withSessionArgs(calls, runtime) {
84
+ return calls.map((call) => {
85
+ if (isCapturedCall(call))
86
+ return call;
87
+ if (call.args !== undefined || call.argsTruncated === true)
88
+ return call;
89
+ const fact = runtime.nestedCalls.args(call.id);
90
+ if (fact === undefined)
91
+ return call;
92
+ return {
93
+ ...call,
94
+ ...(fact.args === undefined ? {} : { args: fact.args }),
95
+ ...(fact.truncated
96
+ ? {
97
+ argsTruncated: true,
98
+ ...(fact.bytes === undefined ? {} : { argsBytes: fact.bytes }),
99
+ }
100
+ : {}),
101
+ };
102
+ });
103
+ }
104
+ /** A call counts only when the host proved it resolved; an absent fact is not a negative one. */
105
+ function callResolved(call, live) {
106
+ if (call.resolved === true)
107
+ return true;
108
+ return call.id !== undefined && live?.has(call.id) === true;
109
+ }
110
+ /** The container's own child facts, in the shape the nested board draws and copies. */
111
+ function presentationChildCalls(children) {
112
+ return children.map((child) => {
113
+ const status = child.status === "running" || child.status === "streaming"
114
+ ? "running"
115
+ : child.status === "warn"
116
+ ? "cancelled"
117
+ : child.status === "error"
118
+ ? "error"
119
+ : "ok";
120
+ return {
121
+ toolName: child.title,
122
+ ...(child.id === undefined ? {} : { id: child.id }),
123
+ ...(child.summary === undefined ? {} : { summary: child.summary }),
124
+ status,
125
+ ...(child.durationMs === undefined ? {} : { durationMs: child.durationMs }),
126
+ };
127
+ });
128
+ }
129
+ /** Nested call facts for a consumer that counts or aggregates them outside the container's body. */
86
130
  function nestedChildren(calls) {
87
131
  return calls.map((call) => {
88
132
  if (isCapturedCall(call)) {
@@ -99,9 +143,6 @@ function nestedChildren(calls) {
99
143
  ? "running"
100
144
  : "ok",
101
145
  ...(call.durationMs === undefined ? {} : { durationMs: call.durationMs }),
102
- ...(call.result === undefined
103
- ? {}
104
- : { detail: extractResultText(call.toolName, call.result).outputText }),
105
146
  };
106
147
  }
107
148
  return {
@@ -116,7 +157,6 @@ function nestedChildren(calls) {
116
157
  ? "error"
117
158
  : "ok",
118
159
  ...(call.durationMs === undefined ? {} : { durationMs: call.durationMs }),
119
- ...(call.error === undefined ? {} : { detail: call.error }),
120
160
  };
121
161
  });
122
162
  }
@@ -156,7 +196,7 @@ function hideCallView(context) {
156
196
  if (view instanceof ToolView)
157
197
  view.hide();
158
198
  }
159
- function formatNestedCallRow(call, theme) {
199
+ function formatNestedCallRow(call, theme, cwd) {
160
200
  const inline = inlinePresentation(call.toolName, call.summary, undefined, call.isError === true);
161
201
  const status = call.status === "running"
162
202
  ? "running"
@@ -167,13 +207,20 @@ function formatNestedCallRow(call, theme) {
167
207
  ? ""
168
208
  : stripTerminalSequences(call.summary.trim());
169
209
  const truncatedArgs = cleanArgs.length > 72 ? `${cleanArgs.slice(0, 69)}…` : cleanArgs;
170
- const costText = formatCostUsd(call.cost);
210
+ const header = toolHeaderFacts({
211
+ toolName: call.toolName,
212
+ args: call.summary,
213
+ theme,
214
+ cwd,
215
+ facts: [formatDurationMs(call.durationMs), formatCostUsd(call.cost)],
216
+ });
217
+ // A record's arguments may be a summary the container already flattened; fall back to it rather
218
+ // than drop the only text the row has.
171
219
  return formatToolHeaderText({
172
220
  status,
173
221
  title: call.toolName,
174
- summary: [inline?.summary ?? truncatedArgs, formatDurationMs(call.durationMs), costText]
175
- .filter(Boolean)
176
- .join(" · "),
222
+ ...header,
223
+ summary: header.summary || (inline?.summary ?? truncatedArgs),
177
224
  }, theme);
178
225
  }
179
226
  export function isMcpTool(toolName) {
@@ -196,25 +243,34 @@ function shouldInterceptTool(toolName, options) {
196
243
  return true;
197
244
  }
198
245
  function renderGenericResult(params) {
199
- const { toolName, theme, context, result, renderOptions, defaultLevel, originalRenderCall, originalRenderResult, resolvePresentation, resolveNativeRenderer, options, } = params;
200
- const heavyCmd = extractHeavyInputCommand(toolName, context.args);
246
+ const { runtime, toolName, theme, context, result, renderOptions, defaultLevel, nativeArgsLine, originalRenderResult, resolvePresentation, resolveNativeRenderer, options, } = params;
247
+ const heavyCmd = nativeHeavyRequest(toolName, context.args);
201
248
  const nestedCalls = extractNestedCalls(result);
202
249
  const captured = toolName === "codemode"
203
- ? (getCapturedNestedCalls(context.toolCallId) ??
250
+ ? (runtime.tracker.getCaptured(context.toolCallId) ??
204
251
  (isRecord(result.details) &&
205
252
  Array.isArray(result.details.__piExtUiCapturedCalls)
206
253
  ? result.details
207
254
  .__piExtUiCapturedCalls
208
255
  : undefined))
209
256
  : undefined;
210
- const childCalls = captured ?? nestedCalls;
257
+ const childCalls = withSessionArgs(captured ?? nestedCalls, runtime);
258
+ const liveResolved = runtime.tracker.getResolved(context.toolCallId);
259
+ const resolvedCalls = childCalls.filter((call) => callResolved(call, liveResolved));
260
+ const unknownCallCount = childCalls.length - resolvedCalls.length;
211
261
  const isMetaTool = toolName === "codemode" || toolName === "subagent" || childCalls.length > 0;
212
- const { outputText: rawOutput, wallTimeSeconds } = extractResultText(toolName, result);
262
+ const { outputText: rawOutput, wallTimeSeconds } = nativeResultText(toolName, result);
213
263
  const isError = (context.isError ?? false) || (result.isError ?? false);
214
264
  const status = toolName === "codemode"
215
- ? childCalls.length === 0 && isError
216
- ? "error"
217
- : "none"
265
+ ? childCalls.length > 0
266
+ ? "none"
267
+ : isError
268
+ ? "error"
269
+ : renderOptions.isPartial === true
270
+ ? context.argsComplete
271
+ ? "running"
272
+ : "streaming"
273
+ : "none"
218
274
  : isMetaTool
219
275
  ? resolveContainerStatus({
220
276
  isError,
@@ -227,42 +283,47 @@ function renderGenericResult(params) {
227
283
  : renderOptions.isPartial
228
284
  ? "running"
229
285
  : "ok";
230
- const summaryParts = [];
231
- const argSummary = resolveToolCallSummary({
232
- toolName,
233
- args: context.args,
234
- originalRenderCall,
235
- theme,
236
- context,
237
- });
238
- if (argSummary !== "")
239
- summaryParts.push(argSummary);
286
+ const facts = [];
240
287
  if (isMetaTool && childCalls.length > 0) {
241
- const failCount = childCalls.filter((c) => Boolean(c.isError || ("status" in c && c.status === "error"))).length;
242
- summaryParts.push(...callStatsParts(childCalls.length, failCount));
288
+ // Only a proven resolution is a call; the rest stay visible as unknown instead of counting.
289
+ const failCount = resolvedCalls.filter((c) => Boolean(c.isError || ("status" in c && c.status === "error"))).length;
290
+ facts.push(...callStatsParts(resolvedCalls.length, failCount));
291
+ if (unknownCallCount > 0)
292
+ facts.push(`${String(unknownCallCount)} unknown`);
243
293
  }
244
294
  if (context.durationMs !== undefined) {
245
- summaryParts.push(formatDurationMs(context.durationMs));
295
+ facts.push(formatDurationMs(context.durationMs));
246
296
  }
247
297
  else if (wallTimeSeconds !== undefined && Number.isFinite(wallTimeSeconds)) {
248
- summaryParts.push(formatDurationMs(wallTimeSeconds * 1000));
298
+ facts.push(formatDurationMs(wallTimeSeconds * 1000));
249
299
  }
250
300
  const totalCost = childCalls.reduce((sum, c) => sum + (c.cost ?? 0), 0);
251
- if (totalCost > 0) {
252
- summaryParts.push(formatCostUsd(totalCost));
253
- }
254
- const summary = summaryParts.join(" · ");
301
+ if (totalCost > 0)
302
+ facts.push(formatCostUsd(totalCost));
303
+ const timeout = nativeTimeoutFact(toolName, context.args);
304
+ if (timeout !== undefined)
305
+ facts.unshift(timeout);
306
+ const header = toolHeaderFacts({
307
+ toolName,
308
+ args: context.args,
309
+ theme,
310
+ cwd: context.cwd,
311
+ facts,
312
+ nativeArgsLine,
313
+ });
255
314
  const codemodeNestedView = toolName === "codemode" && childCalls.length > 0
256
315
  ? new NestedCallsView({
257
316
  calls: childCalls,
258
317
  theme,
259
318
  context,
319
+ headerSummary: (childName, childArgs, childTheme) => nativeHeaderSummary(childName, childArgs, childTheme, context.cwd)?.summary,
260
320
  resolveChildRenderer: (childName) => {
261
321
  return {
262
322
  renderResult: (childResult, childOptions, childTheme, childCtx) => {
263
323
  const provider = resolvePresentation?.(childName);
264
324
  if (provider !== undefined) {
265
325
  return renderOwnedPresentation({
326
+ runtime,
266
327
  toolName: childName,
267
328
  presentation: provider(childCtx.args, childResult, childTheme, childCtx),
268
329
  result: childResult,
@@ -276,13 +337,14 @@ function renderGenericResult(params) {
276
337
  }
277
338
  const registered = resolveNativeRenderer?.(childName);
278
339
  return renderGenericResult({
340
+ runtime,
279
341
  toolName: childName,
280
342
  theme: childTheme,
281
343
  context: childCtx,
282
344
  result: childResult,
283
345
  renderOptions: childOptions,
284
346
  defaultLevel: childOptions.expanded ? 2 : 1,
285
- originalRenderCall: registered?.renderCall,
347
+ nativeArgsLine: registered?.renderCall !== undefined,
286
348
  originalRenderResult: registered?.renderResult,
287
349
  resolvePresentation,
288
350
  resolveNativeRenderer,
@@ -301,7 +363,7 @@ function renderGenericResult(params) {
301
363
  const collapsedRows = codemodeNestedView !== undefined
302
364
  ? codemodeNestedView
303
365
  : isMetaTool && nestedCalls.length > 0
304
- ? nestedCalls.map((call) => formatNestedCallRow(call, theme))
366
+ ? nestedCalls.map((call) => formatNestedCallRow(call, theme, context.cwd))
305
367
  : undefined;
306
368
  const toolRailToken = isError
307
369
  ? "error"
@@ -335,6 +397,7 @@ function renderGenericResult(params) {
335
397
  }
336
398
  else if (originalRenderResult !== undefined) {
337
399
  const viewProbe = new ToolView({
400
+ background: () => runtime.backgrounds,
338
401
  theme,
339
402
  header: { title: toolName },
340
403
  stateHolder: context.state,
@@ -349,18 +412,13 @@ function renderGenericResult(params) {
349
412
  let lastUpstreamComponent;
350
413
  const lazyUpstreamComponent = {
351
414
  render(w) {
352
- try {
353
- const upstreamContext = {
354
- ...context,
355
- lastComponent: lastUpstreamComponent,
356
- };
357
- const comp = originalRenderResult(result, effectiveOptions, theme, upstreamContext);
358
- lastUpstreamComponent = comp;
359
- return comp.render(w);
360
- }
361
- catch {
362
- return rawOutput !== "" ? rawOutput.split("\n") : [];
363
- }
415
+ const upstreamContext = {
416
+ ...context,
417
+ lastComponent: lastUpstreamComponent,
418
+ };
419
+ const comp = originalRenderResult(result, effectiveOptions, theme, upstreamContext);
420
+ lastUpstreamComponent = comp;
421
+ return comp.render(w);
364
422
  },
365
423
  invalidate() { },
366
424
  };
@@ -384,7 +442,7 @@ function renderGenericResult(params) {
384
442
  if (toolName === "codemode") {
385
443
  copy = undefined;
386
444
  }
387
- else if (heavyCmd !== undefined && heavyCmd !== "") {
445
+ else if (heavyCmd !== undefined) {
388
446
  const cleanOutput = stripTerminalSequences(rawOutput);
389
447
  copy = {
390
448
  primary: heavyCmd,
@@ -400,15 +458,15 @@ function renderGenericResult(params) {
400
458
  };
401
459
  }
402
460
  return new ToolView({
461
+ background: () => runtime.backgrounds,
403
462
  theme,
404
- header: {
405
- title: toolName,
406
- status,
407
- summary,
408
- },
463
+ header: { title: toolName, status, ...header },
409
464
  railToken: toolRailToken,
410
465
  collapsedRows,
411
466
  ...(children.length === 0 ? {} : { children }),
467
+ // A container with child facts states its effective count, including zero: a container whose
468
+ // children all failed to resolve is not one call.
469
+ ...(childCalls.length === 0 ? {} : { childCount: resolvedCalls.length }),
412
470
  sections,
413
471
  copy,
414
472
  outputPad: context.outputPad,
@@ -424,7 +482,7 @@ function renderGenericResult(params) {
424
482
  * pass never asks the tool to rebuild its rows.
425
483
  */
426
484
  function renderOwnedPresentation(params) {
427
- const { toolName, presentation, result, theme, context, defaultLevel, resolvePresentation, resolveNativeRenderer, options, } = params;
485
+ const { runtime, toolName, presentation, result, theme, context, defaultLevel, resolvePresentation, resolveNativeRenderer, options, } = params;
428
486
  const isError = context.isError || result?.isError === true;
429
487
  const pending = result === undefined || context.isPartial;
430
488
  const isDiff = !isError && !pending && presentation.kind === "diff";
@@ -465,26 +523,16 @@ function renderOwnedPresentation(params) {
465
523
  }
466
524
  else if (output !== "") {
467
525
  const rawLines = stripTerminalSequences(output).split("\n");
526
+ const lang = nativeOutputLanguage(toolName, context.args);
468
527
  let styledLines;
469
528
  if (isError) {
470
529
  styledLines = rawLines.map((line) => theme.fg("error", line));
471
530
  }
472
- else if (toolName === "read") {
473
- const rawPath = isRecord(context.args) && typeof context.args.path === "string"
474
- ? context.args.path
475
- : isRecord(context.args) && typeof context.args.file_path === "string"
476
- ? context.args.file_path
477
- : "";
478
- const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
479
- if (lang !== undefined) {
480
- try {
481
- styledLines = highlightCode(output.replace(/\t/g, " "), lang);
482
- }
483
- catch {
484
- styledLines = rawLines.map((line) => theme.fg("toolOutput", line));
485
- }
531
+ else if (lang !== undefined) {
532
+ try {
533
+ styledLines = highlightCode(output.replace(/\t/g, " "), lang);
486
534
  }
487
- else {
535
+ catch {
488
536
  styledLines = rawLines.map((line) => theme.fg("toolOutput", line));
489
537
  }
490
538
  }
@@ -497,22 +545,18 @@ function renderOwnedPresentation(params) {
497
545
  });
498
546
  }
499
547
  const duration = result === undefined ? "" : formatDurationMs(context.durationMs);
500
- const summary = [stripTerminalSequences(presentation.summary), duration]
501
- .filter(Boolean)
502
- .join(" · ");
503
- const copy = presentation.copy === undefined
504
- ? isDiff && presentation.diff.trim() !== ""
505
- ? { primary: presentation.diff }
506
- : undefined
507
- : {
508
- primary: stripTerminalSequences(presentation.copy.primary),
509
- ...(presentation.copy.full === undefined
510
- ? {}
511
- : { full: stripTerminalSequences(presentation.copy.full) }),
512
- };
548
+ // The provider owns the request/output boundary; cleaning its payload here corrupts raw input.
549
+ const copy = presentation.copy ??
550
+ (isDiff && presentation.diff.trim() !== "" ? { primary: presentation.diff } : undefined);
513
551
  return new ToolView({
552
+ background: () => runtime.backgrounds,
514
553
  theme,
515
- header: { title: toolName, status, summary },
554
+ header: {
555
+ title: toolName,
556
+ status,
557
+ summary: stripTerminalSequences(presentation.summary),
558
+ ...(duration === "" ? {} : { meta: duration }),
559
+ },
516
560
  railToken,
517
561
  // The tool owns the folded rows and their tones; the frame only sanitizes them for the host.
518
562
  collapsedRows: (() => {
@@ -520,23 +564,29 @@ function renderOwnedPresentation(params) {
520
564
  return presentation.collapsedRows;
521
565
  }
522
566
  const captured = result !== undefined
523
- ? (getCapturedNestedCalls(context.toolCallId) ??
567
+ ? (runtime.tracker.getCaptured(context.toolCallId) ??
524
568
  (isRecord(result.details) && Array.isArray(result.details.__piExtUiCapturedCalls)
525
569
  ? result.details.__piExtUiCapturedCalls
526
570
  : undefined))
527
571
  : undefined;
528
572
  const nestedCalls = extractNestedCalls(result);
529
- const childCalls = captured ?? nestedCalls;
573
+ // The owner's own child facts win over parsing another package's details.
574
+ const ownCalls = presentation.children === undefined
575
+ ? nestedCalls
576
+ : presentationChildCalls(presentation.children);
577
+ const childCalls = withSessionArgs(captured ?? ownCalls, runtime);
530
578
  if (childCalls.length > 0) {
531
579
  return new NestedCallsView({
532
580
  calls: childCalls,
533
581
  theme,
534
582
  context,
583
+ headerSummary: (childName, childArgs, childTheme) => nativeHeaderSummary(childName, childArgs, childTheme, context.cwd)?.summary,
535
584
  resolveChildRenderer: (childName) => ({
536
585
  renderResult: (childResult, childOptions, childTheme, childCtx) => {
537
586
  const childProvider = resolvePresentation?.(childName);
538
587
  if (childProvider !== undefined) {
539
588
  return renderOwnedPresentation({
589
+ runtime,
540
590
  toolName: childName,
541
591
  presentation: childProvider(childCtx.args, childResult, childTheme, childCtx),
542
592
  result: childResult,
@@ -550,13 +600,14 @@ function renderOwnedPresentation(params) {
550
600
  }
551
601
  const registered = resolveNativeRenderer?.(childName);
552
602
  return renderGenericResult({
603
+ runtime,
553
604
  toolName: childName,
554
605
  theme: childTheme,
555
606
  context: childCtx,
556
607
  result: childResult,
557
608
  renderOptions: childOptions,
558
609
  defaultLevel: childOptions.expanded ? 2 : 1,
559
- originalRenderCall: registered?.renderCall,
610
+ nativeArgsLine: registered?.renderCall !== undefined,
560
611
  originalRenderResult: registered?.renderResult,
561
612
  resolvePresentation,
562
613
  resolveNativeRenderer,
@@ -589,10 +640,10 @@ function renderOwnedPresentation(params) {
589
640
  copyToClipboard: options?.copyToClipboard,
590
641
  });
591
642
  }
592
- export function createCollapsibleToolRendererResolver(options) {
593
- return createRendererResolver(options);
643
+ export function createCollapsibleToolRendererResolver(runtime, options) {
644
+ return createRendererResolver(runtime, options);
594
645
  }
595
- function createRendererResolver(options, resolvePresentation, resolveNativeRenderer) {
646
+ function createRendererResolver(runtime, options, resolvePresentation, resolveNativeRenderer) {
596
647
  const defaultLevel = options?.defaultCollapsed === false ? 1 : 0;
597
648
  return (toolName, next) => {
598
649
  const upstream = next();
@@ -603,7 +654,7 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
603
654
  return upstream;
604
655
  }
605
656
  const registeredNative = resolveNativeRenderer?.(toolName);
606
- const originalRenderCall = upstream?.renderCall ?? registeredNative?.renderCall;
657
+ const nativeArgsLine = upstream?.renderCall !== undefined || registeredNative?.renderCall !== undefined;
607
658
  const originalRenderResult = upstream?.renderResult ?? registeredNative?.renderResult;
608
659
  return {
609
660
  renderShell: "self",
@@ -611,6 +662,7 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
611
662
  if (provider !== undefined) {
612
663
  const callContext = { ...context, args };
613
664
  const view = renderOwnedPresentation({
665
+ runtime,
614
666
  toolName,
615
667
  presentation: provider(args, undefined, theme, callContext),
616
668
  theme,
@@ -626,26 +678,14 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
626
678
  const isStreamingArgs = context.isPartial && !context.argsComplete;
627
679
  const status = context.isError
628
680
  ? "error"
629
- : toolName === "codemode"
630
- ? "none"
631
- : isStreamingArgs
632
- ? "streaming"
633
- : "running";
634
- let summary = "";
635
- if (isStreamingArgs) {
636
- const tokens = estimateStreamingTokens(args);
637
- summary = tokens > 0 ? `· ${String(tokens)} tokens` : "";
638
- }
639
- else {
640
- summary = resolveToolCallSummary({
641
- toolName,
642
- args,
643
- originalRenderCall,
644
- theme,
645
- context,
646
- });
647
- }
648
- const heavyCmd = extractHeavyInputCommand(toolName, args);
681
+ : isStreamingArgs
682
+ ? "streaming"
683
+ : "running";
684
+ const streamedTokens = isStreamingArgs ? estimateStreamingTokens(args) : 0;
685
+ const header = isStreamingArgs
686
+ ? { summary: streamedTokens > 0 ? `· ${String(streamedTokens)} tokens` : "" }
687
+ : toolHeaderFacts({ toolName, args, theme, cwd: context.cwd, facts: [], nativeArgsLine });
688
+ const heavyCmd = nativeHeavyRequest(toolName, args);
649
689
  const sections = [];
650
690
  const inputSection = createInputCodeSection(toolName, args, theme);
651
691
  if (inputSection !== undefined) {
@@ -660,18 +700,15 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
660
700
  });
661
701
  }
662
702
  }
663
- const copy = heavyCmd !== undefined && heavyCmd !== ""
703
+ const copy = heavyCmd !== undefined
664
704
  ? { primary: heavyCmd }
665
705
  : formatArgsPreview(args) !== ""
666
706
  ? { primary: formatArgsPreview(args) }
667
707
  : undefined;
668
708
  const view = new ToolView({
709
+ background: () => runtime.backgrounds,
669
710
  theme,
670
- header: {
671
- title: toolName,
672
- status,
673
- summary,
674
- },
711
+ header: { title: toolName, status, ...header },
675
712
  sections,
676
713
  copy,
677
714
  outputPad: context.outputPad,
@@ -698,6 +735,7 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
698
735
  hideCallView(context);
699
736
  if (presentation !== undefined) {
700
737
  return renderOwnedPresentation({
738
+ runtime,
701
739
  toolName,
702
740
  presentation,
703
741
  result,
@@ -709,13 +747,14 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
709
747
  });
710
748
  }
711
749
  return renderGenericResult({
750
+ runtime,
712
751
  toolName,
713
752
  theme,
714
753
  context,
715
754
  result,
716
755
  renderOptions,
717
756
  defaultLevel,
718
- originalRenderCall,
757
+ nativeArgsLine,
719
758
  originalRenderResult,
720
759
  resolvePresentation,
721
760
  resolveNativeRenderer,
@@ -726,24 +765,50 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
726
765
  };
727
766
  }
728
767
  /** UI factory consumed by package-owned direct renderers, independently of host resolver precedence. */
729
- export function createOptionalToolRendererFactory(pi, options) {
730
- return (name, native) => createRendererResolver({ ...options, tools: [name] }, (toolName) => getRegisteredToolPresentationProvider(pi, toolName), (toolName) => getRegisteredToolRenderers(pi, toolName))(name, () => native) ?? native;
768
+ export function createOptionalToolRendererFactory(pi, runtime, options) {
769
+ return (name, native) => createRendererResolver(runtime, { ...options, tools: [name] }, (toolName) => getRegisteredToolPresentationProvider(pi, toolName), (toolName) => getRegisteredToolRenderers(pi, toolName))(name, () => native) ?? native;
731
770
  }
732
- export function registerCollapsibleToolRenderer(pi, options) {
733
- const disposers = [];
734
- if (typeof pi.on === "function") {
735
- const unbindStart = pi.on("tool_execution_start", (event) => {
736
- recordToolExecutionStart(event);
737
- });
738
- if (typeof unbindStart === "function")
739
- disposers.push(unbindStart);
740
- const unbindEnd = pi.on("tool_execution_end", (event) => {
741
- recordToolExecutionEnd(event);
742
- });
743
- if (typeof unbindEnd === "function")
744
- disposers.push(unbindEnd);
745
- }
746
- pi.registerToolRenderer(createRendererResolver(options, (name) => getRegisteredToolPresentationProvider(pi, name), (name) => getRegisteredToolRenderers(pi, name)));
771
+ /**
772
+ * Save the resolution facts of one model-issued call on its own tool result. The host emits
773
+ * `tool_call` only after the tool resolved and its arguments validated, so these ids are proof;
774
+ * writing them into the details keeps the fact with the call through resume, fork and reload.
775
+ */
776
+ function withResolvedCallFacts(event, runtime) {
777
+ if (event.parentToolCallId !== undefined)
778
+ return undefined;
779
+ const ids = runtime.tracker.getResolved(event.toolCallId);
780
+ if (ids === undefined || ids.size === 0)
781
+ return undefined;
782
+ if (!isRecord(event.details))
783
+ return undefined;
784
+ return { ...event.details, [RESOLVED_CALLS_KEY]: [...ids] };
785
+ }
786
+ export function registerCollapsibleToolRenderer(pi, runtime, options) {
787
+ const disposers = [
788
+ pi.on("tool_execution_start", (event) => {
789
+ runtime.tracker.recordStart(event);
790
+ }),
791
+ pi.on("tool_execution_end", (event) => {
792
+ runtime.tracker.recordEnd(event);
793
+ }),
794
+ pi.on("tool_call", (event) => {
795
+ runtime.tracker.recordResolved(event);
796
+ }),
797
+ pi.on("tool_result", (event) => {
798
+ const details = withResolvedCallFacts(event, runtime);
799
+ if (details === undefined)
800
+ return undefined;
801
+ // The hook replaces the whole result, so the fields it does not own pass through.
802
+ return {
803
+ details,
804
+ ...(event.structuredContent === undefined
805
+ ? {}
806
+ : { structuredContent: event.structuredContent }),
807
+ ...(event.usage === undefined ? {} : { usage: event.usage }),
808
+ };
809
+ }),
810
+ ];
811
+ pi.registerToolRenderer(createRendererResolver(runtime, options, (name) => getRegisteredToolPresentationProvider(pi, name), (name) => getRegisteredToolRenderers(pi, name)));
747
812
  return () => {
748
813
  for (const dispose of disposers)
749
814
  dispose();