@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/message-view.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { truncateToWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
|
|
1
|
+
import { Box, truncateToWidth, wrapTextWithAnsi, } from "@earendil-works/pi-tui";
|
|
2
|
+
import { backgroundFill } from "./background.js";
|
|
2
3
|
import { toContentRows, toSingleRow } from "./tool-view.js";
|
|
3
4
|
/**
|
|
4
5
|
* A transcript notice, not a tool execution.
|
|
@@ -59,10 +60,22 @@ export class MessageView {
|
|
|
59
60
|
#localExpanded;
|
|
60
61
|
#lastWidth = 80;
|
|
61
62
|
#lastHeight = 0;
|
|
63
|
+
/** The final rows the host draws: the background shell's output, not the pre-shell lines. */
|
|
62
64
|
#cache;
|
|
65
|
+
/** The rows the shell currently fills; replaced on every layout. */
|
|
66
|
+
#shellRows = [];
|
|
67
|
+
/**
|
|
68
|
+
* The block background a standalone message owns. Pi's custom-message shell only draws its own
|
|
69
|
+
* `customMessageBg` box when no renderer is installed, so this view supplies the same block
|
|
70
|
+
* itself. Inside an activity group the rows come from `previewRows`/`bodyRows` and keep the
|
|
71
|
+
* group's background instead.
|
|
72
|
+
*/
|
|
73
|
+
#shell = new Box(0, 0);
|
|
63
74
|
constructor(spec) {
|
|
64
75
|
this.#spec = spec;
|
|
65
76
|
this.#localExpanded = remembered(spec.stateHolder, spec.defaultExpanded === true);
|
|
77
|
+
this.#shell.addChild({ render: () => this.#shellRows, invalidate() { } });
|
|
78
|
+
this.#shell.setBgFn(backgroundFill(spec.theme, "customMessageBg"));
|
|
66
79
|
}
|
|
67
80
|
get expandable() {
|
|
68
81
|
return hasRows(this.#spec.body);
|
|
@@ -91,27 +104,26 @@ export class MessageView {
|
|
|
91
104
|
const safeWidth = Math.max(1, width - pad * 2);
|
|
92
105
|
this.#lastWidth = width;
|
|
93
106
|
const leftPad = pad > 0 ? " ".repeat(pad) : "";
|
|
94
|
-
|
|
107
|
+
this.#shellRows = [
|
|
95
108
|
this.#header(safeWidth, expanded),
|
|
96
109
|
...layRows(this.#spec.preview, safeWidth),
|
|
97
110
|
...(expanded ? layRows(this.#spec.body, safeWidth) : []),
|
|
98
111
|
].map((line) => (leftPad === "" ? line : `${leftPad}${line}`));
|
|
99
|
-
this.#lastHeight =
|
|
112
|
+
this.#lastHeight = this.#shellRows.length;
|
|
113
|
+
// Cache the shell's output: a cached hit must return the same filled, padded rows.
|
|
114
|
+
const lines = this.#shell.render(width);
|
|
100
115
|
this.#cache = { width, expanded, lines };
|
|
101
116
|
return lines;
|
|
102
117
|
}
|
|
103
118
|
invalidate() {
|
|
104
119
|
this.#cache = undefined;
|
|
120
|
+
this.#shell.invalidate();
|
|
105
121
|
invalidateRows(this.#spec.preview);
|
|
106
122
|
invalidateRows(this.#spec.body);
|
|
107
123
|
}
|
|
108
124
|
handleMouse(event) {
|
|
109
|
-
if (!this.expandable ||
|
|
110
|
-
event.type === "wheel" ||
|
|
111
|
-
event.type !== "click" ||
|
|
112
|
-
event.button !== "left") {
|
|
125
|
+
if (!this.expandable || event.type !== "click" || event.button !== "left")
|
|
113
126
|
return undefined;
|
|
114
|
-
}
|
|
115
127
|
const effectiveWidth = event.width > 0 ? event.width : this.#lastWidth;
|
|
116
128
|
const pad = Math.max(0, this.#spec.outputPad ?? 0);
|
|
117
129
|
const innerX = event.x - pad;
|
|
@@ -125,6 +137,24 @@ export class MessageView {
|
|
|
125
137
|
this.toggle();
|
|
126
138
|
return { handled: true, render: true };
|
|
127
139
|
}
|
|
140
|
+
/** Provider-declared notice type, used to group notices without reading their text. */
|
|
141
|
+
get kind() {
|
|
142
|
+
return this.#spec.header.kind;
|
|
143
|
+
}
|
|
144
|
+
/** Preview rows inside an activity group: visible content below the summary, folded or not. */
|
|
145
|
+
previewRows(width) {
|
|
146
|
+
return this.#rows(this.#spec.preview, width);
|
|
147
|
+
}
|
|
148
|
+
/** Body rows inside an activity group: what opening this notice reveals. */
|
|
149
|
+
bodyRows(width) {
|
|
150
|
+
return this.#rows(this.#spec.body, width);
|
|
151
|
+
}
|
|
152
|
+
#rows(source, width) {
|
|
153
|
+
const pad = Math.max(0, this.#spec.outputPad ?? 0);
|
|
154
|
+
const safeWidth = Math.max(1, width - pad * 2);
|
|
155
|
+
const leftPad = pad > 0 ? " ".repeat(pad) : "";
|
|
156
|
+
return layRows(source, safeWidth).map((line) => (leftPad === "" ? line : `${leftPad}${line}`));
|
|
157
|
+
}
|
|
128
158
|
/** Single-row label used when this notice sits inside an activity group. */
|
|
129
159
|
get activitySummary() {
|
|
130
160
|
const { theme, header } = this.#spec;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type AgentToolResult, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type NestedCallRecord } from "./payload-parser.js";
|
|
3
|
+
import type { ToolStatusKind } from "./tool-view.js";
|
|
4
|
+
import type { CapturedToolCall } from "./tracker.js";
|
|
5
|
+
/**
|
|
6
|
+
* The single owner of Pi's own tool vocabulary in this UI. Concrete tool packages state their own
|
|
7
|
+
* semantics through ext-core's presentation point; this module is the explicit adapter for the tools
|
|
8
|
+
* Pi itself ships (read, write, edit, bash, grep, find, codemode, python_eval and the background and
|
|
9
|
+
* subagent helpers), so a top-level row, a nested row and a restored row read the same facts.
|
|
10
|
+
*/
|
|
11
|
+
export declare const EDIT_TOOL_NAME = "edit";
|
|
12
|
+
export declare const WRITE_TOOL_NAME = "write";
|
|
13
|
+
export declare const CODEMODE_OPTIONS_PREFIX = "// @options:";
|
|
14
|
+
export declare function toolPath(args: unknown): string;
|
|
15
|
+
/**
|
|
16
|
+
* A native tool's header summary. read states its own tokens, bash keeps its command as the row's
|
|
17
|
+
* content, and the rest state their argument vocabulary; undefined means the frame falls back to the
|
|
18
|
+
* generic argument line.
|
|
19
|
+
*/
|
|
20
|
+
export interface NativeHeaderSummary {
|
|
21
|
+
readonly summary: string;
|
|
22
|
+
/** Whether the summary already carries the tool's own colors. */
|
|
23
|
+
readonly styled: boolean;
|
|
24
|
+
/** Whether secondary facts stay in the summary or move to the dim meta slot. */
|
|
25
|
+
readonly factsInSummary: boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare function nativeHeaderSummary(toolName: string, args: unknown, theme: Theme, cwd: string): NativeHeaderSummary | undefined;
|
|
28
|
+
/** The syntax a tool's request is written in, for display only. */
|
|
29
|
+
export declare function nativeCodeLanguage(toolName: string): string | undefined;
|
|
30
|
+
/** The language a tool's own output is written in, when the tool states it through its arguments. */
|
|
31
|
+
export declare function nativeOutputLanguage(toolName: string, args: unknown): string | undefined;
|
|
32
|
+
/** A deadline is a secondary fact about a call, not part of what the tool was asked to do. */
|
|
33
|
+
export declare function nativeTimeoutFact(toolName: string, args: unknown): string | undefined;
|
|
34
|
+
/** The request block a call is drawn from: its bytes, its syntax, and whether it is a shell command. */
|
|
35
|
+
export interface NativeRequestBlock {
|
|
36
|
+
readonly text: string;
|
|
37
|
+
readonly language: string | undefined;
|
|
38
|
+
readonly shell: boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare function nativeRequestBlock(toolName: string, args: unknown): NativeRequestBlock | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* The request a heavy-input tool received, byte for byte: a copy of it must not trim or sanitize
|
|
43
|
+
* model-controlled text. Only an all-whitespace request has no body, so it reads as no request.
|
|
44
|
+
*/
|
|
45
|
+
export declare function nativeHeavyRequest(toolName: string, args: unknown): string | undefined;
|
|
46
|
+
export declare function nativeSummaryArg(toolName: string, args: unknown): string;
|
|
47
|
+
/** The one-row result text a native tool reports, plus the wall time codemode states in its header. */
|
|
48
|
+
export declare function nativeResultText(toolName: string, result: AgentToolResult<unknown>): {
|
|
49
|
+
readonly outputText: string;
|
|
50
|
+
readonly wallTimeSeconds: number | undefined;
|
|
51
|
+
};
|
|
52
|
+
/** Compact nested-row result: a one-line summary plus the status it reports. */
|
|
53
|
+
export interface InlinePresentation {
|
|
54
|
+
readonly summary: string;
|
|
55
|
+
readonly status: ToolStatusKind;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* One compact nested summary for a tool call. Adopts the tool's standard collapsed
|
|
59
|
+
* single-line format when recognized, degrading to undefined so callers can use
|
|
60
|
+
* the default argument rendering.
|
|
61
|
+
*/
|
|
62
|
+
export declare function inlinePresentation(toolName: string, args: unknown, details: unknown, isError: boolean): InlinePresentation | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* The request bytes one call carries. A record whose request the session truncated has none: copying
|
|
65
|
+
* the size-limited preview or the summary would present a partial request as the full one.
|
|
66
|
+
*/
|
|
67
|
+
export declare function nativeRequestCopy(call: CapturedToolCall | NestedCallRecord): string | undefined;
|
|
68
|
+
/** What a child row copies: its own request when it has one, otherwise the result it produced. */
|
|
69
|
+
export declare function extractChildToolCopyText(call: CapturedToolCall | NestedCallRecord, full?: boolean): string;
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { getLanguageFromPath, } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { getCapabilities, hyperlink, stripTerminalSequences } from "@earendil-works/pi-tui";
|
|
6
|
+
import { diffStats } from "./diff-view.js";
|
|
7
|
+
import { formatTimeoutFact, isRecord, parseCallArgs, } from "./payload-parser.js";
|
|
8
|
+
/**
|
|
9
|
+
* The single owner of Pi's own tool vocabulary in this UI. Concrete tool packages state their own
|
|
10
|
+
* semantics through ext-core's presentation point; this module is the explicit adapter for the tools
|
|
11
|
+
* Pi itself ships (read, write, edit, bash, grep, find, codemode, python_eval and the background and
|
|
12
|
+
* subagent helpers), so a top-level row, a nested row and a restored row read the same facts.
|
|
13
|
+
*/
|
|
14
|
+
export const EDIT_TOOL_NAME = "edit";
|
|
15
|
+
export const WRITE_TOOL_NAME = "write";
|
|
16
|
+
export const CODEMODE_OPTIONS_PREFIX = "// @options:";
|
|
17
|
+
const CODEMODE_SCRIPT_HEADER = /^Script (?:completed|failed)\r?\nWall time ([\d.]+) seconds\r?\nOutput:\r?\n/;
|
|
18
|
+
// Pi <1.3 has no exported read-range formatter. Keep this UI summary independent of that export.
|
|
19
|
+
function readLineNumber(value) {
|
|
20
|
+
const parsed = typeof value === "number"
|
|
21
|
+
? value
|
|
22
|
+
: typeof value === "string" && value.trim() !== ""
|
|
23
|
+
? Number(value)
|
|
24
|
+
: Number.NaN;
|
|
25
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
26
|
+
}
|
|
27
|
+
/** The read line range as the native row draws it (`:12`, `:12-40`), or empty. */
|
|
28
|
+
function readLineRange(args) {
|
|
29
|
+
const parsed = parseCallArgs(args);
|
|
30
|
+
const offset = parsed?.offset;
|
|
31
|
+
const limit = parsed?.limit;
|
|
32
|
+
if (offset == null && limit == null)
|
|
33
|
+
return "";
|
|
34
|
+
const start = offset == null ? 1 : readLineNumber(offset);
|
|
35
|
+
if (start === undefined)
|
|
36
|
+
return "";
|
|
37
|
+
if (limit == null)
|
|
38
|
+
return `:${start}`;
|
|
39
|
+
const count = readLineNumber(limit);
|
|
40
|
+
if (count === undefined)
|
|
41
|
+
return "";
|
|
42
|
+
const end = start + count - 1;
|
|
43
|
+
return `:${start}${end ? `-${end}` : ""}`;
|
|
44
|
+
}
|
|
45
|
+
function shortenHome(path) {
|
|
46
|
+
const home = homedir();
|
|
47
|
+
return path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
48
|
+
}
|
|
49
|
+
export function toolPath(args) {
|
|
50
|
+
const parsed = parseCallArgs(args);
|
|
51
|
+
if (!parsed)
|
|
52
|
+
return "";
|
|
53
|
+
const val = parsed.path ?? parsed.file_path;
|
|
54
|
+
return typeof val === "string" ? val.trim() : "";
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* read's header summary with the tokens the native read row uses: the accent path, linked to the
|
|
58
|
+
* file when the terminal draws OSC 8 links, followed by its warning line range.
|
|
59
|
+
*/
|
|
60
|
+
function readHeaderSummary(args, theme, cwd) {
|
|
61
|
+
const path = toolPath(args);
|
|
62
|
+
if (path === "")
|
|
63
|
+
return "";
|
|
64
|
+
const label = theme.fg("accent", shortenHome(path));
|
|
65
|
+
const linked = getCapabilities().hyperlinks
|
|
66
|
+
? hyperlink(label, pathToFileURL(resolve(cwd, path)).href)
|
|
67
|
+
: label;
|
|
68
|
+
const range = readLineRange(args);
|
|
69
|
+
return `${linked}${range === "" ? "" : theme.fg("warning", range)}`;
|
|
70
|
+
}
|
|
71
|
+
export function nativeHeaderSummary(toolName, args, theme, cwd) {
|
|
72
|
+
if (toolName === "read") {
|
|
73
|
+
const semantic = readHeaderSummary(args, theme, cwd);
|
|
74
|
+
return semantic === "" ? undefined : { summary: semantic, styled: true, factsInSummary: false };
|
|
75
|
+
}
|
|
76
|
+
if (toolName === "bash") {
|
|
77
|
+
return { summary: nativeSummaryArg(toolName, args), styled: false, factsInSummary: false };
|
|
78
|
+
}
|
|
79
|
+
const summary = nativeSummaryArg(toolName, args);
|
|
80
|
+
return summary === "" ? undefined : { summary, styled: false, factsInSummary: true };
|
|
81
|
+
}
|
|
82
|
+
/** The syntax a tool's request is written in, for display only. */
|
|
83
|
+
export function nativeCodeLanguage(toolName) {
|
|
84
|
+
if (toolName === "bash")
|
|
85
|
+
return "bash";
|
|
86
|
+
if (toolName === "codemode")
|
|
87
|
+
return "javascript";
|
|
88
|
+
if (toolName === "python_eval")
|
|
89
|
+
return "python";
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
/** The language a tool's own output is written in, when the tool states it through its arguments. */
|
|
93
|
+
export function nativeOutputLanguage(toolName, args) {
|
|
94
|
+
if (toolName !== "read")
|
|
95
|
+
return undefined;
|
|
96
|
+
const path = toolPath(args);
|
|
97
|
+
return path === "" ? undefined : getLanguageFromPath(path);
|
|
98
|
+
}
|
|
99
|
+
/** A deadline is a secondary fact about a call, not part of what the tool was asked to do. */
|
|
100
|
+
export function nativeTimeoutFact(toolName, args) {
|
|
101
|
+
if (toolName !== "bash")
|
|
102
|
+
return undefined;
|
|
103
|
+
const timeout = isRecord(args) ? args.timeout : undefined;
|
|
104
|
+
if (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0)
|
|
105
|
+
return undefined;
|
|
106
|
+
return formatTimeoutFact(timeout * 1000);
|
|
107
|
+
}
|
|
108
|
+
export function nativeRequestBlock(toolName, args) {
|
|
109
|
+
const text = nativeHeavyRequest(toolName, args);
|
|
110
|
+
if (text === undefined)
|
|
111
|
+
return undefined;
|
|
112
|
+
return { text, language: nativeCodeLanguage(toolName), shell: toolName === "bash" };
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The request a heavy-input tool received, byte for byte: a copy of it must not trim or sanitize
|
|
116
|
+
* model-controlled text. Only an all-whitespace request has no body, so it reads as no request.
|
|
117
|
+
*/
|
|
118
|
+
export function nativeHeavyRequest(toolName, args) {
|
|
119
|
+
if (!isRecord(args))
|
|
120
|
+
return undefined;
|
|
121
|
+
const raw = toolName === "bash" && typeof args.command === "string"
|
|
122
|
+
? args.command
|
|
123
|
+
: toolName === "python_eval" && typeof args.code === "string"
|
|
124
|
+
? args.code
|
|
125
|
+
: toolName === "codemode" && typeof args.code === "string"
|
|
126
|
+
? args.code
|
|
127
|
+
: undefined;
|
|
128
|
+
return raw === undefined || raw.trim() === "" ? undefined : raw;
|
|
129
|
+
}
|
|
130
|
+
export function nativeSummaryArg(toolName, args) {
|
|
131
|
+
if (toolName === "codemode") {
|
|
132
|
+
return extractCodemodeHeaderFacts(args);
|
|
133
|
+
}
|
|
134
|
+
const parsed = parseCallArgs(args);
|
|
135
|
+
if (!parsed)
|
|
136
|
+
return "";
|
|
137
|
+
const firstLine = (val) => {
|
|
138
|
+
if (typeof val !== "string" || val.trim() === "")
|
|
139
|
+
return "";
|
|
140
|
+
const clean = stripTerminalSequences(val.trim());
|
|
141
|
+
return clean.split("\n")[0]?.trim() ?? clean;
|
|
142
|
+
};
|
|
143
|
+
switch (toolName) {
|
|
144
|
+
case "bash":
|
|
145
|
+
case "bg_bash_spawn":
|
|
146
|
+
return firstLine(parsed.command);
|
|
147
|
+
case "read":
|
|
148
|
+
case "edit":
|
|
149
|
+
case "write":
|
|
150
|
+
case "ls": {
|
|
151
|
+
const path = typeof parsed.path === "string"
|
|
152
|
+
? parsed.path.trim()
|
|
153
|
+
: typeof parsed.file_path === "string"
|
|
154
|
+
? parsed.file_path.trim()
|
|
155
|
+
: "";
|
|
156
|
+
return stripTerminalSequences(path);
|
|
157
|
+
}
|
|
158
|
+
case "grep":
|
|
159
|
+
case "find": {
|
|
160
|
+
const pattern = typeof parsed.pattern === "string" ? parsed.pattern.trim() : "";
|
|
161
|
+
const path = typeof parsed.path === "string" ? parsed.path.trim() : "";
|
|
162
|
+
if (!pattern)
|
|
163
|
+
return path ? stripTerminalSequences(path) : "";
|
|
164
|
+
const target = path && path !== "." ? ` ${path}` : "";
|
|
165
|
+
return `${pattern}${target}`;
|
|
166
|
+
}
|
|
167
|
+
case "python_eval":
|
|
168
|
+
return firstLine(parsed.code);
|
|
169
|
+
case "bg_bash_get":
|
|
170
|
+
case "bg_bash_stop":
|
|
171
|
+
case "agents_get":
|
|
172
|
+
case "agents_stop":
|
|
173
|
+
return typeof parsed.id === "string" ? parsed.id.trim() : "";
|
|
174
|
+
case "agents_delegate": {
|
|
175
|
+
const agent = typeof parsed.agent === "string" ? parsed.agent.trim() : "";
|
|
176
|
+
const id = typeof parsed.id === "string" ? parsed.id.trim() : "";
|
|
177
|
+
const task = firstLine(parsed.task);
|
|
178
|
+
if (agent && task)
|
|
179
|
+
return `${agent}: ${task}`;
|
|
180
|
+
return agent || id || task;
|
|
181
|
+
}
|
|
182
|
+
case "tool_search":
|
|
183
|
+
return typeof parsed.query === "string" ? parsed.query.trim() : "";
|
|
184
|
+
default:
|
|
185
|
+
return "";
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/** codemode states its own options in the request's first line; they are header facts, not content. */
|
|
189
|
+
function extractCodemodeHeaderFacts(args) {
|
|
190
|
+
if (!isRecord(args))
|
|
191
|
+
return "";
|
|
192
|
+
const options = {};
|
|
193
|
+
for (const [key, value] of Object.entries(args)) {
|
|
194
|
+
if (key !== "code" && value !== undefined && value !== null) {
|
|
195
|
+
options[key] = value;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (typeof args.code === "string") {
|
|
199
|
+
const firstLine = args.code.split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
200
|
+
if (firstLine.startsWith(CODEMODE_OPTIONS_PREFIX)) {
|
|
201
|
+
try {
|
|
202
|
+
const parsed = JSON.parse(firstLine.slice(CODEMODE_OPTIONS_PREFIX.length).trim());
|
|
203
|
+
if (isRecord(parsed)) {
|
|
204
|
+
Object.assign(options, parsed);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch { }
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const facts = [];
|
|
211
|
+
const timeoutMs = typeof options.timeout_ms === "number" ? options.timeout_ms : undefined;
|
|
212
|
+
if (timeoutMs !== undefined)
|
|
213
|
+
facts.push(formatTimeoutFact(timeoutMs));
|
|
214
|
+
if (typeof options.max_output_tokens === "number") {
|
|
215
|
+
facts.push(`(tokens ${String(options.max_output_tokens)})`);
|
|
216
|
+
}
|
|
217
|
+
if (typeof options.target === "string" && options.target !== "local") {
|
|
218
|
+
facts.push(`(${options.target})`);
|
|
219
|
+
}
|
|
220
|
+
return facts.join(" ");
|
|
221
|
+
}
|
|
222
|
+
/** The one-row result text a native tool reports, plus the wall time codemode states in its header. */
|
|
223
|
+
export function nativeResultText(toolName, result) {
|
|
224
|
+
const parts = [];
|
|
225
|
+
for (const block of result.content) {
|
|
226
|
+
if (block.type === "text" && block.text.trim() !== "") {
|
|
227
|
+
parts.push(block.text);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const joined = parts.join("\n");
|
|
231
|
+
if (toolName === "codemode") {
|
|
232
|
+
const stripped = stripTerminalSequences(joined);
|
|
233
|
+
const match = CODEMODE_SCRIPT_HEADER.exec(stripped);
|
|
234
|
+
const wallTimeSeconds = match?.[1] !== undefined ? Number.parseFloat(match[1]) : undefined;
|
|
235
|
+
const cleaned = joined.replace(CODEMODE_SCRIPT_HEADER, "").trim();
|
|
236
|
+
return { outputText: cleaned, wallTimeSeconds };
|
|
237
|
+
}
|
|
238
|
+
return { outputText: joined, wallTimeSeconds: undefined };
|
|
239
|
+
}
|
|
240
|
+
function fileToolPresentation(toolName, args, _details, isError) {
|
|
241
|
+
const path = toolPath(args);
|
|
242
|
+
if (toolName === "read") {
|
|
243
|
+
if (path === "")
|
|
244
|
+
return undefined;
|
|
245
|
+
return { summary: `${path}${readLineRange(args)}`, status: isError ? "error" : "ok" };
|
|
246
|
+
}
|
|
247
|
+
if (toolName === WRITE_TOOL_NAME) {
|
|
248
|
+
return path === "" ? undefined : { summary: path, status: isError ? "error" : "ok" };
|
|
249
|
+
}
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
/** Pi native edit persists a unified patch plus the same change as a numbered display diff. */
|
|
253
|
+
function nativeDiffSource(details) {
|
|
254
|
+
if (!isRecord(details))
|
|
255
|
+
return undefined;
|
|
256
|
+
if (typeof details.patch === "string" && details.patch.trim() !== "") {
|
|
257
|
+
return { kind: "unifiedPatch", text: details.patch };
|
|
258
|
+
}
|
|
259
|
+
if (typeof details.diff === "string" && details.diff.trim() !== "") {
|
|
260
|
+
return displayDiffSource(details.diff);
|
|
261
|
+
}
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
function displayDiffSource(diff) {
|
|
265
|
+
const hunks = [];
|
|
266
|
+
let current = [];
|
|
267
|
+
let pendingBreak = false;
|
|
268
|
+
for (const raw of diff.split(/\r?\n/)) {
|
|
269
|
+
if (raw === "")
|
|
270
|
+
continue;
|
|
271
|
+
const sign = raw.charAt(0);
|
|
272
|
+
const match = /^ *(\d+)? ?(.*)$/.exec(raw.slice(1));
|
|
273
|
+
const lineNumber = match?.[1];
|
|
274
|
+
if (lineNumber === undefined) {
|
|
275
|
+
pendingBreak = true;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (pendingBreak && current.length > 0) {
|
|
279
|
+
hunks.push(current);
|
|
280
|
+
current = [];
|
|
281
|
+
}
|
|
282
|
+
pendingBreak = false;
|
|
283
|
+
const kind = sign === "+" ? "add" : sign === "-" ? "del" : "ctx";
|
|
284
|
+
current.push({ kind, line: Number(lineNumber), text: match?.[2] ?? "" });
|
|
285
|
+
}
|
|
286
|
+
if (current.length === 0)
|
|
287
|
+
return undefined;
|
|
288
|
+
hunks.push(current);
|
|
289
|
+
return { kind: "hunks", hunks: hunks.map((lines) => ({ lines })) };
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* The name-free part of a summary for tools whose call arguments read well on their own. The tool
|
|
293
|
+
* name is the row's title, so repeating it here would print it twice.
|
|
294
|
+
*/
|
|
295
|
+
function formatArgumentSummary(toolName, parsed) {
|
|
296
|
+
const path = typeof parsed.path === "string"
|
|
297
|
+
? parsed.path.trim()
|
|
298
|
+
: typeof parsed.file_path === "string"
|
|
299
|
+
? parsed.file_path.trim()
|
|
300
|
+
: "";
|
|
301
|
+
switch (toolName) {
|
|
302
|
+
case "bash": {
|
|
303
|
+
// The row states model-controlled text: strip before cutting so no half sequence survives.
|
|
304
|
+
const cmd = typeof parsed.command === "string" ? stripTerminalSequences(parsed.command.trim()) : "";
|
|
305
|
+
if (!cmd)
|
|
306
|
+
return undefined;
|
|
307
|
+
return cmd.length > 70 ? `${cmd.slice(0, 67)}…` : cmd;
|
|
308
|
+
}
|
|
309
|
+
case "grep":
|
|
310
|
+
case "find": {
|
|
311
|
+
const pattern = typeof parsed.pattern === "string" ? parsed.pattern.trim() : "";
|
|
312
|
+
if (!pattern)
|
|
313
|
+
return undefined;
|
|
314
|
+
const target = path && path !== "." ? ` ${path}` : "";
|
|
315
|
+
return `${pattern}${target}`;
|
|
316
|
+
}
|
|
317
|
+
default: {
|
|
318
|
+
const keyArg = nativeSummaryArg(toolName, parsed);
|
|
319
|
+
return keyArg ? keyArg : undefined;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* One compact nested summary for a tool call. Adopts the tool's standard collapsed
|
|
325
|
+
* single-line format when recognized, degrading to undefined so callers can use
|
|
326
|
+
* the default argument rendering.
|
|
327
|
+
*/
|
|
328
|
+
export function inlinePresentation(toolName, args, details, isError) {
|
|
329
|
+
const fileTool = fileToolPresentation(toolName, args, details, isError);
|
|
330
|
+
if (fileTool !== undefined)
|
|
331
|
+
return fileTool;
|
|
332
|
+
const parsed = parseCallArgs(args);
|
|
333
|
+
if (parsed === undefined)
|
|
334
|
+
return undefined;
|
|
335
|
+
if (toolName === EDIT_TOOL_NAME) {
|
|
336
|
+
const path = toolPath(parsed);
|
|
337
|
+
const source = isError ? undefined : nativeDiffSource(details);
|
|
338
|
+
const stats = source === undefined ? undefined : diffStats(source);
|
|
339
|
+
if (path === "" && stats === undefined)
|
|
340
|
+
return undefined;
|
|
341
|
+
return {
|
|
342
|
+
summary: stats === undefined
|
|
343
|
+
? path
|
|
344
|
+
: `${path} (+${String(stats.added)} -${String(stats.removed)})`.trim(),
|
|
345
|
+
status: isError ? "error" : "ok",
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
const baseSummary = formatArgumentSummary(toolName, parsed);
|
|
349
|
+
if (baseSummary === undefined)
|
|
350
|
+
return undefined;
|
|
351
|
+
return {
|
|
352
|
+
summary: baseSummary,
|
|
353
|
+
status: isError ? "error" : "ok",
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* The request bytes one call carries. A record whose request the session truncated has none: copying
|
|
358
|
+
* the size-limited preview or the summary would present a partial request as the full one.
|
|
359
|
+
*/
|
|
360
|
+
export function nativeRequestCopy(call) {
|
|
361
|
+
if ("argsTruncated" in call && call.argsTruncated === true)
|
|
362
|
+
return undefined;
|
|
363
|
+
const args = "args" in call ? call.args : undefined;
|
|
364
|
+
if (!isRecord(args))
|
|
365
|
+
return undefined;
|
|
366
|
+
const toolName = "toolName" in call ? call.toolName : call.name;
|
|
367
|
+
if (toolName === "write" && typeof args.content === "string")
|
|
368
|
+
return args.content;
|
|
369
|
+
if (toolName === "bash" && typeof args.command === "string")
|
|
370
|
+
return args.command;
|
|
371
|
+
if ((toolName === "codemode" || toolName === "python_eval") && typeof args.code === "string") {
|
|
372
|
+
return args.code;
|
|
373
|
+
}
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
function resultText(result) {
|
|
377
|
+
if (result === undefined || !Array.isArray(result.content))
|
|
378
|
+
return undefined;
|
|
379
|
+
const texts = result.content
|
|
380
|
+
.filter((c) => isRecord(c) && c.type === "text" && typeof c.text === "string")
|
|
381
|
+
.map((c) => c.text);
|
|
382
|
+
return texts.length === 0 ? undefined : texts.join("\n");
|
|
383
|
+
}
|
|
384
|
+
/** What a child row copies: its own request when it has one, otherwise the result it produced. */
|
|
385
|
+
export function extractChildToolCopyText(call, full) {
|
|
386
|
+
const toolName = "toolName" in call ? call.toolName : call.name;
|
|
387
|
+
const result = "result" in call ? call.result : undefined;
|
|
388
|
+
const args = "args" in call ? call.args : undefined;
|
|
389
|
+
const request = nativeRequestCopy(call);
|
|
390
|
+
if (request !== undefined) {
|
|
391
|
+
// write's request is the whole file: its result only confirms the write.
|
|
392
|
+
if (full !== true || toolName === WRITE_TOOL_NAME)
|
|
393
|
+
return request;
|
|
394
|
+
const output = resultText(result);
|
|
395
|
+
return output === undefined ? request : `${request}\n---\n${output}`;
|
|
396
|
+
}
|
|
397
|
+
if (result !== undefined) {
|
|
398
|
+
const text = resultText(result);
|
|
399
|
+
if (text !== undefined)
|
|
400
|
+
return text;
|
|
401
|
+
}
|
|
402
|
+
if (args !== undefined) {
|
|
403
|
+
if (typeof args === "string")
|
|
404
|
+
return args;
|
|
405
|
+
try {
|
|
406
|
+
return JSON.stringify(args, null, 2);
|
|
407
|
+
}
|
|
408
|
+
catch {
|
|
409
|
+
return String(args);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
// A summary is a summary: without arguments or a result there is no request to copy.
|
|
413
|
+
return "";
|
|
414
|
+
}
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import { type Theme, type ToolRenderers } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type Component, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
3
3
|
import type { ToolPresentationProvider } from "@hheei/pi-ext-core";
|
|
4
|
-
import {
|
|
4
|
+
import type { NestedCallRecord, ToolRenderContext } from "./renderer.js";
|
|
5
5
|
import { type Collapsible } from "./tool-view.js";
|
|
6
6
|
import type { CapturedToolCall } from "./tracker.js";
|
|
7
|
-
export declare function extractChildToolCopyText(call: CapturedToolCall | NestedCallRecord, full?: boolean): string;
|
|
8
7
|
export interface NestedCallsViewProps {
|
|
9
8
|
readonly resolvePresentation?: ((toolName: string) => ToolPresentationProvider | undefined) | undefined;
|
|
10
9
|
readonly calls: readonly (CapturedToolCall | NestedCallRecord)[];
|
|
11
10
|
readonly theme: Theme;
|
|
12
11
|
readonly context: ToolRenderContext;
|
|
13
12
|
readonly resolveChildRenderer?: ((toolName: string) => ToolRenderers | undefined) | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* The owning layer's themed summary for a child call. A board cannot recover a child's semantic
|
|
15
|
+
* tokens from text, so the layer that owns that tool states them here.
|
|
16
|
+
*/
|
|
17
|
+
readonly headerSummary?: ((toolName: string, args: unknown, theme: Theme) => string | undefined) | undefined;
|
|
14
18
|
readonly copyToClipboard?: ((text: string) => Promise<void>) | undefined;
|
|
15
19
|
readonly stateHolder?: Record<string, unknown> | undefined;
|
|
16
20
|
readonly invalidate?: (() => void) | undefined;
|