@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { copyToClipboard as copyToSystemClipboard, getLanguageFromPath, highlightCode, } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { stripTerminalSequences, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
|
|
3
|
-
import { inlinePresentation } from "./
|
|
3
|
+
import { extractChildToolCopyText, inlinePresentation, nativeRequestCopy } from "./native-tools.js";
|
|
4
4
|
import { formatToolHeaderText, ToolView, toContentRows, transitionFoldLevel, } from "./tool-view.js";
|
|
5
5
|
function truncate(text, width) {
|
|
6
6
|
return truncateToWidth(text, Math.max(1, width));
|
|
@@ -8,53 +8,15 @@ function truncate(text, width) {
|
|
|
8
8
|
function isRecord(value) {
|
|
9
9
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
10
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
/**
|
|
12
|
+
* A copy keeps the request half byte-exact and strips only what follows it: the result is display
|
|
13
|
+
* text, while the request is what the model sent and may legitimately contain escapes.
|
|
14
|
+
*/
|
|
15
|
+
function copyText(text, request) {
|
|
16
|
+
if (request === undefined || request === "" || !text.startsWith(request)) {
|
|
17
|
+
return stripTerminalSequences(text);
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
-
? args.command
|
|
20
|
-
: toolName === "codemode" && isRecord(args) && typeof args.code === "string"
|
|
21
|
-
? args.code
|
|
22
|
-
: undefined;
|
|
23
|
-
if (heavyCommand !== undefined && heavyCommand.trim() !== "") {
|
|
24
|
-
if (full !== true)
|
|
25
|
-
return heavyCommand;
|
|
26
|
-
const output = resultText(result);
|
|
27
|
-
return output === undefined ? heavyCommand : `${heavyCommand}\n---\n${output}`;
|
|
28
|
-
}
|
|
29
|
-
if (result && Array.isArray(result.content)) {
|
|
30
|
-
const texts = result.content
|
|
31
|
-
.filter((c) => isRecord(c) && c.type === "text" && typeof c.text === "string")
|
|
32
|
-
.map((c) => c.text);
|
|
33
|
-
if (texts.length > 0)
|
|
34
|
-
return texts.join("\n");
|
|
35
|
-
}
|
|
36
|
-
if (args !== undefined) {
|
|
37
|
-
if (typeof args === "string")
|
|
38
|
-
return args;
|
|
39
|
-
try {
|
|
40
|
-
return JSON.stringify(args, null, 2);
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
return String(args);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
if ("summary" in call && typeof call.summary === "string") {
|
|
47
|
-
return call.summary;
|
|
48
|
-
}
|
|
49
|
-
return "";
|
|
50
|
-
}
|
|
51
|
-
function resultText(result) {
|
|
52
|
-
if (result === undefined || !Array.isArray(result.content))
|
|
53
|
-
return undefined;
|
|
54
|
-
const texts = result.content
|
|
55
|
-
.filter((c) => isRecord(c) && c.type === "text" && typeof c.text === "string")
|
|
56
|
-
.map((c) => c.text);
|
|
57
|
-
return texts.length === 0 ? undefined : texts.join("\n");
|
|
19
|
+
return request + stripTerminalSequences(text.slice(request.length));
|
|
58
20
|
}
|
|
59
21
|
function formatDuration(ms) {
|
|
60
22
|
if (ms === undefined || !Number.isFinite(ms))
|
|
@@ -162,7 +124,7 @@ export class NestedCallsView {
|
|
|
162
124
|
const { context } = this.#props;
|
|
163
125
|
// A captured call carries its own id. A persisted record has none, so it falls back to the
|
|
164
126
|
// parent's id — the only identity the host offers, and never a child's guess.
|
|
165
|
-
const toolCallId = "id" in call ? call.id : context.toolCallId;
|
|
127
|
+
const toolCallId = ("id" in call ? call.id : undefined) ?? context.toolCallId;
|
|
166
128
|
return {
|
|
167
129
|
...context,
|
|
168
130
|
lastComponent: undefined,
|
|
@@ -180,12 +142,15 @@ export class NestedCallsView {
|
|
|
180
142
|
return;
|
|
181
143
|
const provider = this.#props.resolvePresentation?.(call.toolName);
|
|
182
144
|
const presentation = provider?.("args" in call ? call.args : undefined, "result" in call ? call.result : undefined, this.#props.theme, this.#callContext(call));
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
145
|
+
const chosen = full ? presentation?.copy?.full : presentation?.copy?.primary;
|
|
146
|
+
const text = chosen ?? copyText(extractChildToolCopyText(call, full), nativeRequestCopy(call));
|
|
147
|
+
const durationMs = 1500;
|
|
148
|
+
if (text === "") {
|
|
149
|
+
// Nothing was recorded to copy. A summary is not the request, so the copy reports failure.
|
|
150
|
+
this.#showCopyFeedback(callIndex, durationMs, true);
|
|
186
151
|
return;
|
|
152
|
+
}
|
|
187
153
|
const writeClip = this.#props.copyToClipboard ?? copyToSystemClipboard;
|
|
188
|
-
const durationMs = 1500;
|
|
189
154
|
// Feedback starts when the write settles: a rejected clipboard write is not a copy.
|
|
190
155
|
void writeClip(text).then(() => this.#showCopyFeedback(callIndex, durationMs, false), () => this.#showCopyFeedback(callIndex, durationMs, true));
|
|
191
156
|
}
|
|
@@ -253,18 +218,25 @@ export class NestedCallsView {
|
|
|
253
218
|
const isExpanded = expansionLevel !== 0;
|
|
254
219
|
const rawSummary = "summary" in call ? call.summary : undefined;
|
|
255
220
|
const rawArgs = "args" in call ? call.args : undefined;
|
|
256
|
-
|
|
221
|
+
// A record's summary is the fact its owner stated; the arguments only fill in for a capture.
|
|
222
|
+
const inline = inlinePresentation(toolName, rawSummary ?? rawArgs, "result" in call ? call.result?.details : undefined, isError);
|
|
257
223
|
const argText = typeof rawArgs === "string" ? rawArgs : typeof rawSummary === "string" ? rawSummary : "";
|
|
258
224
|
const provider = this.#props.resolvePresentation?.(toolName);
|
|
259
225
|
const presentation = provider?.(rawArgs, "result" in call ? call.result : undefined, theme, this.#callContext(call));
|
|
260
226
|
// The child row is the child's own header minus the fold pin, so it carries the same token
|
|
261
227
|
// assignment as the row the same call would draw at top level.
|
|
262
|
-
const
|
|
263
|
-
|
|
228
|
+
const ownSummary = presentation?.summary === undefined
|
|
229
|
+
? this.#props.headerSummary?.(toolName, rawArgs ?? rawSummary, theme)
|
|
230
|
+
: undefined;
|
|
231
|
+
const childSummary = ownSummary === undefined || ownSummary === ""
|
|
232
|
+
? stripTerminalSequences(presentation?.summary ?? inline?.summary ?? argText)
|
|
233
|
+
: ownSummary;
|
|
264
234
|
const leftContent = formatToolHeaderText({
|
|
265
235
|
status: isRunning ? "running" : isError ? "error" : "ok",
|
|
266
236
|
title: toolName,
|
|
267
|
-
summary:
|
|
237
|
+
summary: childSummary,
|
|
238
|
+
...(ownSummary === undefined || ownSummary === "" ? {} : { summaryStyled: true }),
|
|
239
|
+
meta: formatDuration(call.durationMs),
|
|
268
240
|
}, theme);
|
|
269
241
|
const isCopied = this.#copiedIndex === i;
|
|
270
242
|
const copyIcon = this.#failedIndex === i
|
|
@@ -322,22 +294,17 @@ export class NestedCallsView {
|
|
|
322
294
|
if (result && resolveChildRenderer) {
|
|
323
295
|
const renderer = resolveChildRenderer(toolName);
|
|
324
296
|
if (renderer?.renderResult) {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
return comp.renderBody(width, { noRail: true });
|
|
335
|
-
}
|
|
336
|
-
return comp.render(width);
|
|
337
|
-
}
|
|
338
|
-
catch {
|
|
339
|
-
// Fall through to plain text extraction on renderer failure
|
|
297
|
+
const childContext = {
|
|
298
|
+
...this.#callContext(call),
|
|
299
|
+
state: undefined,
|
|
300
|
+
};
|
|
301
|
+
const comp = renderer.renderResult(result, { expanded: expansionLevel === 2, isPartial: false }, theme, childContext);
|
|
302
|
+
if (comp instanceof ToolView) {
|
|
303
|
+
comp.setFoldLevel(expansionLevel);
|
|
304
|
+
// Render body cleanly via deep headless interface without outer rail or header row
|
|
305
|
+
return comp.renderBody(width, { noRail: true });
|
|
340
306
|
}
|
|
307
|
+
return comp.render(width);
|
|
341
308
|
}
|
|
342
309
|
}
|
|
343
310
|
// Fallback rendering
|
|
@@ -381,11 +348,29 @@ export class NestedCallsView {
|
|
|
381
348
|
if ("error" in call && typeof call.error === "string" && call.error) {
|
|
382
349
|
return call.error.split("\n").map((l) => theme.fg("error", l));
|
|
383
350
|
}
|
|
351
|
+
if ("argsTruncated" in call && call.argsTruncated === true) {
|
|
352
|
+
const bytes = "argsBytes" in call && typeof call.argsBytes === "number" ? call.argsBytes : 0;
|
|
353
|
+
return [
|
|
354
|
+
theme.fg("dim", bytes > 0 ? `(request not recorded: ${String(bytes)} bytes)` : "(request not recorded)"),
|
|
355
|
+
];
|
|
356
|
+
}
|
|
357
|
+
// The request as the model sent it: the command or source, not a JSON wrapper around it.
|
|
358
|
+
const request = nativeRequestCopy(call);
|
|
359
|
+
if (request !== undefined) {
|
|
360
|
+
return stripTerminalSequences(request)
|
|
361
|
+
.split("\n")
|
|
362
|
+
.map((line) => theme.fg("dim", line));
|
|
363
|
+
}
|
|
384
364
|
if (args !== undefined) {
|
|
385
|
-
const text = typeof args === "string" ? args : JSON.stringify(args, null, 2);
|
|
365
|
+
const text = typeof args === "string" ? stripTerminalSequences(args) : JSON.stringify(args, null, 2);
|
|
386
366
|
return text.split("\n").map((l) => theme.fg("dim", l));
|
|
387
367
|
}
|
|
388
|
-
|
|
368
|
+
// A persisted record keeps a summary, not its request: say so instead of implying an empty call.
|
|
369
|
+
return [
|
|
370
|
+
theme.fg("dim", "summary" in call && typeof call.summary === "string" && call.summary !== ""
|
|
371
|
+
? "(request not recorded; summary only)"
|
|
372
|
+
: "(no output)"),
|
|
373
|
+
];
|
|
389
374
|
}
|
|
390
375
|
handleMouse(event) {
|
|
391
376
|
if (event.type !== "click" || event.button !== "left")
|
package/dist/payload-parser.d.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { type DiffSource } from "./diff-view.js";
|
|
3
2
|
import type { ToolStatusKind } from "./tool-view.js";
|
|
4
|
-
export declare const
|
|
5
|
-
export declare const WRITE_TOOL_NAME = "write";
|
|
6
|
-
export declare const CODEMODE_OPTIONS_PREFIX = "// @options:";
|
|
7
|
-
export declare const CODEMODE_SCRIPT_HEADER: RegExp;
|
|
3
|
+
export declare const RESOLVED_CALLS_KEY = "__piExtUiResolvedCalls";
|
|
8
4
|
export interface NestedCallRecord {
|
|
9
5
|
readonly toolName: string;
|
|
6
|
+
/** The host's call id (`<parent>/<n>`); the key its saved arguments are indexed under. */
|
|
7
|
+
readonly id?: string | undefined;
|
|
8
|
+
/** The request bytes when this process or the session branch still has them. */
|
|
9
|
+
readonly args?: unknown;
|
|
10
|
+
/** The session saved the size of the request but not the request itself. */
|
|
11
|
+
readonly argsTruncated?: boolean | undefined;
|
|
12
|
+
readonly argsBytes?: number | undefined;
|
|
13
|
+
/** Positive fact only: the host proved this call resolved to a real tool. */
|
|
14
|
+
readonly resolved?: boolean | undefined;
|
|
10
15
|
readonly summary?: string | undefined;
|
|
11
16
|
readonly status?: "running" | "ok" | "error" | "cancelled" | undefined;
|
|
12
17
|
readonly isError?: boolean | undefined;
|
|
@@ -21,15 +26,8 @@ export declare function formatDurationMs(ms: number | undefined): string;
|
|
|
21
26
|
export declare function formatCostUsd(cost: number | undefined): string;
|
|
22
27
|
/** One header fact for a deadline, in the same shape codemode's `timeout_ms` renders as. */
|
|
23
28
|
export declare function formatTimeoutFact(timeoutMs: number): string;
|
|
24
|
-
export declare function extractCodemodeHeaderFacts(args: unknown): string;
|
|
25
29
|
export declare function parseCallArgs(raw: unknown): Record<string, unknown> | undefined;
|
|
26
|
-
export declare function extractSummaryArg(toolName: string, args: unknown): string;
|
|
27
|
-
export declare function extractHeavyInputCommand(toolName: string, args: unknown): string | undefined;
|
|
28
30
|
export declare function extractNestedCalls(result: AgentToolResult<unknown> | undefined): readonly NestedCallRecord[];
|
|
29
|
-
export declare function extractResultText(toolName: string, result: AgentToolResult<unknown>): {
|
|
30
|
-
readonly outputText: string;
|
|
31
|
-
readonly wallTimeSeconds: number | undefined;
|
|
32
|
-
};
|
|
33
31
|
export declare function callStatsParts(total: number, failCount: number): string[];
|
|
34
32
|
export declare function resolveContainerStatus(params: {
|
|
35
33
|
readonly isError: boolean;
|
|
@@ -37,19 +35,3 @@ export declare function resolveContainerStatus(params: {
|
|
|
37
35
|
readonly hasChildren: boolean;
|
|
38
36
|
readonly selfFailed: boolean;
|
|
39
37
|
}): ToolStatusKind;
|
|
40
|
-
export declare function toolPath(args: unknown): string;
|
|
41
|
-
/** Compact nested-row result: a one-line summary plus the status it reports. */
|
|
42
|
-
export interface InlinePresentation {
|
|
43
|
-
readonly summary: string;
|
|
44
|
-
readonly status: ToolStatusKind;
|
|
45
|
-
}
|
|
46
|
-
export declare function fileToolPresentation(toolName: string, args: unknown, _details: unknown, isError: boolean): InlinePresentation | undefined;
|
|
47
|
-
/** Pi native edit persists a unified patch plus the same change as a numbered display diff. */
|
|
48
|
-
export declare function editDiffSource(details: unknown): DiffSource | undefined;
|
|
49
|
-
export declare function displayDiffSource(diff: string): DiffSource | undefined;
|
|
50
|
-
/**
|
|
51
|
-
* One compact nested summary for a tool call. Adopts the tool's standard collapsed
|
|
52
|
-
* single-line format when recognized, degrading to undefined so callers can use
|
|
53
|
-
* the default argument rendering.
|
|
54
|
-
*/
|
|
55
|
-
export declare function inlinePresentation(toolName: string, args: unknown, details: unknown, isError: boolean): InlinePresentation | undefined;
|
package/dist/payload-parser.js
CHANGED
|
@@ -1,18 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { diffStats } from "./diff-view.js";
|
|
3
|
-
export const EDIT_TOOL_NAME = "edit";
|
|
4
|
-
export const WRITE_TOOL_NAME = "write";
|
|
5
|
-
export const CODEMODE_OPTIONS_PREFIX = "// @options:";
|
|
6
|
-
export const CODEMODE_SCRIPT_HEADER = /^Script (?:completed|failed)\r?\nWall time ([\d.]+) seconds\r?\nOutput:\r?\n/;
|
|
7
|
-
// Pi <1.3 has no exported read-range formatter. Keep this UI summary independent of that export.
|
|
8
|
-
function readLineNumber(value) {
|
|
9
|
-
const parsed = typeof value === "number"
|
|
10
|
-
? value
|
|
11
|
-
: typeof value === "string" && value.trim() !== ""
|
|
12
|
-
? Number(value)
|
|
13
|
-
: Number.NaN;
|
|
14
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
15
|
-
}
|
|
1
|
+
export const RESOLVED_CALLS_KEY = "__piExtUiResolvedCalls";
|
|
16
2
|
export function isRecord(value) {
|
|
17
3
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
4
|
}
|
|
@@ -44,39 +30,6 @@ export function formatTimeoutFact(timeoutMs) {
|
|
|
44
30
|
const label = timeoutMs >= 1000 ? `${String(Math.round(timeoutMs / 1000))}s` : `${String(timeoutMs)}ms`;
|
|
45
31
|
return `(timeout ${label})`;
|
|
46
32
|
}
|
|
47
|
-
export function extractCodemodeHeaderFacts(args) {
|
|
48
|
-
if (!isRecord(args))
|
|
49
|
-
return "";
|
|
50
|
-
const options = {};
|
|
51
|
-
for (const [key, value] of Object.entries(args)) {
|
|
52
|
-
if (key !== "code" && value !== undefined && value !== null) {
|
|
53
|
-
options[key] = value;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if (typeof args.code === "string") {
|
|
57
|
-
const firstLine = args.code.split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
58
|
-
if (firstLine.startsWith(CODEMODE_OPTIONS_PREFIX)) {
|
|
59
|
-
try {
|
|
60
|
-
const parsed = JSON.parse(firstLine.slice(CODEMODE_OPTIONS_PREFIX.length).trim());
|
|
61
|
-
if (isRecord(parsed)) {
|
|
62
|
-
Object.assign(options, parsed);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
catch { }
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
const facts = [];
|
|
69
|
-
const timeoutMs = typeof options.timeout_ms === "number" ? options.timeout_ms : undefined;
|
|
70
|
-
if (timeoutMs !== undefined)
|
|
71
|
-
facts.push(formatTimeoutFact(timeoutMs));
|
|
72
|
-
if (typeof options.max_output_tokens === "number") {
|
|
73
|
-
facts.push(`(tokens ${String(options.max_output_tokens)})`);
|
|
74
|
-
}
|
|
75
|
-
if (typeof options.target === "string" && options.target !== "local") {
|
|
76
|
-
facts.push(`(${options.target})`);
|
|
77
|
-
}
|
|
78
|
-
return facts.join(" ");
|
|
79
|
-
}
|
|
80
33
|
export function parseCallArgs(raw) {
|
|
81
34
|
if (isRecord(raw))
|
|
82
35
|
return raw;
|
|
@@ -126,77 +79,6 @@ export function parseCallArgs(raw) {
|
|
|
126
79
|
}
|
|
127
80
|
return undefined;
|
|
128
81
|
}
|
|
129
|
-
export function extractSummaryArg(toolName, args) {
|
|
130
|
-
if (toolName === "codemode") {
|
|
131
|
-
return extractCodemodeHeaderFacts(args);
|
|
132
|
-
}
|
|
133
|
-
const parsed = parseCallArgs(args);
|
|
134
|
-
if (!parsed)
|
|
135
|
-
return "";
|
|
136
|
-
const firstLine = (val) => {
|
|
137
|
-
if (typeof val !== "string" || val.trim() === "")
|
|
138
|
-
return "";
|
|
139
|
-
const clean = stripTerminalSequences(val.trim());
|
|
140
|
-
return clean.split("\n")[0]?.trim() ?? clean;
|
|
141
|
-
};
|
|
142
|
-
switch (toolName) {
|
|
143
|
-
case "bash":
|
|
144
|
-
case "bg_bash_spawn":
|
|
145
|
-
return firstLine(parsed.command);
|
|
146
|
-
case "read":
|
|
147
|
-
case "edit":
|
|
148
|
-
case "write": {
|
|
149
|
-
const path = typeof parsed.path === "string"
|
|
150
|
-
? parsed.path.trim()
|
|
151
|
-
: typeof parsed.file_path === "string"
|
|
152
|
-
? parsed.file_path.trim()
|
|
153
|
-
: "";
|
|
154
|
-
return stripTerminalSequences(path);
|
|
155
|
-
}
|
|
156
|
-
case "grep":
|
|
157
|
-
case "find": {
|
|
158
|
-
const pattern = typeof parsed.pattern === "string" ? parsed.pattern.trim() : "";
|
|
159
|
-
const path = typeof parsed.path === "string" ? parsed.path.trim() : "";
|
|
160
|
-
if (!pattern)
|
|
161
|
-
return path ? stripTerminalSequences(path) : "";
|
|
162
|
-
const target = path && path !== "." ? ` ${path}` : "";
|
|
163
|
-
return `${pattern}${target}`;
|
|
164
|
-
}
|
|
165
|
-
case "python_eval":
|
|
166
|
-
return firstLine(parsed.code);
|
|
167
|
-
case "bg_bash_get":
|
|
168
|
-
case "bg_bash_stop":
|
|
169
|
-
case "agents_get":
|
|
170
|
-
case "agents_stop":
|
|
171
|
-
return typeof parsed.id === "string" ? parsed.id.trim() : "";
|
|
172
|
-
case "agents_delegate": {
|
|
173
|
-
const agent = typeof parsed.agent === "string" ? parsed.agent.trim() : "";
|
|
174
|
-
const id = typeof parsed.id === "string" ? parsed.id.trim() : "";
|
|
175
|
-
const task = firstLine(parsed.task);
|
|
176
|
-
if (agent && task)
|
|
177
|
-
return `${agent}: ${task}`;
|
|
178
|
-
return agent || id || task;
|
|
179
|
-
}
|
|
180
|
-
case "tool_search":
|
|
181
|
-
return typeof parsed.query === "string" ? parsed.query.trim() : "";
|
|
182
|
-
default:
|
|
183
|
-
return "";
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
export function extractHeavyInputCommand(toolName, args) {
|
|
187
|
-
if (!isRecord(args))
|
|
188
|
-
return undefined;
|
|
189
|
-
if (toolName === "bash" && typeof args.command === "string") {
|
|
190
|
-
return args.command.trim();
|
|
191
|
-
}
|
|
192
|
-
if (toolName === "python_eval" && typeof args.code === "string") {
|
|
193
|
-
return args.code.trim();
|
|
194
|
-
}
|
|
195
|
-
if (toolName === "codemode" && typeof args.code === "string") {
|
|
196
|
-
return args.code.trim();
|
|
197
|
-
}
|
|
198
|
-
return undefined;
|
|
199
|
-
}
|
|
200
82
|
export function extractNestedCalls(result) {
|
|
201
83
|
if (result === undefined || !isRecord(result.details))
|
|
202
84
|
return [];
|
|
@@ -207,6 +89,10 @@ export function extractNestedCalls(result) {
|
|
|
207
89
|
: undefined;
|
|
208
90
|
if (rawList === undefined)
|
|
209
91
|
return [];
|
|
92
|
+
const rawResolved = result.details[RESOLVED_CALLS_KEY];
|
|
93
|
+
const resolvedIds = Array.isArray(rawResolved)
|
|
94
|
+
? new Set(rawResolved.filter((id) => typeof id === "string"))
|
|
95
|
+
: undefined;
|
|
210
96
|
const items = [];
|
|
211
97
|
for (const entry of rawList) {
|
|
212
98
|
if (!isRecord(entry))
|
|
@@ -230,8 +116,12 @@ export function extractNestedCalls(result) {
|
|
|
230
116
|
: typeof entry.summary === "string"
|
|
231
117
|
? entry.summary
|
|
232
118
|
: undefined;
|
|
119
|
+
const id = typeof entry.id === "string" ? entry.id : undefined;
|
|
233
120
|
items.push({
|
|
234
121
|
toolName: name,
|
|
122
|
+
...(id === undefined ? {} : { id }),
|
|
123
|
+
// Absent facts stay absent: a legacy record and an unresolved call are both unknown.
|
|
124
|
+
...(id !== undefined && resolvedIds?.has(id) === true ? { resolved: true } : {}),
|
|
235
125
|
summary,
|
|
236
126
|
status,
|
|
237
127
|
isError: entry.isError === true || status === "error",
|
|
@@ -243,23 +133,6 @@ export function extractNestedCalls(result) {
|
|
|
243
133
|
}
|
|
244
134
|
return items;
|
|
245
135
|
}
|
|
246
|
-
export function extractResultText(toolName, result) {
|
|
247
|
-
const parts = [];
|
|
248
|
-
for (const block of result.content) {
|
|
249
|
-
if (block.type === "text" && block.text.trim() !== "") {
|
|
250
|
-
parts.push(block.text);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
const joined = parts.join("\n");
|
|
254
|
-
if (toolName === "codemode") {
|
|
255
|
-
const stripped = stripTerminalSequences(joined);
|
|
256
|
-
const match = CODEMODE_SCRIPT_HEADER.exec(stripped);
|
|
257
|
-
const wallTimeSeconds = match?.[1] !== undefined ? Number.parseFloat(match[1]) : undefined;
|
|
258
|
-
const cleaned = joined.replace(CODEMODE_SCRIPT_HEADER, "").trim();
|
|
259
|
-
return { outputText: cleaned, wallTimeSeconds };
|
|
260
|
-
}
|
|
261
|
-
return { outputText: joined, wallTimeSeconds: undefined };
|
|
262
|
-
}
|
|
263
136
|
export function callStatsParts(total, failCount) {
|
|
264
137
|
if (failCount > 0) {
|
|
265
138
|
return [`${String(total - failCount)} ok`, `${String(failCount)} failed`];
|
|
@@ -275,136 +148,3 @@ export function resolveContainerStatus(params) {
|
|
|
275
148
|
return "running";
|
|
276
149
|
return params.isError ? "error" : "ok";
|
|
277
150
|
}
|
|
278
|
-
export function toolPath(args) {
|
|
279
|
-
const parsed = parseCallArgs(args);
|
|
280
|
-
if (!parsed)
|
|
281
|
-
return "";
|
|
282
|
-
const val = parsed.path ?? parsed.file_path;
|
|
283
|
-
return typeof val === "string" ? val.trim() : "";
|
|
284
|
-
}
|
|
285
|
-
export function fileToolPresentation(toolName, args, _details, isError) {
|
|
286
|
-
const path = toolPath(args);
|
|
287
|
-
if (toolName === "read") {
|
|
288
|
-
if (path === "")
|
|
289
|
-
return undefined;
|
|
290
|
-
const parsed = parseCallArgs(args);
|
|
291
|
-
const offset = parsed?.offset;
|
|
292
|
-
const limit = parsed?.limit;
|
|
293
|
-
const start = offset == null ? 1 : readLineNumber(offset);
|
|
294
|
-
const count = limit == null ? undefined : readLineNumber(limit);
|
|
295
|
-
const end = start !== undefined && count !== undefined ? start + count - 1 : undefined;
|
|
296
|
-
const range = (offset == null && limit == null) ||
|
|
297
|
-
start === undefined ||
|
|
298
|
-
(limit != null && count === undefined)
|
|
299
|
-
? ""
|
|
300
|
-
: `:${start}${end ? `-${end}` : ""}`;
|
|
301
|
-
return { summary: `${path}${range}`, status: isError ? "error" : "ok" };
|
|
302
|
-
}
|
|
303
|
-
if (toolName === WRITE_TOOL_NAME) {
|
|
304
|
-
return path === "" ? undefined : { summary: path, status: isError ? "error" : "ok" };
|
|
305
|
-
}
|
|
306
|
-
return undefined;
|
|
307
|
-
}
|
|
308
|
-
/** Pi native edit persists a unified patch plus the same change as a numbered display diff. */
|
|
309
|
-
export function editDiffSource(details) {
|
|
310
|
-
if (!isRecord(details))
|
|
311
|
-
return undefined;
|
|
312
|
-
if (typeof details.patch === "string" && details.patch.trim() !== "") {
|
|
313
|
-
return { kind: "unifiedPatch", text: details.patch };
|
|
314
|
-
}
|
|
315
|
-
if (typeof details.diff === "string" && details.diff.trim() !== "") {
|
|
316
|
-
return displayDiffSource(details.diff);
|
|
317
|
-
}
|
|
318
|
-
return undefined;
|
|
319
|
-
}
|
|
320
|
-
export function displayDiffSource(diff) {
|
|
321
|
-
const hunks = [];
|
|
322
|
-
let current = [];
|
|
323
|
-
let pendingBreak = false;
|
|
324
|
-
for (const raw of diff.split(/\r?\n/)) {
|
|
325
|
-
if (raw === "")
|
|
326
|
-
continue;
|
|
327
|
-
const sign = raw.charAt(0);
|
|
328
|
-
const match = /^ *(\d+)? ?(.*)$/.exec(raw.slice(1));
|
|
329
|
-
const lineNumber = match?.[1];
|
|
330
|
-
if (lineNumber === undefined) {
|
|
331
|
-
pendingBreak = true;
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
if (pendingBreak && current.length > 0) {
|
|
335
|
-
hunks.push(current);
|
|
336
|
-
current = [];
|
|
337
|
-
}
|
|
338
|
-
pendingBreak = false;
|
|
339
|
-
const kind = sign === "+" ? "add" : sign === "-" ? "del" : "ctx";
|
|
340
|
-
current.push({ kind, line: Number(lineNumber), text: match?.[2] ?? "" });
|
|
341
|
-
}
|
|
342
|
-
if (current.length === 0)
|
|
343
|
-
return undefined;
|
|
344
|
-
hunks.push(current);
|
|
345
|
-
return { kind: "hunks", hunks: hunks.map((lines) => ({ lines })) };
|
|
346
|
-
}
|
|
347
|
-
/**
|
|
348
|
-
* The name-free part of a summary for tools whose call arguments read well on their own. The tool
|
|
349
|
-
* name is the row's title, so repeating it here would print it twice.
|
|
350
|
-
*/
|
|
351
|
-
function formatArgumentSummary(toolName, parsed) {
|
|
352
|
-
const path = typeof parsed.path === "string"
|
|
353
|
-
? parsed.path.trim()
|
|
354
|
-
: typeof parsed.file_path === "string"
|
|
355
|
-
? parsed.file_path.trim()
|
|
356
|
-
: "";
|
|
357
|
-
switch (toolName) {
|
|
358
|
-
case "bash": {
|
|
359
|
-
const cmd = typeof parsed.command === "string" ? parsed.command.trim() : "";
|
|
360
|
-
if (!cmd)
|
|
361
|
-
return undefined;
|
|
362
|
-
return cmd.length > 70 ? `${cmd.slice(0, 67)}…` : cmd;
|
|
363
|
-
}
|
|
364
|
-
case "grep":
|
|
365
|
-
case "find": {
|
|
366
|
-
const pattern = typeof parsed.pattern === "string" ? parsed.pattern.trim() : "";
|
|
367
|
-
if (!pattern)
|
|
368
|
-
return undefined;
|
|
369
|
-
const target = path && path !== "." ? ` ${path}` : "";
|
|
370
|
-
return `${pattern}${target}`;
|
|
371
|
-
}
|
|
372
|
-
default: {
|
|
373
|
-
const keyArg = extractSummaryArg(toolName, parsed);
|
|
374
|
-
return keyArg ? keyArg : undefined;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
/**
|
|
379
|
-
* One compact nested summary for a tool call. Adopts the tool's standard collapsed
|
|
380
|
-
* single-line format when recognized, degrading to undefined so callers can use
|
|
381
|
-
* the default argument rendering.
|
|
382
|
-
*/
|
|
383
|
-
export function inlinePresentation(toolName, args, details, isError) {
|
|
384
|
-
const fileTool = fileToolPresentation(toolName, args, details, isError);
|
|
385
|
-
if (fileTool !== undefined)
|
|
386
|
-
return fileTool;
|
|
387
|
-
const parsed = parseCallArgs(args);
|
|
388
|
-
if (parsed === undefined)
|
|
389
|
-
return undefined;
|
|
390
|
-
if (toolName === EDIT_TOOL_NAME) {
|
|
391
|
-
const path = toolPath(parsed);
|
|
392
|
-
const source = isError ? undefined : editDiffSource(details);
|
|
393
|
-
const stats = source === undefined ? undefined : diffStats(source);
|
|
394
|
-
if (path === "" && stats === undefined)
|
|
395
|
-
return undefined;
|
|
396
|
-
return {
|
|
397
|
-
summary: stats === undefined
|
|
398
|
-
? path
|
|
399
|
-
: `${path} (+${String(stats.added)} -${String(stats.removed)})`.trim(),
|
|
400
|
-
status: isError ? "error" : "ok",
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
const baseSummary = formatArgumentSummary(toolName, parsed);
|
|
404
|
-
if (baseSummary === undefined)
|
|
405
|
-
return undefined;
|
|
406
|
-
return {
|
|
407
|
-
summary: baseSummary,
|
|
408
|
-
status: isError ? "error" : "ok",
|
|
409
|
-
};
|
|
410
|
-
}
|
package/dist/renderer.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ExtensionAPI, type ToolRendererResolver, type ToolRenderers } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ToolUiRuntime } from "./runtime.js";
|
|
2
3
|
/** Optional host metadata is absent on early supported Pi versions. */
|
|
3
4
|
export type ToolRenderContext = Parameters<NonNullable<ToolRenderers["renderCall"]>>[2] & {
|
|
4
5
|
readonly durationMs?: number | undefined;
|
|
@@ -11,7 +12,8 @@ export type ToolRenderContext = Parameters<NonNullable<ToolRenderers["renderCall
|
|
|
11
12
|
*/
|
|
12
13
|
export declare const SPECIALIZED_TOOL_RENDERERS: readonly ["codemode"];
|
|
13
14
|
export type SpecializedToolRendererName = (typeof SPECIALIZED_TOOL_RENDERERS)[number];
|
|
14
|
-
export {
|
|
15
|
+
export { extractChildToolCopyText, inlinePresentation } from "./native-tools.js";
|
|
16
|
+
export { estimateStreamingTokens, type NestedCallRecord, } from "./payload-parser.js";
|
|
15
17
|
export interface CollapsibleToolRendererOptions {
|
|
16
18
|
readonly tools?: readonly string[] | undefined;
|
|
17
19
|
readonly match?: ((toolName: string) => boolean) | undefined;
|
|
@@ -20,7 +22,7 @@ export interface CollapsibleToolRendererOptions {
|
|
|
20
22
|
readonly copyToClipboard?: ((text: string) => Promise<void>) | undefined;
|
|
21
23
|
}
|
|
22
24
|
export declare function isMcpTool(toolName: string): boolean;
|
|
23
|
-
export declare function createCollapsibleToolRendererResolver(options?: CollapsibleToolRendererOptions): ToolRendererResolver;
|
|
25
|
+
export declare function createCollapsibleToolRendererResolver(runtime: ToolUiRuntime, options?: CollapsibleToolRendererOptions): ToolRendererResolver;
|
|
24
26
|
/** UI factory consumed by package-owned direct renderers, independently of host resolver precedence. */
|
|
25
|
-
export declare function createOptionalToolRendererFactory(pi: ExtensionAPI, options?: CollapsibleToolRendererOptions): import("@hheei/pi-ext-core").ToolRendererFactory;
|
|
26
|
-
export declare function registerCollapsibleToolRenderer(pi: ExtensionAPI, options?: CollapsibleToolRendererOptions): () => void;
|
|
27
|
+
export declare function createOptionalToolRendererFactory(pi: ExtensionAPI, runtime: ToolUiRuntime, options?: CollapsibleToolRendererOptions): import("@hheei/pi-ext-core").ToolRendererFactory;
|
|
28
|
+
export declare function registerCollapsibleToolRenderer(pi: ExtensionAPI, runtime: ToolUiRuntime, options?: CollapsibleToolRendererOptions): () => void;
|