@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/README.md +2 -2
- package/dist/background.d.ts +11 -0
- package/dist/background.js +31 -0
- package/dist/extension.js +59 -7
- package/dist/host-transcript-patch.js +176 -58
- package/dist/index.d.ts +5 -3
- package/dist/index.js +5 -3
- package/dist/message-view.d.ts +4 -10
- package/dist/message-view.js +29 -16
- package/dist/native-tools.d.ts +69 -0
- package/dist/native-tools.js +414 -0
- package/dist/nested-calls-view.d.ts +6 -2
- package/dist/nested-calls-view.js +58 -73
- package/dist/payload-parser.d.ts +10 -28
- package/dist/payload-parser.js +9 -269
- package/dist/renderer.d.ts +6 -4
- package/dist/renderer.js +242 -181
- package/dist/runtime.d.ts +13 -0
- package/dist/runtime.js +16 -0
- package/dist/settings.d.ts +3 -0
- package/dist/settings.js +18 -1
- package/dist/tool-group-view.d.ts +52 -8
- package/dist/tool-group-view.js +351 -164
- package/dist/tool-view.d.ts +20 -6
- package/dist/tool-view.js +77 -30
- package/dist/tracker.d.ts +36 -17
- package/dist/tracker.js +157 -19
- package/package.json +2 -2
package/dist/renderer.js
CHANGED
|
@@ -1,43 +1,34 @@
|
|
|
1
|
-
import {
|
|
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 {
|
|
15
|
-
|
|
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
|
|
29
|
-
if (
|
|
20
|
+
const block = nativeRequestBlock(toolName, args);
|
|
21
|
+
if (block === undefined)
|
|
30
22
|
return undefined;
|
|
31
|
-
|
|
32
|
-
const clean =
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
80
|
-
return
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
175
|
-
|
|
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,
|
|
200
|
-
const heavyCmd =
|
|
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
|
-
? (
|
|
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 } =
|
|
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
|
|
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
|
-
|
|
242
|
-
|
|
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
|
-
|
|
289
|
+
facts.push(formatDurationMs(context.durationMs));
|
|
246
290
|
}
|
|
247
291
|
else if (wallTimeSeconds !== undefined && Number.isFinite(wallTimeSeconds)) {
|
|
248
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
|
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 (
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
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
|
-
|
|
501
|
-
|
|
502
|
-
.
|
|
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: {
|
|
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
|
-
? (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
}
|
|
746
|
-
|
|
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();
|