@hheei/pi-ext-ui 0.5.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/renderer.js CHANGED
@@ -1,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,39 +36,129 @@ 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);
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);
52
72
  }
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();
73
+ const summary = nativeArgsLine === true ? genericArgsLine(args) : "";
74
+ return { summary: [summary, ...secondary].filter(Boolean).join(" · ") };
75
+ }
76
+ function isCapturedCall(call) {
77
+ return "args" in call || "result" in call;
78
+ }
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 }),
71
99
  }
72
- return firstNonEmpty;
73
- }
74
- }
75
- catch {
76
- // Upstream threw, fall through to fallback
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. */
130
+ function nestedChildren(calls) {
131
+ return calls.map((call) => {
132
+ if (isCapturedCall(call)) {
133
+ const isError = call.isError === true;
134
+ const inline = inlinePresentation(call.toolName, call.args, call.result?.details, isError);
135
+ return {
136
+ title: call.toolName,
137
+ ...(inline === undefined ? {} : { summary: inline.summary }),
138
+ // The tracker records a call when it starts, so a missing result means it is still
139
+ // running, not that it succeeded.
140
+ status: isError
141
+ ? "error"
142
+ : call.result === undefined
143
+ ? "running"
144
+ : "ok",
145
+ ...(call.durationMs === undefined ? {} : { durationMs: call.durationMs }),
146
+ };
77
147
  }
78
- }
79
- // Tier 2: Contractual Fallback for built-in tools
80
- return extractSummaryArg(toolName, args);
148
+ return {
149
+ title: call.toolName,
150
+ ...(call.summary === undefined ? {} : { summary: call.summary }),
151
+ // Same precedence as formatNestedCallRow: a cancelled call is a warning, not a success.
152
+ status: call.status === "running"
153
+ ? "running"
154
+ : call.isWarning === true
155
+ ? "warn"
156
+ : call.isError === true || call.status === "error"
157
+ ? "error"
158
+ : "ok",
159
+ ...(call.durationMs === undefined ? {} : { durationMs: call.durationMs }),
160
+ };
161
+ });
81
162
  }
82
163
  function formatArgsPreview(args) {
83
164
  if (args === undefined || args === null)
@@ -115,7 +196,7 @@ function hideCallView(context) {
115
196
  if (view instanceof ToolView)
116
197
  view.hide();
117
198
  }
118
- function formatNestedCallRow(call, theme) {
199
+ function formatNestedCallRow(call, theme, cwd) {
119
200
  const inline = inlinePresentation(call.toolName, call.summary, undefined, call.isError === true);
120
201
  const status = call.status === "running"
121
202
  ? "running"
@@ -126,13 +207,20 @@ function formatNestedCallRow(call, theme) {
126
207
  ? ""
127
208
  : stripTerminalSequences(call.summary.trim());
128
209
  const truncatedArgs = cleanArgs.length > 72 ? `${cleanArgs.slice(0, 69)}…` : cleanArgs;
129
- 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.
130
219
  return formatToolHeaderText({
131
220
  status,
132
221
  title: call.toolName,
133
- summary: [inline?.summary ?? truncatedArgs, formatDurationMs(call.durationMs), costText]
134
- .filter(Boolean)
135
- .join(" · "),
222
+ ...header,
223
+ summary: header.summary || (inline?.summary ?? truncatedArgs),
136
224
  }, theme);
137
225
  }
138
226
  export function isMcpTool(toolName) {
@@ -155,20 +243,23 @@ function shouldInterceptTool(toolName, options) {
155
243
  return true;
156
244
  }
157
245
  function renderGenericResult(params) {
158
- const { toolName, theme, context, result, renderOptions, defaultLevel, originalRenderCall, originalRenderResult, resolvePresentation, resolveNativeRenderer, options, } = params;
159
- 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);
160
248
  const nestedCalls = extractNestedCalls(result);
161
249
  const captured = toolName === "codemode"
162
- ? (getCapturedNestedCalls(context.toolCallId) ??
250
+ ? (runtime.tracker.getCaptured(context.toolCallId) ??
163
251
  (isRecord(result.details) &&
164
252
  Array.isArray(result.details.__piExtUiCapturedCalls)
165
253
  ? result.details
166
254
  .__piExtUiCapturedCalls
167
255
  : undefined))
168
256
  : undefined;
169
- 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;
170
261
  const isMetaTool = toolName === "codemode" || toolName === "subagent" || childCalls.length > 0;
171
- const { outputText: rawOutput, wallTimeSeconds } = extractResultText(toolName, result);
262
+ const { outputText: rawOutput, wallTimeSeconds } = nativeResultText(toolName, result);
172
263
  const isError = (context.isError ?? false) || (result.isError ?? false);
173
264
  const status = toolName === "codemode"
174
265
  ? childCalls.length === 0 && isError
@@ -186,42 +277,47 @@ function renderGenericResult(params) {
186
277
  : renderOptions.isPartial
187
278
  ? "running"
188
279
  : "ok";
189
- const summaryParts = [];
190
- const argSummary = resolveToolCallSummary({
191
- toolName,
192
- args: context.args,
193
- originalRenderCall,
194
- theme,
195
- context,
196
- });
197
- if (argSummary !== "")
198
- summaryParts.push(argSummary);
280
+ const facts = [];
199
281
  if (isMetaTool && childCalls.length > 0) {
200
- const failCount = childCalls.filter((c) => Boolean(c.isError || ("status" in c && c.status === "error"))).length;
201
- summaryParts.push(...callStatsParts(childCalls.length, failCount));
282
+ // Only a proven resolution is a call; the rest stay visible as unknown instead of counting.
283
+ const failCount = resolvedCalls.filter((c) => Boolean(c.isError || ("status" in c && c.status === "error"))).length;
284
+ facts.push(...callStatsParts(resolvedCalls.length, failCount));
285
+ if (unknownCallCount > 0)
286
+ facts.push(`${String(unknownCallCount)} unknown`);
202
287
  }
203
288
  if (context.durationMs !== undefined) {
204
- summaryParts.push(formatDurationMs(context.durationMs));
289
+ facts.push(formatDurationMs(context.durationMs));
205
290
  }
206
291
  else if (wallTimeSeconds !== undefined && Number.isFinite(wallTimeSeconds)) {
207
- summaryParts.push(formatDurationMs(wallTimeSeconds * 1000));
292
+ facts.push(formatDurationMs(wallTimeSeconds * 1000));
208
293
  }
209
294
  const totalCost = childCalls.reduce((sum, c) => sum + (c.cost ?? 0), 0);
210
- if (totalCost > 0) {
211
- summaryParts.push(formatCostUsd(totalCost));
212
- }
213
- const summary = summaryParts.join(" · ");
295
+ if (totalCost > 0)
296
+ facts.push(formatCostUsd(totalCost));
297
+ const timeout = nativeTimeoutFact(toolName, context.args);
298
+ if (timeout !== undefined)
299
+ facts.unshift(timeout);
300
+ const header = toolHeaderFacts({
301
+ toolName,
302
+ args: context.args,
303
+ theme,
304
+ cwd: context.cwd,
305
+ facts,
306
+ nativeArgsLine,
307
+ });
214
308
  const codemodeNestedView = toolName === "codemode" && childCalls.length > 0
215
309
  ? new NestedCallsView({
216
310
  calls: childCalls,
217
311
  theme,
218
312
  context,
313
+ headerSummary: (childName, childArgs, childTheme) => nativeHeaderSummary(childName, childArgs, childTheme, context.cwd)?.summary,
219
314
  resolveChildRenderer: (childName) => {
220
315
  return {
221
316
  renderResult: (childResult, childOptions, childTheme, childCtx) => {
222
317
  const provider = resolvePresentation?.(childName);
223
318
  if (provider !== undefined) {
224
319
  return renderOwnedPresentation({
320
+ runtime,
225
321
  toolName: childName,
226
322
  presentation: provider(childCtx.args, childResult, childTheme, childCtx),
227
323
  result: childResult,
@@ -235,13 +331,14 @@ function renderGenericResult(params) {
235
331
  }
236
332
  const registered = resolveNativeRenderer?.(childName);
237
333
  return renderGenericResult({
334
+ runtime,
238
335
  toolName: childName,
239
336
  theme: childTheme,
240
337
  context: childCtx,
241
338
  result: childResult,
242
339
  renderOptions: childOptions,
243
340
  defaultLevel: childOptions.expanded ? 2 : 1,
244
- originalRenderCall: registered?.renderCall,
341
+ nativeArgsLine: registered?.renderCall !== undefined,
245
342
  originalRenderResult: registered?.renderResult,
246
343
  resolvePresentation,
247
344
  resolveNativeRenderer,
@@ -256,10 +353,11 @@ function renderGenericResult(params) {
256
353
  invalidate: () => context.invalidate(),
257
354
  })
258
355
  : undefined;
356
+ const children = nestedChildren(childCalls);
259
357
  const collapsedRows = codemodeNestedView !== undefined
260
358
  ? codemodeNestedView
261
359
  : isMetaTool && nestedCalls.length > 0
262
- ? nestedCalls.map((call) => formatNestedCallRow(call, theme))
360
+ ? nestedCalls.map((call) => formatNestedCallRow(call, theme, context.cwd))
263
361
  : undefined;
264
362
  const toolRailToken = isError
265
363
  ? "error"
@@ -293,6 +391,7 @@ function renderGenericResult(params) {
293
391
  }
294
392
  else if (originalRenderResult !== undefined) {
295
393
  const viewProbe = new ToolView({
394
+ background: () => runtime.backgrounds,
296
395
  theme,
297
396
  header: { title: toolName },
298
397
  stateHolder: context.state,
@@ -307,18 +406,13 @@ function renderGenericResult(params) {
307
406
  let lastUpstreamComponent;
308
407
  const lazyUpstreamComponent = {
309
408
  render(w) {
310
- try {
311
- const upstreamContext = {
312
- ...context,
313
- lastComponent: lastUpstreamComponent,
314
- };
315
- const comp = originalRenderResult(result, effectiveOptions, theme, upstreamContext);
316
- lastUpstreamComponent = comp;
317
- return comp.render(w);
318
- }
319
- catch {
320
- return rawOutput !== "" ? rawOutput.split("\n") : [];
321
- }
409
+ const upstreamContext = {
410
+ ...context,
411
+ lastComponent: lastUpstreamComponent,
412
+ };
413
+ const comp = originalRenderResult(result, effectiveOptions, theme, upstreamContext);
414
+ lastUpstreamComponent = comp;
415
+ return comp.render(w);
322
416
  },
323
417
  invalidate() { },
324
418
  };
@@ -342,7 +436,7 @@ function renderGenericResult(params) {
342
436
  if (toolName === "codemode") {
343
437
  copy = undefined;
344
438
  }
345
- else if (heavyCmd !== undefined && heavyCmd !== "") {
439
+ else if (heavyCmd !== undefined) {
346
440
  const cleanOutput = stripTerminalSequences(rawOutput);
347
441
  copy = {
348
442
  primary: heavyCmd,
@@ -358,14 +452,15 @@ function renderGenericResult(params) {
358
452
  };
359
453
  }
360
454
  return new ToolView({
455
+ background: () => runtime.backgrounds,
361
456
  theme,
362
- header: {
363
- title: toolName,
364
- status,
365
- summary,
366
- },
457
+ header: { title: toolName, status, ...header },
367
458
  railToken: toolRailToken,
368
459
  collapsedRows,
460
+ ...(children.length === 0 ? {} : { children }),
461
+ // A container with child facts states its effective count, including zero: a container whose
462
+ // children all failed to resolve is not one call.
463
+ ...(childCalls.length === 0 ? {} : { childCount: resolvedCalls.length }),
369
464
  sections,
370
465
  copy,
371
466
  outputPad: context.outputPad,
@@ -381,7 +476,7 @@ function renderGenericResult(params) {
381
476
  * pass never asks the tool to rebuild its rows.
382
477
  */
383
478
  function renderOwnedPresentation(params) {
384
- const { toolName, presentation, result, theme, context, defaultLevel, resolvePresentation, resolveNativeRenderer, options, } = params;
479
+ const { runtime, toolName, presentation, result, theme, context, defaultLevel, resolvePresentation, resolveNativeRenderer, options, } = params;
385
480
  const isError = context.isError || result?.isError === true;
386
481
  const pending = result === undefined || context.isPartial;
387
482
  const isDiff = !isError && !pending && presentation.kind === "diff";
@@ -422,26 +517,16 @@ function renderOwnedPresentation(params) {
422
517
  }
423
518
  else if (output !== "") {
424
519
  const rawLines = stripTerminalSequences(output).split("\n");
520
+ const lang = nativeOutputLanguage(toolName, context.args);
425
521
  let styledLines;
426
522
  if (isError) {
427
523
  styledLines = rawLines.map((line) => theme.fg("error", line));
428
524
  }
429
- else if (toolName === "read") {
430
- const rawPath = isRecord(context.args) && typeof context.args.path === "string"
431
- ? context.args.path
432
- : isRecord(context.args) && typeof context.args.file_path === "string"
433
- ? context.args.file_path
434
- : "";
435
- const lang = rawPath ? getLanguageFromPath(rawPath) : undefined;
436
- if (lang !== undefined) {
437
- try {
438
- styledLines = highlightCode(output.replace(/\t/g, " "), lang);
439
- }
440
- catch {
441
- styledLines = rawLines.map((line) => theme.fg("toolOutput", line));
442
- }
525
+ else if (lang !== undefined) {
526
+ try {
527
+ styledLines = highlightCode(output.replace(/\t/g, " "), lang);
443
528
  }
444
- else {
529
+ catch {
445
530
  styledLines = rawLines.map((line) => theme.fg("toolOutput", line));
446
531
  }
447
532
  }
@@ -454,22 +539,18 @@ function renderOwnedPresentation(params) {
454
539
  });
455
540
  }
456
541
  const duration = result === undefined ? "" : formatDurationMs(context.durationMs);
457
- const summary = [stripTerminalSequences(presentation.summary), duration]
458
- .filter(Boolean)
459
- .join(" · ");
460
- const copy = presentation.copy === undefined
461
- ? isDiff && presentation.diff.trim() !== ""
462
- ? { primary: presentation.diff }
463
- : undefined
464
- : {
465
- primary: stripTerminalSequences(presentation.copy.primary),
466
- ...(presentation.copy.full === undefined
467
- ? {}
468
- : { full: stripTerminalSequences(presentation.copy.full) }),
469
- };
542
+ // The provider owns the request/output boundary; cleaning its payload here corrupts raw input.
543
+ const copy = presentation.copy ??
544
+ (isDiff && presentation.diff.trim() !== "" ? { primary: presentation.diff } : undefined);
470
545
  return new ToolView({
546
+ background: () => runtime.backgrounds,
471
547
  theme,
472
- header: { title: toolName, status, summary },
548
+ header: {
549
+ title: toolName,
550
+ status,
551
+ summary: stripTerminalSequences(presentation.summary),
552
+ ...(duration === "" ? {} : { meta: duration }),
553
+ },
473
554
  railToken,
474
555
  // The tool owns the folded rows and their tones; the frame only sanitizes them for the host.
475
556
  collapsedRows: (() => {
@@ -477,23 +558,29 @@ function renderOwnedPresentation(params) {
477
558
  return presentation.collapsedRows;
478
559
  }
479
560
  const captured = result !== undefined
480
- ? (getCapturedNestedCalls(context.toolCallId) ??
561
+ ? (runtime.tracker.getCaptured(context.toolCallId) ??
481
562
  (isRecord(result.details) && Array.isArray(result.details.__piExtUiCapturedCalls)
482
563
  ? result.details.__piExtUiCapturedCalls
483
564
  : undefined))
484
565
  : undefined;
485
566
  const nestedCalls = extractNestedCalls(result);
486
- const childCalls = captured ?? nestedCalls;
567
+ // The owner's own child facts win over parsing another package's details.
568
+ const ownCalls = presentation.children === undefined
569
+ ? nestedCalls
570
+ : presentationChildCalls(presentation.children);
571
+ const childCalls = withSessionArgs(captured ?? ownCalls, runtime);
487
572
  if (childCalls.length > 0) {
488
573
  return new NestedCallsView({
489
574
  calls: childCalls,
490
575
  theme,
491
576
  context,
577
+ headerSummary: (childName, childArgs, childTheme) => nativeHeaderSummary(childName, childArgs, childTheme, context.cwd)?.summary,
492
578
  resolveChildRenderer: (childName) => ({
493
579
  renderResult: (childResult, childOptions, childTheme, childCtx) => {
494
580
  const childProvider = resolvePresentation?.(childName);
495
581
  if (childProvider !== undefined) {
496
582
  return renderOwnedPresentation({
583
+ runtime,
497
584
  toolName: childName,
498
585
  presentation: childProvider(childCtx.args, childResult, childTheme, childCtx),
499
586
  result: childResult,
@@ -507,13 +594,14 @@ function renderOwnedPresentation(params) {
507
594
  }
508
595
  const registered = resolveNativeRenderer?.(childName);
509
596
  return renderGenericResult({
597
+ runtime,
510
598
  toolName: childName,
511
599
  theme: childTheme,
512
600
  context: childCtx,
513
601
  result: childResult,
514
602
  renderOptions: childOptions,
515
603
  defaultLevel: childOptions.expanded ? 2 : 1,
516
- originalRenderCall: registered?.renderCall,
604
+ nativeArgsLine: registered?.renderCall !== undefined,
517
605
  originalRenderResult: registered?.renderResult,
518
606
  resolvePresentation,
519
607
  resolveNativeRenderer,
@@ -534,6 +622,8 @@ function renderOwnedPresentation(params) {
534
622
  .map((line) => theme.fg("error", stripTerminalSequences(line)))
535
623
  : undefined;
536
624
  })(),
625
+ ...(presentation.children === undefined ? {} : { children: presentation.children }),
626
+ ...(presentation.childCount === undefined ? {} : { childCount: presentation.childCount }),
537
627
  sections,
538
628
  copy,
539
629
  outputPad: context.outputPad,
@@ -544,10 +634,10 @@ function renderOwnedPresentation(params) {
544
634
  copyToClipboard: options?.copyToClipboard,
545
635
  });
546
636
  }
547
- export function createCollapsibleToolRendererResolver(options) {
548
- return createRendererResolver(options);
637
+ export function createCollapsibleToolRendererResolver(runtime, options) {
638
+ return createRendererResolver(runtime, options);
549
639
  }
550
- function createRendererResolver(options, resolvePresentation, resolveNativeRenderer) {
640
+ function createRendererResolver(runtime, options, resolvePresentation, resolveNativeRenderer) {
551
641
  const defaultLevel = options?.defaultCollapsed === false ? 1 : 0;
552
642
  return (toolName, next) => {
553
643
  const upstream = next();
@@ -558,7 +648,7 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
558
648
  return upstream;
559
649
  }
560
650
  const registeredNative = resolveNativeRenderer?.(toolName);
561
- const originalRenderCall = upstream?.renderCall ?? registeredNative?.renderCall;
651
+ const nativeArgsLine = upstream?.renderCall !== undefined || registeredNative?.renderCall !== undefined;
562
652
  const originalRenderResult = upstream?.renderResult ?? registeredNative?.renderResult;
563
653
  return {
564
654
  renderShell: "self",
@@ -566,6 +656,7 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
566
656
  if (provider !== undefined) {
567
657
  const callContext = { ...context, args };
568
658
  const view = renderOwnedPresentation({
659
+ runtime,
569
660
  toolName,
570
661
  presentation: provider(args, undefined, theme, callContext),
571
662
  theme,
@@ -586,21 +677,11 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
586
677
  : isStreamingArgs
587
678
  ? "streaming"
588
679
  : "running";
589
- let summary = "";
590
- if (isStreamingArgs) {
591
- const tokens = estimateStreamingTokens(args);
592
- summary = tokens > 0 ? `· ${String(tokens)} tokens` : "";
593
- }
594
- else {
595
- summary = resolveToolCallSummary({
596
- toolName,
597
- args,
598
- originalRenderCall,
599
- theme,
600
- context,
601
- });
602
- }
603
- const heavyCmd = extractHeavyInputCommand(toolName, args);
680
+ const streamedTokens = isStreamingArgs ? estimateStreamingTokens(args) : 0;
681
+ const header = isStreamingArgs
682
+ ? { summary: streamedTokens > 0 ? `· ${String(streamedTokens)} tokens` : "" }
683
+ : toolHeaderFacts({ toolName, args, theme, cwd: context.cwd, facts: [], nativeArgsLine });
684
+ const heavyCmd = nativeHeavyRequest(toolName, args);
604
685
  const sections = [];
605
686
  const inputSection = createInputCodeSection(toolName, args, theme);
606
687
  if (inputSection !== undefined) {
@@ -615,18 +696,15 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
615
696
  });
616
697
  }
617
698
  }
618
- const copy = heavyCmd !== undefined && heavyCmd !== ""
699
+ const copy = heavyCmd !== undefined
619
700
  ? { primary: heavyCmd }
620
701
  : formatArgsPreview(args) !== ""
621
702
  ? { primary: formatArgsPreview(args) }
622
703
  : undefined;
623
704
  const view = new ToolView({
705
+ background: () => runtime.backgrounds,
624
706
  theme,
625
- header: {
626
- title: toolName,
627
- status,
628
- summary,
629
- },
707
+ header: { title: toolName, status, ...header },
630
708
  sections,
631
709
  copy,
632
710
  outputPad: context.outputPad,
@@ -653,6 +731,7 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
653
731
  hideCallView(context);
654
732
  if (presentation !== undefined) {
655
733
  return renderOwnedPresentation({
734
+ runtime,
656
735
  toolName,
657
736
  presentation,
658
737
  result,
@@ -664,13 +743,14 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
664
743
  });
665
744
  }
666
745
  return renderGenericResult({
746
+ runtime,
667
747
  toolName,
668
748
  theme,
669
749
  context,
670
750
  result,
671
751
  renderOptions,
672
752
  defaultLevel,
673
- originalRenderCall,
753
+ nativeArgsLine,
674
754
  originalRenderResult,
675
755
  resolvePresentation,
676
756
  resolveNativeRenderer,
@@ -681,24 +761,50 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
681
761
  };
682
762
  }
683
763
  /** UI factory consumed by package-owned direct renderers, independently of host resolver precedence. */
684
- export function createOptionalToolRendererFactory(pi, options) {
685
- return (name, native) => createRendererResolver({ ...options, tools: [name] }, (toolName) => getRegisteredToolPresentationProvider(pi, toolName), (toolName) => getRegisteredToolRenderers(pi, toolName))(name, () => native) ?? native;
764
+ export function createOptionalToolRendererFactory(pi, runtime, options) {
765
+ return (name, native) => createRendererResolver(runtime, { ...options, tools: [name] }, (toolName) => getRegisteredToolPresentationProvider(pi, toolName), (toolName) => getRegisteredToolRenderers(pi, toolName))(name, () => native) ?? native;
686
766
  }
687
- export function registerCollapsibleToolRenderer(pi, options) {
688
- const disposers = [];
689
- if (typeof pi.on === "function") {
690
- const unbindStart = pi.on("tool_execution_start", (event) => {
691
- recordToolExecutionStart(event);
692
- });
693
- if (typeof unbindStart === "function")
694
- disposers.push(unbindStart);
695
- const unbindEnd = pi.on("tool_execution_end", (event) => {
696
- recordToolExecutionEnd(event);
697
- });
698
- if (typeof unbindEnd === "function")
699
- disposers.push(unbindEnd);
700
- }
701
- pi.registerToolRenderer(createRendererResolver(options, (name) => getRegisteredToolPresentationProvider(pi, name), (name) => getRegisteredToolRenderers(pi, name)));
767
+ /**
768
+ * Save the resolution facts of one model-issued call on its own tool result. The host emits
769
+ * `tool_call` only after the tool resolved and its arguments validated, so these ids are proof;
770
+ * writing them into the details keeps the fact with the call through resume, fork and reload.
771
+ */
772
+ function withResolvedCallFacts(event, runtime) {
773
+ if (event.parentToolCallId !== undefined)
774
+ return undefined;
775
+ const ids = runtime.tracker.getResolved(event.toolCallId);
776
+ if (ids === undefined || ids.size === 0)
777
+ return undefined;
778
+ if (!isRecord(event.details))
779
+ return undefined;
780
+ return { ...event.details, [RESOLVED_CALLS_KEY]: [...ids] };
781
+ }
782
+ export function registerCollapsibleToolRenderer(pi, runtime, options) {
783
+ const disposers = [
784
+ pi.on("tool_execution_start", (event) => {
785
+ runtime.tracker.recordStart(event);
786
+ }),
787
+ pi.on("tool_execution_end", (event) => {
788
+ runtime.tracker.recordEnd(event);
789
+ }),
790
+ pi.on("tool_call", (event) => {
791
+ runtime.tracker.recordResolved(event);
792
+ }),
793
+ pi.on("tool_result", (event) => {
794
+ const details = withResolvedCallFacts(event, runtime);
795
+ if (details === undefined)
796
+ return undefined;
797
+ // The hook replaces the whole result, so the fields it does not own pass through.
798
+ return {
799
+ details,
800
+ ...(event.structuredContent === undefined
801
+ ? {}
802
+ : { structuredContent: event.structuredContent }),
803
+ ...(event.usage === undefined ? {} : { usage: event.usage }),
804
+ };
805
+ }),
806
+ ];
807
+ pi.registerToolRenderer(createRendererResolver(runtime, options, (name) => getRegisteredToolPresentationProvider(pi, name), (name) => getRegisteredToolRenderers(pi, name)));
702
808
  return () => {
703
809
  for (const dispose of disposers)
704
810
  dispose();