@hheei/pi-ext-ui 0.6.0 → 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,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,20 +243,23 @@ 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
265
  ? childCalls.length === 0 && isError
@@ -227,42 +277,47 @@ function renderGenericResult(params) {
227
277
  : renderOptions.isPartial
228
278
  ? "running"
229
279
  : "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);
280
+ const facts = [];
240
281
  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));
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`);
243
287
  }
244
288
  if (context.durationMs !== undefined) {
245
- summaryParts.push(formatDurationMs(context.durationMs));
289
+ facts.push(formatDurationMs(context.durationMs));
246
290
  }
247
291
  else if (wallTimeSeconds !== undefined && Number.isFinite(wallTimeSeconds)) {
248
- summaryParts.push(formatDurationMs(wallTimeSeconds * 1000));
292
+ facts.push(formatDurationMs(wallTimeSeconds * 1000));
249
293
  }
250
294
  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(" · ");
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
+ });
255
308
  const codemodeNestedView = toolName === "codemode" && childCalls.length > 0
256
309
  ? new NestedCallsView({
257
310
  calls: childCalls,
258
311
  theme,
259
312
  context,
313
+ headerSummary: (childName, childArgs, childTheme) => nativeHeaderSummary(childName, childArgs, childTheme, context.cwd)?.summary,
260
314
  resolveChildRenderer: (childName) => {
261
315
  return {
262
316
  renderResult: (childResult, childOptions, childTheme, childCtx) => {
263
317
  const provider = resolvePresentation?.(childName);
264
318
  if (provider !== undefined) {
265
319
  return renderOwnedPresentation({
320
+ runtime,
266
321
  toolName: childName,
267
322
  presentation: provider(childCtx.args, childResult, childTheme, childCtx),
268
323
  result: childResult,
@@ -276,13 +331,14 @@ function renderGenericResult(params) {
276
331
  }
277
332
  const registered = resolveNativeRenderer?.(childName);
278
333
  return renderGenericResult({
334
+ runtime,
279
335
  toolName: childName,
280
336
  theme: childTheme,
281
337
  context: childCtx,
282
338
  result: childResult,
283
339
  renderOptions: childOptions,
284
340
  defaultLevel: childOptions.expanded ? 2 : 1,
285
- originalRenderCall: registered?.renderCall,
341
+ nativeArgsLine: registered?.renderCall !== undefined,
286
342
  originalRenderResult: registered?.renderResult,
287
343
  resolvePresentation,
288
344
  resolveNativeRenderer,
@@ -301,7 +357,7 @@ function renderGenericResult(params) {
301
357
  const collapsedRows = codemodeNestedView !== undefined
302
358
  ? codemodeNestedView
303
359
  : isMetaTool && nestedCalls.length > 0
304
- ? nestedCalls.map((call) => formatNestedCallRow(call, theme))
360
+ ? nestedCalls.map((call) => formatNestedCallRow(call, theme, context.cwd))
305
361
  : undefined;
306
362
  const toolRailToken = isError
307
363
  ? "error"
@@ -335,6 +391,7 @@ function renderGenericResult(params) {
335
391
  }
336
392
  else if (originalRenderResult !== undefined) {
337
393
  const viewProbe = new ToolView({
394
+ background: () => runtime.backgrounds,
338
395
  theme,
339
396
  header: { title: toolName },
340
397
  stateHolder: context.state,
@@ -349,18 +406,13 @@ function renderGenericResult(params) {
349
406
  let lastUpstreamComponent;
350
407
  const lazyUpstreamComponent = {
351
408
  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
- }
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);
364
416
  },
365
417
  invalidate() { },
366
418
  };
@@ -384,7 +436,7 @@ function renderGenericResult(params) {
384
436
  if (toolName === "codemode") {
385
437
  copy = undefined;
386
438
  }
387
- else if (heavyCmd !== undefined && heavyCmd !== "") {
439
+ else if (heavyCmd !== undefined) {
388
440
  const cleanOutput = stripTerminalSequences(rawOutput);
389
441
  copy = {
390
442
  primary: heavyCmd,
@@ -400,15 +452,15 @@ function renderGenericResult(params) {
400
452
  };
401
453
  }
402
454
  return new ToolView({
455
+ background: () => runtime.backgrounds,
403
456
  theme,
404
- header: {
405
- title: toolName,
406
- status,
407
- summary,
408
- },
457
+ header: { title: toolName, status, ...header },
409
458
  railToken: toolRailToken,
410
459
  collapsedRows,
411
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 }),
412
464
  sections,
413
465
  copy,
414
466
  outputPad: context.outputPad,
@@ -424,7 +476,7 @@ function renderGenericResult(params) {
424
476
  * pass never asks the tool to rebuild its rows.
425
477
  */
426
478
  function renderOwnedPresentation(params) {
427
- const { toolName, presentation, result, theme, context, defaultLevel, resolvePresentation, resolveNativeRenderer, options, } = params;
479
+ const { runtime, toolName, presentation, result, theme, context, defaultLevel, resolvePresentation, resolveNativeRenderer, options, } = params;
428
480
  const isError = context.isError || result?.isError === true;
429
481
  const pending = result === undefined || context.isPartial;
430
482
  const isDiff = !isError && !pending && presentation.kind === "diff";
@@ -465,26 +517,16 @@ function renderOwnedPresentation(params) {
465
517
  }
466
518
  else if (output !== "") {
467
519
  const rawLines = stripTerminalSequences(output).split("\n");
520
+ const lang = nativeOutputLanguage(toolName, context.args);
468
521
  let styledLines;
469
522
  if (isError) {
470
523
  styledLines = rawLines.map((line) => theme.fg("error", line));
471
524
  }
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
- }
525
+ else if (lang !== undefined) {
526
+ try {
527
+ styledLines = highlightCode(output.replace(/\t/g, " "), lang);
486
528
  }
487
- else {
529
+ catch {
488
530
  styledLines = rawLines.map((line) => theme.fg("toolOutput", line));
489
531
  }
490
532
  }
@@ -497,22 +539,18 @@ function renderOwnedPresentation(params) {
497
539
  });
498
540
  }
499
541
  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
- };
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);
513
545
  return new ToolView({
546
+ background: () => runtime.backgrounds,
514
547
  theme,
515
- header: { title: toolName, status, summary },
548
+ header: {
549
+ title: toolName,
550
+ status,
551
+ summary: stripTerminalSequences(presentation.summary),
552
+ ...(duration === "" ? {} : { meta: duration }),
553
+ },
516
554
  railToken,
517
555
  // The tool owns the folded rows and their tones; the frame only sanitizes them for the host.
518
556
  collapsedRows: (() => {
@@ -520,23 +558,29 @@ function renderOwnedPresentation(params) {
520
558
  return presentation.collapsedRows;
521
559
  }
522
560
  const captured = result !== undefined
523
- ? (getCapturedNestedCalls(context.toolCallId) ??
561
+ ? (runtime.tracker.getCaptured(context.toolCallId) ??
524
562
  (isRecord(result.details) && Array.isArray(result.details.__piExtUiCapturedCalls)
525
563
  ? result.details.__piExtUiCapturedCalls
526
564
  : undefined))
527
565
  : undefined;
528
566
  const nestedCalls = extractNestedCalls(result);
529
- 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);
530
572
  if (childCalls.length > 0) {
531
573
  return new NestedCallsView({
532
574
  calls: childCalls,
533
575
  theme,
534
576
  context,
577
+ headerSummary: (childName, childArgs, childTheme) => nativeHeaderSummary(childName, childArgs, childTheme, context.cwd)?.summary,
535
578
  resolveChildRenderer: (childName) => ({
536
579
  renderResult: (childResult, childOptions, childTheme, childCtx) => {
537
580
  const childProvider = resolvePresentation?.(childName);
538
581
  if (childProvider !== undefined) {
539
582
  return renderOwnedPresentation({
583
+ runtime,
540
584
  toolName: childName,
541
585
  presentation: childProvider(childCtx.args, childResult, childTheme, childCtx),
542
586
  result: childResult,
@@ -550,13 +594,14 @@ function renderOwnedPresentation(params) {
550
594
  }
551
595
  const registered = resolveNativeRenderer?.(childName);
552
596
  return renderGenericResult({
597
+ runtime,
553
598
  toolName: childName,
554
599
  theme: childTheme,
555
600
  context: childCtx,
556
601
  result: childResult,
557
602
  renderOptions: childOptions,
558
603
  defaultLevel: childOptions.expanded ? 2 : 1,
559
- originalRenderCall: registered?.renderCall,
604
+ nativeArgsLine: registered?.renderCall !== undefined,
560
605
  originalRenderResult: registered?.renderResult,
561
606
  resolvePresentation,
562
607
  resolveNativeRenderer,
@@ -589,10 +634,10 @@ function renderOwnedPresentation(params) {
589
634
  copyToClipboard: options?.copyToClipboard,
590
635
  });
591
636
  }
592
- export function createCollapsibleToolRendererResolver(options) {
593
- return createRendererResolver(options);
637
+ export function createCollapsibleToolRendererResolver(runtime, options) {
638
+ return createRendererResolver(runtime, options);
594
639
  }
595
- function createRendererResolver(options, resolvePresentation, resolveNativeRenderer) {
640
+ function createRendererResolver(runtime, options, resolvePresentation, resolveNativeRenderer) {
596
641
  const defaultLevel = options?.defaultCollapsed === false ? 1 : 0;
597
642
  return (toolName, next) => {
598
643
  const upstream = next();
@@ -603,7 +648,7 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
603
648
  return upstream;
604
649
  }
605
650
  const registeredNative = resolveNativeRenderer?.(toolName);
606
- const originalRenderCall = upstream?.renderCall ?? registeredNative?.renderCall;
651
+ const nativeArgsLine = upstream?.renderCall !== undefined || registeredNative?.renderCall !== undefined;
607
652
  const originalRenderResult = upstream?.renderResult ?? registeredNative?.renderResult;
608
653
  return {
609
654
  renderShell: "self",
@@ -611,6 +656,7 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
611
656
  if (provider !== undefined) {
612
657
  const callContext = { ...context, args };
613
658
  const view = renderOwnedPresentation({
659
+ runtime,
614
660
  toolName,
615
661
  presentation: provider(args, undefined, theme, callContext),
616
662
  theme,
@@ -631,21 +677,11 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
631
677
  : isStreamingArgs
632
678
  ? "streaming"
633
679
  : "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);
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);
649
685
  const sections = [];
650
686
  const inputSection = createInputCodeSection(toolName, args, theme);
651
687
  if (inputSection !== undefined) {
@@ -660,18 +696,15 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
660
696
  });
661
697
  }
662
698
  }
663
- const copy = heavyCmd !== undefined && heavyCmd !== ""
699
+ const copy = heavyCmd !== undefined
664
700
  ? { primary: heavyCmd }
665
701
  : formatArgsPreview(args) !== ""
666
702
  ? { primary: formatArgsPreview(args) }
667
703
  : undefined;
668
704
  const view = new ToolView({
705
+ background: () => runtime.backgrounds,
669
706
  theme,
670
- header: {
671
- title: toolName,
672
- status,
673
- summary,
674
- },
707
+ header: { title: toolName, status, ...header },
675
708
  sections,
676
709
  copy,
677
710
  outputPad: context.outputPad,
@@ -698,6 +731,7 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
698
731
  hideCallView(context);
699
732
  if (presentation !== undefined) {
700
733
  return renderOwnedPresentation({
734
+ runtime,
701
735
  toolName,
702
736
  presentation,
703
737
  result,
@@ -709,13 +743,14 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
709
743
  });
710
744
  }
711
745
  return renderGenericResult({
746
+ runtime,
712
747
  toolName,
713
748
  theme,
714
749
  context,
715
750
  result,
716
751
  renderOptions,
717
752
  defaultLevel,
718
- originalRenderCall,
753
+ nativeArgsLine,
719
754
  originalRenderResult,
720
755
  resolvePresentation,
721
756
  resolveNativeRenderer,
@@ -726,24 +761,50 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
726
761
  };
727
762
  }
728
763
  /** 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;
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;
731
766
  }
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)));
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)));
747
808
  return () => {
748
809
  for (const dispose of disposers)
749
810
  dispose();