@hheei/pi-ext-ui 0.1.0 → 0.2.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 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/nested-calls-view.d.ts +28 -0
- package/dist/nested-calls-view.js +406 -0
- package/dist/payload-parser.d.ts +120 -0
- package/dist/payload-parser.js +644 -0
- package/dist/renderer.d.ts +2 -26
- package/dist/renderer.js +333 -616
- package/dist/tool-view.d.ts +50 -3
- package/dist/tool-view.js +209 -55
- package/dist/tracker.d.ts +26 -0
- package/dist/tracker.js +81 -0
- package/package.json +51 -49
- package/dist/collapsible-tool-frame.d.ts +0 -1
- package/dist/collapsible-tool-frame.js +0 -1
package/README.md
CHANGED
|
@@ -5,3 +5,5 @@ Pi UI primitives and a small set of specialized tool renderers.
|
|
|
5
5
|
`pi-ext-ui` owns the visual result, not tool execution. Its default resolver only handles tools with an explicit specialized renderer (`edit`, `read`, `write`, `apply_patch`, `eval`, and `codemode`). Other tools remain on Pi's native renderer. Extensions can opt into the shared visual API with `createToolView`, `ToolView`, and `DiffView`, or explicitly add a tool through resolver options.
|
|
6
6
|
|
|
7
7
|
The shared view API owns fold levels, compact sections, status glyphs, copy feedback, and width-safe rendering. It does not read files, execute tools, infer outcomes, or import a concrete extension.
|
|
8
|
+
|
|
9
|
+
Background Bash and subagent tools own their display formats through ext-core `ToolPresentation` providers. This resolver consumes those providers automatically, including nested codemode calls, and adds shared folding and copy controls. Without `pi-ext-ui`, the same tools retain native Pi renderers. No direct dependency on `pi-ext-ui` is required by the tool extensions.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { type DiffHunk, type DiffLine, type DiffSource, type DiffStats, DiffView, type DiffViewOptions, diffStats, } from "./diff-view.js";
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
2
|
+
export { extractChildToolCopyText, NestedCallsView, type NestedCallsViewProps, } from "./nested-calls-view.js";
|
|
3
|
+
export { type CollapsibleToolRendererOptions, createCollapsibleToolRendererResolver, estimateStreamingTokens, inlinePresentation, type NestedCallRecord, registerCollapsibleToolRenderer, SPECIALIZED_TOOL_RENDERERS, type SpecializedToolRendererName, type ToolRenderContext, WEB_ACCESS_TOOLS, type WebAccessToolName, } from "./renderer.js";
|
|
4
|
+
export { type Collapsible, createToolView, type FoldLevel, formatStatusGlyph, formatToolHeaderText, isCollapsible, type RailColorToken, type RailSection, type ToolStatusKind, ToolView, type ToolViewCopySpec, type ToolViewHeaderSpec, type ToolViewSpec, transitionFoldLevel, } from "./tool-view.js";
|
|
5
|
+
export { type CapturedToolCall, clearCapturedNestedCalls, getCapturedNestedCalls, recordToolExecutionEnd, recordToolExecutionStart, } from "./tracker.js";
|
|
4
6
|
export { formatTurnFlow, type TurnFlowData, type TurnToolCallSummary, } from "./turn-copy.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { DiffView, diffStats, } from "./diff-view.js";
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
2
|
+
export { extractChildToolCopyText, NestedCallsView, } from "./nested-calls-view.js";
|
|
3
|
+
export { createCollapsibleToolRendererResolver, estimateStreamingTokens, inlinePresentation, registerCollapsibleToolRenderer, SPECIALIZED_TOOL_RENDERERS, WEB_ACCESS_TOOLS, } from "./renderer.js";
|
|
4
|
+
export { createToolView, formatStatusGlyph, formatToolHeaderText, isCollapsible, ToolView, transitionFoldLevel, } from "./tool-view.js";
|
|
5
|
+
export { clearCapturedNestedCalls, getCapturedNestedCalls, recordToolExecutionEnd, recordToolExecutionStart, } from "./tracker.js";
|
|
4
6
|
export { formatTurnFlow, } from "./turn-copy.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Theme, type ToolRenderers } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type Component, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { ToolPresentationProvider } from "@hheei/pi-ext-core";
|
|
4
|
+
import { type NestedCallRecord, type ToolRenderContext } from "./renderer.js";
|
|
5
|
+
import { type Collapsible } from "./tool-view.js";
|
|
6
|
+
import type { CapturedToolCall } from "./tracker.js";
|
|
7
|
+
export declare function extractChildToolCopyText(call: CapturedToolCall | NestedCallRecord, full?: boolean): string;
|
|
8
|
+
export interface NestedCallsViewProps {
|
|
9
|
+
readonly resolvePresentation?: ((toolName: string) => ToolPresentationProvider | undefined) | undefined;
|
|
10
|
+
readonly calls: readonly (CapturedToolCall | NestedCallRecord)[];
|
|
11
|
+
readonly theme: Theme;
|
|
12
|
+
readonly context: ToolRenderContext;
|
|
13
|
+
readonly resolveChildRenderer?: ((toolName: string) => ToolRenderers | undefined) | undefined;
|
|
14
|
+
readonly copyToClipboard?: ((text: string) => Promise<void>) | undefined;
|
|
15
|
+
readonly stateHolder?: Record<string, unknown> | undefined;
|
|
16
|
+
readonly invalidate?: (() => void) | undefined;
|
|
17
|
+
}
|
|
18
|
+
export declare const NESTED_CALLS_EXPANDED_KEY = "__pi_ext_ui_nested_expanded__";
|
|
19
|
+
export declare const NESTED_CALLS_COPIED_KEY = "__pi_ext_ui_nested_copied__";
|
|
20
|
+
export declare class NestedCallsView implements Component, Collapsible {
|
|
21
|
+
#private;
|
|
22
|
+
constructor(props: NestedCallsViewProps);
|
|
23
|
+
render(width: number): string[];
|
|
24
|
+
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined;
|
|
25
|
+
dispose(): void;
|
|
26
|
+
collapseAll(): void;
|
|
27
|
+
invalidate(): void;
|
|
28
|
+
}
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { copyToClipboard as copyToSystemClipboard, getLanguageFromPath, highlightCode, } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { stripTerminalSequences, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
|
|
3
|
+
import { inlinePresentation } from "./renderer.js";
|
|
4
|
+
import { formatToolHeaderText, ToolView, toContentRows, transitionFoldLevel, } from "./tool-view.js";
|
|
5
|
+
function truncate(text, width) {
|
|
6
|
+
return truncateToWidth(text, Math.max(1, width));
|
|
7
|
+
}
|
|
8
|
+
function isRecord(value) {
|
|
9
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
export function extractChildToolCopyText(call, full) {
|
|
12
|
+
const toolName = "toolName" in call ? call.toolName : call.name;
|
|
13
|
+
const result = "result" in call ? call.result : undefined;
|
|
14
|
+
const args = "args" in call ? call.args : undefined;
|
|
15
|
+
if (toolName === "write" && isRecord(args) && typeof args.content === "string") {
|
|
16
|
+
return args.content;
|
|
17
|
+
}
|
|
18
|
+
if (toolName === "apply_patch" && isRecord(args) && typeof args.patch === "string") {
|
|
19
|
+
return args.patch;
|
|
20
|
+
}
|
|
21
|
+
const heavyCommand = toolName === "bash" && isRecord(args) && typeof args.command === "string"
|
|
22
|
+
? args.command
|
|
23
|
+
: (toolName === "python_eval" || toolName === "codemode") &&
|
|
24
|
+
isRecord(args) &&
|
|
25
|
+
typeof args.code === "string"
|
|
26
|
+
? args.code
|
|
27
|
+
: undefined;
|
|
28
|
+
if (heavyCommand !== undefined && heavyCommand.trim() !== "") {
|
|
29
|
+
if (full !== true)
|
|
30
|
+
return heavyCommand;
|
|
31
|
+
const output = resultText(result);
|
|
32
|
+
return output === undefined ? heavyCommand : `${heavyCommand}\n---\n${output}`;
|
|
33
|
+
}
|
|
34
|
+
if (toolName === "edit" && result && isRecord(result.details)) {
|
|
35
|
+
const editView = result.details.__piExtToolsEditView;
|
|
36
|
+
if (isRecord(editView) && Array.isArray(editView.ops)) {
|
|
37
|
+
const opsText = editView.ops
|
|
38
|
+
.map((op) => isRecord(op) && typeof op.newContent === "string" ? op.newContent : "")
|
|
39
|
+
.filter(Boolean)
|
|
40
|
+
.join("\n---\n");
|
|
41
|
+
if (opsText)
|
|
42
|
+
return opsText;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (result && Array.isArray(result.content)) {
|
|
46
|
+
const texts = result.content
|
|
47
|
+
.filter((c) => isRecord(c) && c.type === "text" && typeof c.text === "string")
|
|
48
|
+
.map((c) => c.text);
|
|
49
|
+
if (texts.length > 0)
|
|
50
|
+
return texts.join("\n");
|
|
51
|
+
}
|
|
52
|
+
if (args !== undefined) {
|
|
53
|
+
if (typeof args === "string")
|
|
54
|
+
return args;
|
|
55
|
+
try {
|
|
56
|
+
return JSON.stringify(args, null, 2);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return String(args);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if ("summary" in call && typeof call.summary === "string") {
|
|
63
|
+
return call.summary;
|
|
64
|
+
}
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
function resultText(result) {
|
|
68
|
+
if (result === undefined || !Array.isArray(result.content))
|
|
69
|
+
return undefined;
|
|
70
|
+
const texts = result.content
|
|
71
|
+
.filter((c) => isRecord(c) && c.type === "text" && typeof c.text === "string")
|
|
72
|
+
.map((c) => c.text);
|
|
73
|
+
return texts.length === 0 ? undefined : texts.join("\n");
|
|
74
|
+
}
|
|
75
|
+
function formatDuration(ms) {
|
|
76
|
+
if (ms === undefined || !Number.isFinite(ms))
|
|
77
|
+
return "";
|
|
78
|
+
return ms < 1000 ? `${String(Math.round(ms))}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
79
|
+
}
|
|
80
|
+
export const NESTED_CALLS_EXPANDED_KEY = "__pi_ext_ui_nested_expanded__";
|
|
81
|
+
export const NESTED_CALLS_COPIED_KEY = "__pi_ext_ui_nested_copied__";
|
|
82
|
+
export class NestedCallsView {
|
|
83
|
+
#props;
|
|
84
|
+
#inMemoryLevels = new Map();
|
|
85
|
+
#copiedIndex = undefined;
|
|
86
|
+
#copiedTimer = undefined;
|
|
87
|
+
#lineMapping = [];
|
|
88
|
+
#renderCache;
|
|
89
|
+
constructor(props) {
|
|
90
|
+
this.#props = props;
|
|
91
|
+
const state = props.stateHolder;
|
|
92
|
+
if (state !== undefined) {
|
|
93
|
+
const stored = state[NESTED_CALLS_EXPANDED_KEY];
|
|
94
|
+
if (isRecord(stored)) {
|
|
95
|
+
for (const [k, v] of Object.entries(stored)) {
|
|
96
|
+
const idx = Number(k);
|
|
97
|
+
if (!Number.isNaN(idx) && (v === 1 || v === 2)) {
|
|
98
|
+
this.#inMemoryLevels.set(idx, v);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else if (Array.isArray(stored)) {
|
|
103
|
+
for (const item of stored) {
|
|
104
|
+
if (typeof item === "number") {
|
|
105
|
+
this.#inMemoryLevels.set(item, 1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const copiedState = state[NESTED_CALLS_COPIED_KEY];
|
|
110
|
+
if (isRecord(copiedState) &&
|
|
111
|
+
typeof copiedState.index === "number" &&
|
|
112
|
+
typeof copiedState.until === "number") {
|
|
113
|
+
const remaining = copiedState.until - Date.now();
|
|
114
|
+
if (remaining > 0) {
|
|
115
|
+
this.#copiedIndex = copiedState.index;
|
|
116
|
+
this.#copiedTimer = setTimeout(() => {
|
|
117
|
+
this.#copiedIndex = undefined;
|
|
118
|
+
this.#renderCache = undefined;
|
|
119
|
+
const s = this.#props.stateHolder;
|
|
120
|
+
if (s !== undefined) {
|
|
121
|
+
delete s[NESTED_CALLS_COPIED_KEY];
|
|
122
|
+
}
|
|
123
|
+
this.#props.invalidate?.();
|
|
124
|
+
}, remaining);
|
|
125
|
+
this.#copiedTimer.unref?.();
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
delete state[NESTED_CALLS_COPIED_KEY];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
#getExpansionLevel(index) {
|
|
134
|
+
const state = this.#props.stateHolder;
|
|
135
|
+
if (state !== undefined) {
|
|
136
|
+
const stored = state[NESTED_CALLS_EXPANDED_KEY];
|
|
137
|
+
if (isRecord(stored)) {
|
|
138
|
+
const val = stored[String(index)];
|
|
139
|
+
if (val === 1 || val === 2)
|
|
140
|
+
return val;
|
|
141
|
+
}
|
|
142
|
+
else if (Array.isArray(stored)) {
|
|
143
|
+
if (stored.includes(index))
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return this.#inMemoryLevels.get(index) ?? 0;
|
|
148
|
+
}
|
|
149
|
+
#getLevelsKey() {
|
|
150
|
+
const parts = [];
|
|
151
|
+
for (let i = 0; i < this.#props.calls.length; i++) {
|
|
152
|
+
const level = this.#getExpansionLevel(i);
|
|
153
|
+
if (level !== 0)
|
|
154
|
+
parts.push(`${i}:${level}`);
|
|
155
|
+
}
|
|
156
|
+
return parts.join(",");
|
|
157
|
+
}
|
|
158
|
+
#setExpansionLevel(index, level) {
|
|
159
|
+
if (level === 0) {
|
|
160
|
+
this.#inMemoryLevels.delete(index);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
this.#inMemoryLevels.set(index, level);
|
|
164
|
+
}
|
|
165
|
+
this.#renderCache = undefined;
|
|
166
|
+
const state = this.#props.stateHolder;
|
|
167
|
+
if (state !== undefined) {
|
|
168
|
+
const record = {};
|
|
169
|
+
for (const [k, v] of this.#inMemoryLevels.entries()) {
|
|
170
|
+
record[String(k)] = v;
|
|
171
|
+
}
|
|
172
|
+
state[NESTED_CALLS_EXPANDED_KEY] = record;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
#triggerCopy(callIndex, full) {
|
|
176
|
+
const call = this.#props.calls[callIndex];
|
|
177
|
+
if (!call)
|
|
178
|
+
return;
|
|
179
|
+
const provider = this.#props.resolvePresentation?.(call.toolName);
|
|
180
|
+
const presentation = provider?.("args" in call ? call.args : undefined, "result" in call ? call.result : undefined, this.#props.theme);
|
|
181
|
+
const text = stripTerminalSequences((full ? presentation?.copy?.full : presentation?.copy?.primary) ??
|
|
182
|
+
extractChildToolCopyText(call, full));
|
|
183
|
+
if (!text)
|
|
184
|
+
return;
|
|
185
|
+
const writeClip = this.#props.copyToClipboard ?? copyToSystemClipboard;
|
|
186
|
+
void writeClip(text).catch(() => { });
|
|
187
|
+
const durationMs = 1500;
|
|
188
|
+
const until = Date.now() + durationMs;
|
|
189
|
+
this.#copiedIndex = callIndex;
|
|
190
|
+
this.#renderCache = undefined;
|
|
191
|
+
const state = this.#props.stateHolder;
|
|
192
|
+
if (state !== undefined) {
|
|
193
|
+
state[NESTED_CALLS_COPIED_KEY] = {
|
|
194
|
+
index: callIndex,
|
|
195
|
+
until,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (this.#copiedTimer !== undefined) {
|
|
199
|
+
clearTimeout(this.#copiedTimer);
|
|
200
|
+
}
|
|
201
|
+
this.#copiedTimer = setTimeout(() => {
|
|
202
|
+
this.#copiedIndex = undefined;
|
|
203
|
+
this.#renderCache = undefined;
|
|
204
|
+
const s = this.#props.stateHolder;
|
|
205
|
+
if (s !== undefined) {
|
|
206
|
+
delete s[NESTED_CALLS_COPIED_KEY];
|
|
207
|
+
}
|
|
208
|
+
this.#props.invalidate?.();
|
|
209
|
+
}, durationMs);
|
|
210
|
+
this.#copiedTimer.unref?.();
|
|
211
|
+
this.#props.invalidate?.();
|
|
212
|
+
}
|
|
213
|
+
render(width) {
|
|
214
|
+
const safeWidth = Math.max(1, width);
|
|
215
|
+
const levelsKey = this.#getLevelsKey();
|
|
216
|
+
if (this.#renderCache !== undefined &&
|
|
217
|
+
this.#renderCache.width === safeWidth &&
|
|
218
|
+
this.#renderCache.copiedIndex === this.#copiedIndex &&
|
|
219
|
+
this.#renderCache.levelsKey === levelsKey) {
|
|
220
|
+
this.#lineMapping = this.#renderCache.lineMapping;
|
|
221
|
+
return this.#renderCache.lines;
|
|
222
|
+
}
|
|
223
|
+
this.#lineMapping = [];
|
|
224
|
+
const { calls, theme } = this.#props;
|
|
225
|
+
const lines = [];
|
|
226
|
+
const rightMargin = 1;
|
|
227
|
+
for (let i = 0; i < calls.length; i++) {
|
|
228
|
+
const call = calls[i];
|
|
229
|
+
if (!call)
|
|
230
|
+
continue;
|
|
231
|
+
const toolName = "toolName" in call ? call.toolName : call.name;
|
|
232
|
+
const isError = Boolean(call.isError || ("status" in call && call.status === "error"));
|
|
233
|
+
const isRunning = ("status" in call && call.status === "running") ||
|
|
234
|
+
(!("status" in call) && call.result === undefined);
|
|
235
|
+
const expansionLevel = this.#getExpansionLevel(i);
|
|
236
|
+
const isExpanded = expansionLevel !== 0;
|
|
237
|
+
const rawSummary = "summary" in call ? call.summary : undefined;
|
|
238
|
+
const rawArgs = "args" in call ? call.args : undefined;
|
|
239
|
+
const inline = inlinePresentation(toolName, rawArgs ?? rawSummary, "result" in call ? call.result?.details : undefined, isError);
|
|
240
|
+
const argText = typeof rawArgs === "string" ? rawArgs : typeof rawSummary === "string" ? rawSummary : "";
|
|
241
|
+
const provider = this.#props.resolvePresentation?.(toolName);
|
|
242
|
+
const presentation = provider?.(rawArgs, "result" in call ? call.result : undefined, theme);
|
|
243
|
+
// The child row is the child's own header minus the fold pin, so it carries the same token
|
|
244
|
+
// assignment as the row the same call would draw at top level.
|
|
245
|
+
const childSummary = stripTerminalSequences(presentation?.summary ?? inline?.summary ?? argText);
|
|
246
|
+
const duration = formatDuration(call.durationMs);
|
|
247
|
+
const leftContent = formatToolHeaderText({
|
|
248
|
+
status: isRunning ? "running" : isError ? "error" : "ok",
|
|
249
|
+
title: toolName,
|
|
250
|
+
summary: [childSummary, duration].filter(Boolean).join(" · "),
|
|
251
|
+
}, theme);
|
|
252
|
+
const isCopied = this.#copiedIndex === i;
|
|
253
|
+
const copyIcon = isCopied ? theme.fg("success", "✓") : theme.fg("dim", "");
|
|
254
|
+
const copySlotWidth = 2 + rightMargin;
|
|
255
|
+
const maxLeft = Math.max(1, width - copySlotWidth);
|
|
256
|
+
const truncatedLeft = truncate(leftContent, maxLeft);
|
|
257
|
+
const leftVis = visibleWidth(truncatedLeft);
|
|
258
|
+
const gap = " ".repeat(Math.max(1, width - leftVis - rightMargin - 1));
|
|
259
|
+
const headerLine = truncate(truncatedLeft + gap + copyIcon + " ".repeat(rightMargin), width);
|
|
260
|
+
const headerLineIdx = lines.length;
|
|
261
|
+
lines.push(headerLine);
|
|
262
|
+
this.#lineMapping[headerLineIdx] = {
|
|
263
|
+
callIndex: i,
|
|
264
|
+
isHeader: true,
|
|
265
|
+
copyRegion: { startX: width - copySlotWidth - 1, endX: width },
|
|
266
|
+
};
|
|
267
|
+
if (isExpanded) {
|
|
268
|
+
const bodyLines = this.#renderChildBody(call, Math.max(1, width - 2), expansionLevel);
|
|
269
|
+
for (const rawLine of bodyLines) {
|
|
270
|
+
for (const bodyLine of toContentRows(rawLine)) {
|
|
271
|
+
const bodyLineIdx = lines.length;
|
|
272
|
+
lines.push(truncate(` ${bodyLine}`, width));
|
|
273
|
+
this.#lineMapping[bodyLineIdx] = {
|
|
274
|
+
callIndex: i,
|
|
275
|
+
isHeader: false,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
this.#renderCache = {
|
|
282
|
+
width: safeWidth,
|
|
283
|
+
copiedIndex: this.#copiedIndex,
|
|
284
|
+
levelsKey,
|
|
285
|
+
lines,
|
|
286
|
+
lineMapping: this.#lineMapping,
|
|
287
|
+
};
|
|
288
|
+
return lines;
|
|
289
|
+
}
|
|
290
|
+
#renderChildBody(call, width, expansionLevel = 1) {
|
|
291
|
+
const { theme, context, resolveChildRenderer } = this.#props;
|
|
292
|
+
const toolName = "toolName" in call ? call.toolName : call.name;
|
|
293
|
+
const result = "result" in call ? call.result : undefined;
|
|
294
|
+
const args = "args" in call ? call.args : undefined;
|
|
295
|
+
if (result && resolveChildRenderer) {
|
|
296
|
+
const renderer = resolveChildRenderer(toolName);
|
|
297
|
+
if (renderer?.renderResult) {
|
|
298
|
+
try {
|
|
299
|
+
const childContext = {
|
|
300
|
+
...context,
|
|
301
|
+
toolCallId: "toolCallId" in call ? call.toolCallId : context.toolCallId,
|
|
302
|
+
args,
|
|
303
|
+
durationMs: call.durationMs,
|
|
304
|
+
isError: Boolean(call.isError || ("status" in call && call.status === "error")),
|
|
305
|
+
state: undefined,
|
|
306
|
+
};
|
|
307
|
+
const comp = renderer.renderResult(result, { expanded: expansionLevel === 2, isPartial: false }, theme, childContext);
|
|
308
|
+
if (comp instanceof ToolView) {
|
|
309
|
+
comp.setFoldLevel(expansionLevel);
|
|
310
|
+
// Render body cleanly via deep headless interface without outer rail or header row
|
|
311
|
+
return comp.renderBody(width, { noRail: true });
|
|
312
|
+
}
|
|
313
|
+
return comp.render(width);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
// Fall through to plain text extraction on renderer failure
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// Fallback rendering
|
|
321
|
+
if (result && Array.isArray(result.content)) {
|
|
322
|
+
const text = result.content
|
|
323
|
+
.filter((c) => isRecord(c) && c.type === "text" && typeof c.text === "string")
|
|
324
|
+
.map((c) => c.text)
|
|
325
|
+
.join("\n")
|
|
326
|
+
.trim();
|
|
327
|
+
if (text) {
|
|
328
|
+
let lines;
|
|
329
|
+
const filePath = typeof args === "object" &&
|
|
330
|
+
args !== null &&
|
|
331
|
+
"path" in args &&
|
|
332
|
+
typeof args.path === "string"
|
|
333
|
+
? args.path
|
|
334
|
+
: "";
|
|
335
|
+
const lang = filePath ? getLanguageFromPath(filePath) : undefined;
|
|
336
|
+
if (lang) {
|
|
337
|
+
try {
|
|
338
|
+
lines = highlightCode(text, lang);
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
lines = text.split("\n");
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
lines = text.split("\n");
|
|
346
|
+
}
|
|
347
|
+
if (expansionLevel === 2 || lines.length <= 20) {
|
|
348
|
+
return lines;
|
|
349
|
+
}
|
|
350
|
+
return [...lines.slice(0, 20), `... (${lines.length - 20} more lines)`];
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if ("error" in call && typeof call.error === "string" && call.error) {
|
|
354
|
+
return call.error.split("\n").map((l) => theme.fg("error", l));
|
|
355
|
+
}
|
|
356
|
+
if (args !== undefined) {
|
|
357
|
+
const text = typeof args === "string" ? args : JSON.stringify(args, null, 2);
|
|
358
|
+
return text.split("\n").map((l) => theme.fg("dim", l));
|
|
359
|
+
}
|
|
360
|
+
return [theme.fg("dim", "(no output)")];
|
|
361
|
+
}
|
|
362
|
+
handleMouse(event) {
|
|
363
|
+
if (event.type !== "click" || event.button !== "left")
|
|
364
|
+
return undefined;
|
|
365
|
+
const mapping = this.#lineMapping[event.y];
|
|
366
|
+
if (!mapping)
|
|
367
|
+
return { handled: true };
|
|
368
|
+
if (mapping.isHeader) {
|
|
369
|
+
if (mapping.copyRegion && event.x >= mapping.copyRegion.startX) {
|
|
370
|
+
this.#triggerCopy(mapping.callIndex, event.shift === true);
|
|
371
|
+
return { handled: true, render: true };
|
|
372
|
+
}
|
|
373
|
+
const isCtrl = event.ctrl === true;
|
|
374
|
+
const currentLevel = this.#getExpansionLevel(mapping.callIndex);
|
|
375
|
+
const nextLevel = transitionFoldLevel(currentLevel, isCtrl);
|
|
376
|
+
this.#setExpansionLevel(mapping.callIndex, nextLevel);
|
|
377
|
+
this.#props.invalidate?.();
|
|
378
|
+
return { handled: true, render: true };
|
|
379
|
+
}
|
|
380
|
+
return { handled: true };
|
|
381
|
+
}
|
|
382
|
+
dispose() {
|
|
383
|
+
if (this.#copiedTimer !== undefined) {
|
|
384
|
+
clearTimeout(this.#copiedTimer);
|
|
385
|
+
this.#copiedTimer = undefined;
|
|
386
|
+
}
|
|
387
|
+
this.#renderCache = undefined;
|
|
388
|
+
}
|
|
389
|
+
collapseAll() {
|
|
390
|
+
if (this.#copiedTimer !== undefined) {
|
|
391
|
+
clearTimeout(this.#copiedTimer);
|
|
392
|
+
this.#copiedTimer = undefined;
|
|
393
|
+
}
|
|
394
|
+
this.#inMemoryLevels.clear();
|
|
395
|
+
const state = this.#props.stateHolder;
|
|
396
|
+
if (state !== undefined) {
|
|
397
|
+
state[NESTED_CALLS_EXPANDED_KEY] = {};
|
|
398
|
+
}
|
|
399
|
+
this.#lineMapping = [];
|
|
400
|
+
this.#renderCache = undefined;
|
|
401
|
+
}
|
|
402
|
+
invalidate() {
|
|
403
|
+
this.#lineMapping = [];
|
|
404
|
+
this.#renderCache = undefined;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type DiffLine, type DiffSource } from "./diff-view.js";
|
|
3
|
+
import type { ToolStatusKind } from "./tool-view.js";
|
|
4
|
+
export declare const APPLY_PATCH_TOOL_NAME = "apply_patch";
|
|
5
|
+
export declare const PYTHON_EVAL_TOOL_NAME = "python_eval";
|
|
6
|
+
export declare const PYTHON_EVAL_DETAILS_FORMAT = "pi-ext-tools-python-eval";
|
|
7
|
+
export declare const EDIT_TOOL_NAME = "edit";
|
|
8
|
+
export declare const WRITE_TOOL_NAME = "write";
|
|
9
|
+
export declare const EDIT_VIEW_KEY = "__piExtToolsEditView";
|
|
10
|
+
export declare const WRITE_VIEW_KEY = "__piExtToolsWriteView";
|
|
11
|
+
export declare const CODEMODE_OPTIONS_PREFIX = "// @options:";
|
|
12
|
+
export declare const CODEMODE_SCRIPT_HEADER: RegExp;
|
|
13
|
+
export interface NestedCallRecord {
|
|
14
|
+
readonly toolName: string;
|
|
15
|
+
readonly summary?: string | undefined;
|
|
16
|
+
readonly status?: "running" | "ok" | "error" | "cancelled" | undefined;
|
|
17
|
+
readonly isError?: boolean | undefined;
|
|
18
|
+
readonly isWarning?: boolean | undefined;
|
|
19
|
+
readonly durationMs?: number | undefined;
|
|
20
|
+
readonly cost?: number | undefined;
|
|
21
|
+
readonly error?: string | undefined;
|
|
22
|
+
}
|
|
23
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
24
|
+
export declare function estimateStreamingTokens(args: unknown): number;
|
|
25
|
+
export declare function formatDurationMs(ms: number | undefined): string;
|
|
26
|
+
export declare function formatCostUsd(cost: number | undefined): string;
|
|
27
|
+
/** One header fact for a deadline, in the same shape codemode's `timeout_ms` renders as. */
|
|
28
|
+
export declare function formatTimeoutFact(timeoutMs: number): string;
|
|
29
|
+
export declare function extractCodemodeHeaderFacts(args: unknown): string;
|
|
30
|
+
export declare function parseCallArgs(raw: unknown): Record<string, unknown> | undefined;
|
|
31
|
+
export declare function extractSummaryArg(toolName: string, args: unknown): string;
|
|
32
|
+
export declare function extractHeavyInputCommand(toolName: string, args: unknown): string | undefined;
|
|
33
|
+
export declare function extractNestedCalls(result: AgentToolResult<unknown> | undefined): readonly NestedCallRecord[];
|
|
34
|
+
export declare function extractResultText(toolName: string, result: AgentToolResult<unknown>): {
|
|
35
|
+
readonly outputText: string;
|
|
36
|
+
readonly wallTimeSeconds: number | undefined;
|
|
37
|
+
};
|
|
38
|
+
export declare function callStatsParts(total: number, failCount: number): string[];
|
|
39
|
+
export declare function resolveContainerStatus(params: {
|
|
40
|
+
readonly isError: boolean;
|
|
41
|
+
readonly isPartial: boolean;
|
|
42
|
+
readonly hasChildren: boolean;
|
|
43
|
+
readonly selfFailed: boolean;
|
|
44
|
+
}): ToolStatusKind;
|
|
45
|
+
export declare function toolPath(args: unknown): string;
|
|
46
|
+
export declare function editPath(args: unknown): string;
|
|
47
|
+
export declare function numericField(value: unknown, key: string): number | undefined;
|
|
48
|
+
export type WritePresentation = {
|
|
49
|
+
readonly kind: "diff";
|
|
50
|
+
readonly source: DiffSource;
|
|
51
|
+
readonly added: number;
|
|
52
|
+
readonly removed: number;
|
|
53
|
+
} | {
|
|
54
|
+
readonly kind: "content";
|
|
55
|
+
readonly label: string;
|
|
56
|
+
readonly content: string;
|
|
57
|
+
} | {
|
|
58
|
+
readonly kind: "noChange";
|
|
59
|
+
};
|
|
60
|
+
export declare function writeStoredDiff(lines: readonly unknown[]): DiffSource | undefined;
|
|
61
|
+
/** Uses only the baseline and content already stored by the write tool. */
|
|
62
|
+
export declare function writePresentation(args: unknown, details: unknown): WritePresentation | undefined;
|
|
63
|
+
/** Compact nested-row result: a one-line summary plus the status it reports. */
|
|
64
|
+
export interface InlinePresentation {
|
|
65
|
+
readonly summary: string;
|
|
66
|
+
readonly status: ToolStatusKind;
|
|
67
|
+
}
|
|
68
|
+
export declare function fileToolPresentation(toolName: string, args: unknown, details: unknown, isError: boolean): InlinePresentation | undefined;
|
|
69
|
+
/** Pi native edit persists a unified patch plus the same change as a numbered display diff. */
|
|
70
|
+
export declare function editDiffSource(details: unknown): DiffSource | undefined;
|
|
71
|
+
export declare function displayDiffSource(diff: string): DiffSource | undefined;
|
|
72
|
+
export declare function editViewSource(view: unknown): DiffSource | undefined;
|
|
73
|
+
export declare function editViewHunks(view: unknown): readonly {
|
|
74
|
+
readonly lines: readonly DiffLine[];
|
|
75
|
+
}[];
|
|
76
|
+
export declare function hunksForEditOp(op: Record<string, unknown>): readonly {
|
|
77
|
+
readonly lines: readonly DiffLine[];
|
|
78
|
+
}[];
|
|
79
|
+
export declare function splitFragment(text: string): readonly string[];
|
|
80
|
+
/** Line LCS of one stored before/after fragment. No file context is invented. */
|
|
81
|
+
export declare function lineHunks(before: string, after: string, startLine: number): readonly {
|
|
82
|
+
readonly lines: readonly DiffLine[];
|
|
83
|
+
}[];
|
|
84
|
+
export declare function patchTextForCopy(details: unknown): string | undefined;
|
|
85
|
+
export declare function patchSummary(files: number, added: number, removed: number): string;
|
|
86
|
+
/**
|
|
87
|
+
* One compact nested summary for a tool call. Adopts the tool's standard collapsed
|
|
88
|
+
* single-line format when recognized, degrading to undefined so callers can use
|
|
89
|
+
* the default argument rendering.
|
|
90
|
+
*/
|
|
91
|
+
export declare function inlinePresentation(toolName: string, args: unknown, details: unknown, isError: boolean): InlinePresentation | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* One row of the eval cell transcript: printed text, display value, nested call, the final
|
|
94
|
+
* value, or the kernel's namespace report.
|
|
95
|
+
*/
|
|
96
|
+
export type EvalRow = {
|
|
97
|
+
readonly kind: "text";
|
|
98
|
+
readonly text: string;
|
|
99
|
+
} | {
|
|
100
|
+
readonly kind: "display";
|
|
101
|
+
readonly text: string;
|
|
102
|
+
} | {
|
|
103
|
+
readonly kind: "tool";
|
|
104
|
+
readonly name: string;
|
|
105
|
+
readonly args: unknown;
|
|
106
|
+
readonly durationMs: number | undefined;
|
|
107
|
+
readonly error: string | undefined;
|
|
108
|
+
} | {
|
|
109
|
+
readonly kind: "result";
|
|
110
|
+
readonly text: string;
|
|
111
|
+
} | {
|
|
112
|
+
readonly kind: "state";
|
|
113
|
+
readonly text: string;
|
|
114
|
+
};
|
|
115
|
+
export interface EvalFacts {
|
|
116
|
+
readonly rows: readonly EvalRow[];
|
|
117
|
+
readonly error: string | undefined;
|
|
118
|
+
}
|
|
119
|
+
/** Reads only the persisted `pi-ext-tools-eval` details; other shapes degrade to plain output. */
|
|
120
|
+
export declare function pythonEvalFacts(details: unknown): EvalFacts | undefined;
|