@hheei/pi-ext-ui 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/background.d.ts +18 -0
- package/dist/background.js +44 -0
- package/dist/extension.js +59 -7
- package/dist/host-transcript-patch.js +186 -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 +36 -18
- 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 +254 -189
- 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 +54 -8
- package/dist/tool-group-view.js +399 -169
- package/dist/tool-view.d.ts +20 -6
- package/dist/tool-view.js +87 -34
- 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,25 +243,34 @@ function shouldInterceptTool(toolName, options) {
|
|
|
196
243
|
return true;
|
|
197
244
|
}
|
|
198
245
|
function renderGenericResult(params) {
|
|
199
|
-
const { toolName, theme, context, result, renderOptions, defaultLevel,
|
|
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
|
-
? childCalls.length
|
|
216
|
-
? "
|
|
217
|
-
:
|
|
265
|
+
? childCalls.length > 0
|
|
266
|
+
? "none"
|
|
267
|
+
: isError
|
|
268
|
+
? "error"
|
|
269
|
+
: renderOptions.isPartial === true
|
|
270
|
+
? context.argsComplete
|
|
271
|
+
? "running"
|
|
272
|
+
: "streaming"
|
|
273
|
+
: "none"
|
|
218
274
|
: isMetaTool
|
|
219
275
|
? resolveContainerStatus({
|
|
220
276
|
isError,
|
|
@@ -227,42 +283,47 @@ function renderGenericResult(params) {
|
|
|
227
283
|
: renderOptions.isPartial
|
|
228
284
|
? "running"
|
|
229
285
|
: "ok";
|
|
230
|
-
const
|
|
231
|
-
const argSummary = resolveToolCallSummary({
|
|
232
|
-
toolName,
|
|
233
|
-
args: context.args,
|
|
234
|
-
originalRenderCall,
|
|
235
|
-
theme,
|
|
236
|
-
context,
|
|
237
|
-
});
|
|
238
|
-
if (argSummary !== "")
|
|
239
|
-
summaryParts.push(argSummary);
|
|
286
|
+
const facts = [];
|
|
240
287
|
if (isMetaTool && childCalls.length > 0) {
|
|
241
|
-
|
|
242
|
-
|
|
288
|
+
// Only a proven resolution is a call; the rest stay visible as unknown instead of counting.
|
|
289
|
+
const failCount = resolvedCalls.filter((c) => Boolean(c.isError || ("status" in c && c.status === "error"))).length;
|
|
290
|
+
facts.push(...callStatsParts(resolvedCalls.length, failCount));
|
|
291
|
+
if (unknownCallCount > 0)
|
|
292
|
+
facts.push(`${String(unknownCallCount)} unknown`);
|
|
243
293
|
}
|
|
244
294
|
if (context.durationMs !== undefined) {
|
|
245
|
-
|
|
295
|
+
facts.push(formatDurationMs(context.durationMs));
|
|
246
296
|
}
|
|
247
297
|
else if (wallTimeSeconds !== undefined && Number.isFinite(wallTimeSeconds)) {
|
|
248
|
-
|
|
298
|
+
facts.push(formatDurationMs(wallTimeSeconds * 1000));
|
|
249
299
|
}
|
|
250
300
|
const totalCost = childCalls.reduce((sum, c) => sum + (c.cost ?? 0), 0);
|
|
251
|
-
if (totalCost > 0)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
301
|
+
if (totalCost > 0)
|
|
302
|
+
facts.push(formatCostUsd(totalCost));
|
|
303
|
+
const timeout = nativeTimeoutFact(toolName, context.args);
|
|
304
|
+
if (timeout !== undefined)
|
|
305
|
+
facts.unshift(timeout);
|
|
306
|
+
const header = toolHeaderFacts({
|
|
307
|
+
toolName,
|
|
308
|
+
args: context.args,
|
|
309
|
+
theme,
|
|
310
|
+
cwd: context.cwd,
|
|
311
|
+
facts,
|
|
312
|
+
nativeArgsLine,
|
|
313
|
+
});
|
|
255
314
|
const codemodeNestedView = toolName === "codemode" && childCalls.length > 0
|
|
256
315
|
? new NestedCallsView({
|
|
257
316
|
calls: childCalls,
|
|
258
317
|
theme,
|
|
259
318
|
context,
|
|
319
|
+
headerSummary: (childName, childArgs, childTheme) => nativeHeaderSummary(childName, childArgs, childTheme, context.cwd)?.summary,
|
|
260
320
|
resolveChildRenderer: (childName) => {
|
|
261
321
|
return {
|
|
262
322
|
renderResult: (childResult, childOptions, childTheme, childCtx) => {
|
|
263
323
|
const provider = resolvePresentation?.(childName);
|
|
264
324
|
if (provider !== undefined) {
|
|
265
325
|
return renderOwnedPresentation({
|
|
326
|
+
runtime,
|
|
266
327
|
toolName: childName,
|
|
267
328
|
presentation: provider(childCtx.args, childResult, childTheme, childCtx),
|
|
268
329
|
result: childResult,
|
|
@@ -276,13 +337,14 @@ function renderGenericResult(params) {
|
|
|
276
337
|
}
|
|
277
338
|
const registered = resolveNativeRenderer?.(childName);
|
|
278
339
|
return renderGenericResult({
|
|
340
|
+
runtime,
|
|
279
341
|
toolName: childName,
|
|
280
342
|
theme: childTheme,
|
|
281
343
|
context: childCtx,
|
|
282
344
|
result: childResult,
|
|
283
345
|
renderOptions: childOptions,
|
|
284
346
|
defaultLevel: childOptions.expanded ? 2 : 1,
|
|
285
|
-
|
|
347
|
+
nativeArgsLine: registered?.renderCall !== undefined,
|
|
286
348
|
originalRenderResult: registered?.renderResult,
|
|
287
349
|
resolvePresentation,
|
|
288
350
|
resolveNativeRenderer,
|
|
@@ -301,7 +363,7 @@ function renderGenericResult(params) {
|
|
|
301
363
|
const collapsedRows = codemodeNestedView !== undefined
|
|
302
364
|
? codemodeNestedView
|
|
303
365
|
: isMetaTool && nestedCalls.length > 0
|
|
304
|
-
? nestedCalls.map((call) => formatNestedCallRow(call, theme))
|
|
366
|
+
? nestedCalls.map((call) => formatNestedCallRow(call, theme, context.cwd))
|
|
305
367
|
: undefined;
|
|
306
368
|
const toolRailToken = isError
|
|
307
369
|
? "error"
|
|
@@ -335,6 +397,7 @@ function renderGenericResult(params) {
|
|
|
335
397
|
}
|
|
336
398
|
else if (originalRenderResult !== undefined) {
|
|
337
399
|
const viewProbe = new ToolView({
|
|
400
|
+
background: () => runtime.backgrounds,
|
|
338
401
|
theme,
|
|
339
402
|
header: { title: toolName },
|
|
340
403
|
stateHolder: context.state,
|
|
@@ -349,18 +412,13 @@ function renderGenericResult(params) {
|
|
|
349
412
|
let lastUpstreamComponent;
|
|
350
413
|
const lazyUpstreamComponent = {
|
|
351
414
|
render(w) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
return comp.render(w);
|
|
360
|
-
}
|
|
361
|
-
catch {
|
|
362
|
-
return rawOutput !== "" ? rawOutput.split("\n") : [];
|
|
363
|
-
}
|
|
415
|
+
const upstreamContext = {
|
|
416
|
+
...context,
|
|
417
|
+
lastComponent: lastUpstreamComponent,
|
|
418
|
+
};
|
|
419
|
+
const comp = originalRenderResult(result, effectiveOptions, theme, upstreamContext);
|
|
420
|
+
lastUpstreamComponent = comp;
|
|
421
|
+
return comp.render(w);
|
|
364
422
|
},
|
|
365
423
|
invalidate() { },
|
|
366
424
|
};
|
|
@@ -384,7 +442,7 @@ function renderGenericResult(params) {
|
|
|
384
442
|
if (toolName === "codemode") {
|
|
385
443
|
copy = undefined;
|
|
386
444
|
}
|
|
387
|
-
else if (heavyCmd !== undefined
|
|
445
|
+
else if (heavyCmd !== undefined) {
|
|
388
446
|
const cleanOutput = stripTerminalSequences(rawOutput);
|
|
389
447
|
copy = {
|
|
390
448
|
primary: heavyCmd,
|
|
@@ -400,15 +458,15 @@ function renderGenericResult(params) {
|
|
|
400
458
|
};
|
|
401
459
|
}
|
|
402
460
|
return new ToolView({
|
|
461
|
+
background: () => runtime.backgrounds,
|
|
403
462
|
theme,
|
|
404
|
-
header: {
|
|
405
|
-
title: toolName,
|
|
406
|
-
status,
|
|
407
|
-
summary,
|
|
408
|
-
},
|
|
463
|
+
header: { title: toolName, status, ...header },
|
|
409
464
|
railToken: toolRailToken,
|
|
410
465
|
collapsedRows,
|
|
411
466
|
...(children.length === 0 ? {} : { children }),
|
|
467
|
+
// A container with child facts states its effective count, including zero: a container whose
|
|
468
|
+
// children all failed to resolve is not one call.
|
|
469
|
+
...(childCalls.length === 0 ? {} : { childCount: resolvedCalls.length }),
|
|
412
470
|
sections,
|
|
413
471
|
copy,
|
|
414
472
|
outputPad: context.outputPad,
|
|
@@ -424,7 +482,7 @@ function renderGenericResult(params) {
|
|
|
424
482
|
* pass never asks the tool to rebuild its rows.
|
|
425
483
|
*/
|
|
426
484
|
function renderOwnedPresentation(params) {
|
|
427
|
-
const { toolName, presentation, result, theme, context, defaultLevel, resolvePresentation, resolveNativeRenderer, options, } = params;
|
|
485
|
+
const { runtime, toolName, presentation, result, theme, context, defaultLevel, resolvePresentation, resolveNativeRenderer, options, } = params;
|
|
428
486
|
const isError = context.isError || result?.isError === true;
|
|
429
487
|
const pending = result === undefined || context.isPartial;
|
|
430
488
|
const isDiff = !isError && !pending && presentation.kind === "diff";
|
|
@@ -465,26 +523,16 @@ function renderOwnedPresentation(params) {
|
|
|
465
523
|
}
|
|
466
524
|
else if (output !== "") {
|
|
467
525
|
const rawLines = stripTerminalSequences(output).split("\n");
|
|
526
|
+
const lang = nativeOutputLanguage(toolName, context.args);
|
|
468
527
|
let styledLines;
|
|
469
528
|
if (isError) {
|
|
470
529
|
styledLines = rawLines.map((line) => theme.fg("error", line));
|
|
471
530
|
}
|
|
472
|
-
else if (
|
|
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
|
-
}
|
|
531
|
+
else if (lang !== undefined) {
|
|
532
|
+
try {
|
|
533
|
+
styledLines = highlightCode(output.replace(/\t/g, " "), lang);
|
|
486
534
|
}
|
|
487
|
-
|
|
535
|
+
catch {
|
|
488
536
|
styledLines = rawLines.map((line) => theme.fg("toolOutput", line));
|
|
489
537
|
}
|
|
490
538
|
}
|
|
@@ -497,22 +545,18 @@ function renderOwnedPresentation(params) {
|
|
|
497
545
|
});
|
|
498
546
|
}
|
|
499
547
|
const duration = result === undefined ? "" : formatDurationMs(context.durationMs);
|
|
500
|
-
|
|
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
|
-
};
|
|
548
|
+
// The provider owns the request/output boundary; cleaning its payload here corrupts raw input.
|
|
549
|
+
const copy = presentation.copy ??
|
|
550
|
+
(isDiff && presentation.diff.trim() !== "" ? { primary: presentation.diff } : undefined);
|
|
513
551
|
return new ToolView({
|
|
552
|
+
background: () => runtime.backgrounds,
|
|
514
553
|
theme,
|
|
515
|
-
header: {
|
|
554
|
+
header: {
|
|
555
|
+
title: toolName,
|
|
556
|
+
status,
|
|
557
|
+
summary: stripTerminalSequences(presentation.summary),
|
|
558
|
+
...(duration === "" ? {} : { meta: duration }),
|
|
559
|
+
},
|
|
516
560
|
railToken,
|
|
517
561
|
// The tool owns the folded rows and their tones; the frame only sanitizes them for the host.
|
|
518
562
|
collapsedRows: (() => {
|
|
@@ -520,23 +564,29 @@ function renderOwnedPresentation(params) {
|
|
|
520
564
|
return presentation.collapsedRows;
|
|
521
565
|
}
|
|
522
566
|
const captured = result !== undefined
|
|
523
|
-
? (
|
|
567
|
+
? (runtime.tracker.getCaptured(context.toolCallId) ??
|
|
524
568
|
(isRecord(result.details) && Array.isArray(result.details.__piExtUiCapturedCalls)
|
|
525
569
|
? result.details.__piExtUiCapturedCalls
|
|
526
570
|
: undefined))
|
|
527
571
|
: undefined;
|
|
528
572
|
const nestedCalls = extractNestedCalls(result);
|
|
529
|
-
|
|
573
|
+
// The owner's own child facts win over parsing another package's details.
|
|
574
|
+
const ownCalls = presentation.children === undefined
|
|
575
|
+
? nestedCalls
|
|
576
|
+
: presentationChildCalls(presentation.children);
|
|
577
|
+
const childCalls = withSessionArgs(captured ?? ownCalls, runtime);
|
|
530
578
|
if (childCalls.length > 0) {
|
|
531
579
|
return new NestedCallsView({
|
|
532
580
|
calls: childCalls,
|
|
533
581
|
theme,
|
|
534
582
|
context,
|
|
583
|
+
headerSummary: (childName, childArgs, childTheme) => nativeHeaderSummary(childName, childArgs, childTheme, context.cwd)?.summary,
|
|
535
584
|
resolveChildRenderer: (childName) => ({
|
|
536
585
|
renderResult: (childResult, childOptions, childTheme, childCtx) => {
|
|
537
586
|
const childProvider = resolvePresentation?.(childName);
|
|
538
587
|
if (childProvider !== undefined) {
|
|
539
588
|
return renderOwnedPresentation({
|
|
589
|
+
runtime,
|
|
540
590
|
toolName: childName,
|
|
541
591
|
presentation: childProvider(childCtx.args, childResult, childTheme, childCtx),
|
|
542
592
|
result: childResult,
|
|
@@ -550,13 +600,14 @@ function renderOwnedPresentation(params) {
|
|
|
550
600
|
}
|
|
551
601
|
const registered = resolveNativeRenderer?.(childName);
|
|
552
602
|
return renderGenericResult({
|
|
603
|
+
runtime,
|
|
553
604
|
toolName: childName,
|
|
554
605
|
theme: childTheme,
|
|
555
606
|
context: childCtx,
|
|
556
607
|
result: childResult,
|
|
557
608
|
renderOptions: childOptions,
|
|
558
609
|
defaultLevel: childOptions.expanded ? 2 : 1,
|
|
559
|
-
|
|
610
|
+
nativeArgsLine: registered?.renderCall !== undefined,
|
|
560
611
|
originalRenderResult: registered?.renderResult,
|
|
561
612
|
resolvePresentation,
|
|
562
613
|
resolveNativeRenderer,
|
|
@@ -589,10 +640,10 @@ function renderOwnedPresentation(params) {
|
|
|
589
640
|
copyToClipboard: options?.copyToClipboard,
|
|
590
641
|
});
|
|
591
642
|
}
|
|
592
|
-
export function createCollapsibleToolRendererResolver(options) {
|
|
593
|
-
return createRendererResolver(options);
|
|
643
|
+
export function createCollapsibleToolRendererResolver(runtime, options) {
|
|
644
|
+
return createRendererResolver(runtime, options);
|
|
594
645
|
}
|
|
595
|
-
function createRendererResolver(options, resolvePresentation, resolveNativeRenderer) {
|
|
646
|
+
function createRendererResolver(runtime, options, resolvePresentation, resolveNativeRenderer) {
|
|
596
647
|
const defaultLevel = options?.defaultCollapsed === false ? 1 : 0;
|
|
597
648
|
return (toolName, next) => {
|
|
598
649
|
const upstream = next();
|
|
@@ -603,7 +654,7 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
|
|
|
603
654
|
return upstream;
|
|
604
655
|
}
|
|
605
656
|
const registeredNative = resolveNativeRenderer?.(toolName);
|
|
606
|
-
const
|
|
657
|
+
const nativeArgsLine = upstream?.renderCall !== undefined || registeredNative?.renderCall !== undefined;
|
|
607
658
|
const originalRenderResult = upstream?.renderResult ?? registeredNative?.renderResult;
|
|
608
659
|
return {
|
|
609
660
|
renderShell: "self",
|
|
@@ -611,6 +662,7 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
|
|
|
611
662
|
if (provider !== undefined) {
|
|
612
663
|
const callContext = { ...context, args };
|
|
613
664
|
const view = renderOwnedPresentation({
|
|
665
|
+
runtime,
|
|
614
666
|
toolName,
|
|
615
667
|
presentation: provider(args, undefined, theme, callContext),
|
|
616
668
|
theme,
|
|
@@ -626,26 +678,14 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
|
|
|
626
678
|
const isStreamingArgs = context.isPartial && !context.argsComplete;
|
|
627
679
|
const status = context.isError
|
|
628
680
|
? "error"
|
|
629
|
-
:
|
|
630
|
-
? "
|
|
631
|
-
:
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
summary = tokens > 0 ? `· ${String(tokens)} tokens` : "";
|
|
638
|
-
}
|
|
639
|
-
else {
|
|
640
|
-
summary = resolveToolCallSummary({
|
|
641
|
-
toolName,
|
|
642
|
-
args,
|
|
643
|
-
originalRenderCall,
|
|
644
|
-
theme,
|
|
645
|
-
context,
|
|
646
|
-
});
|
|
647
|
-
}
|
|
648
|
-
const heavyCmd = extractHeavyInputCommand(toolName, args);
|
|
681
|
+
: isStreamingArgs
|
|
682
|
+
? "streaming"
|
|
683
|
+
: "running";
|
|
684
|
+
const streamedTokens = isStreamingArgs ? estimateStreamingTokens(args) : 0;
|
|
685
|
+
const header = isStreamingArgs
|
|
686
|
+
? { summary: streamedTokens > 0 ? `· ${String(streamedTokens)} tokens` : "" }
|
|
687
|
+
: toolHeaderFacts({ toolName, args, theme, cwd: context.cwd, facts: [], nativeArgsLine });
|
|
688
|
+
const heavyCmd = nativeHeavyRequest(toolName, args);
|
|
649
689
|
const sections = [];
|
|
650
690
|
const inputSection = createInputCodeSection(toolName, args, theme);
|
|
651
691
|
if (inputSection !== undefined) {
|
|
@@ -660,18 +700,15 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
|
|
|
660
700
|
});
|
|
661
701
|
}
|
|
662
702
|
}
|
|
663
|
-
const copy = heavyCmd !== undefined
|
|
703
|
+
const copy = heavyCmd !== undefined
|
|
664
704
|
? { primary: heavyCmd }
|
|
665
705
|
: formatArgsPreview(args) !== ""
|
|
666
706
|
? { primary: formatArgsPreview(args) }
|
|
667
707
|
: undefined;
|
|
668
708
|
const view = new ToolView({
|
|
709
|
+
background: () => runtime.backgrounds,
|
|
669
710
|
theme,
|
|
670
|
-
header: {
|
|
671
|
-
title: toolName,
|
|
672
|
-
status,
|
|
673
|
-
summary,
|
|
674
|
-
},
|
|
711
|
+
header: { title: toolName, status, ...header },
|
|
675
712
|
sections,
|
|
676
713
|
copy,
|
|
677
714
|
outputPad: context.outputPad,
|
|
@@ -698,6 +735,7 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
|
|
|
698
735
|
hideCallView(context);
|
|
699
736
|
if (presentation !== undefined) {
|
|
700
737
|
return renderOwnedPresentation({
|
|
738
|
+
runtime,
|
|
701
739
|
toolName,
|
|
702
740
|
presentation,
|
|
703
741
|
result,
|
|
@@ -709,13 +747,14 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
|
|
|
709
747
|
});
|
|
710
748
|
}
|
|
711
749
|
return renderGenericResult({
|
|
750
|
+
runtime,
|
|
712
751
|
toolName,
|
|
713
752
|
theme,
|
|
714
753
|
context,
|
|
715
754
|
result,
|
|
716
755
|
renderOptions,
|
|
717
756
|
defaultLevel,
|
|
718
|
-
|
|
757
|
+
nativeArgsLine,
|
|
719
758
|
originalRenderResult,
|
|
720
759
|
resolvePresentation,
|
|
721
760
|
resolveNativeRenderer,
|
|
@@ -726,24 +765,50 @@ function createRendererResolver(options, resolvePresentation, resolveNativeRende
|
|
|
726
765
|
};
|
|
727
766
|
}
|
|
728
767
|
/** UI factory consumed by package-owned direct renderers, independently of host resolver precedence. */
|
|
729
|
-
export function createOptionalToolRendererFactory(pi, options) {
|
|
730
|
-
return (name, native) => createRendererResolver({ ...options, tools: [name] }, (toolName) => getRegisteredToolPresentationProvider(pi, toolName), (toolName) => getRegisteredToolRenderers(pi, toolName))(name, () => native) ?? native;
|
|
768
|
+
export function createOptionalToolRendererFactory(pi, runtime, options) {
|
|
769
|
+
return (name, native) => createRendererResolver(runtime, { ...options, tools: [name] }, (toolName) => getRegisteredToolPresentationProvider(pi, toolName), (toolName) => getRegisteredToolRenderers(pi, toolName))(name, () => native) ?? native;
|
|
731
770
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
}
|
|
746
|
-
|
|
771
|
+
/**
|
|
772
|
+
* Save the resolution facts of one model-issued call on its own tool result. The host emits
|
|
773
|
+
* `tool_call` only after the tool resolved and its arguments validated, so these ids are proof;
|
|
774
|
+
* writing them into the details keeps the fact with the call through resume, fork and reload.
|
|
775
|
+
*/
|
|
776
|
+
function withResolvedCallFacts(event, runtime) {
|
|
777
|
+
if (event.parentToolCallId !== undefined)
|
|
778
|
+
return undefined;
|
|
779
|
+
const ids = runtime.tracker.getResolved(event.toolCallId);
|
|
780
|
+
if (ids === undefined || ids.size === 0)
|
|
781
|
+
return undefined;
|
|
782
|
+
if (!isRecord(event.details))
|
|
783
|
+
return undefined;
|
|
784
|
+
return { ...event.details, [RESOLVED_CALLS_KEY]: [...ids] };
|
|
785
|
+
}
|
|
786
|
+
export function registerCollapsibleToolRenderer(pi, runtime, options) {
|
|
787
|
+
const disposers = [
|
|
788
|
+
pi.on("tool_execution_start", (event) => {
|
|
789
|
+
runtime.tracker.recordStart(event);
|
|
790
|
+
}),
|
|
791
|
+
pi.on("tool_execution_end", (event) => {
|
|
792
|
+
runtime.tracker.recordEnd(event);
|
|
793
|
+
}),
|
|
794
|
+
pi.on("tool_call", (event) => {
|
|
795
|
+
runtime.tracker.recordResolved(event);
|
|
796
|
+
}),
|
|
797
|
+
pi.on("tool_result", (event) => {
|
|
798
|
+
const details = withResolvedCallFacts(event, runtime);
|
|
799
|
+
if (details === undefined)
|
|
800
|
+
return undefined;
|
|
801
|
+
// The hook replaces the whole result, so the fields it does not own pass through.
|
|
802
|
+
return {
|
|
803
|
+
details,
|
|
804
|
+
...(event.structuredContent === undefined
|
|
805
|
+
? {}
|
|
806
|
+
: { structuredContent: event.structuredContent }),
|
|
807
|
+
...(event.usage === undefined ? {} : { usage: event.usage }),
|
|
808
|
+
};
|
|
809
|
+
}),
|
|
810
|
+
];
|
|
811
|
+
pi.registerToolRenderer(createRendererResolver(runtime, options, (name) => getRegisteredToolPresentationProvider(pi, name), (name) => getRegisteredToolRenderers(pi, name)));
|
|
747
812
|
return () => {
|
|
748
813
|
for (const dispose of disposers)
|
|
749
814
|
dispose();
|