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