@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/dist/tool-view.d.ts
CHANGED
|
@@ -12,28 +12,69 @@ export interface ToolViewHeaderSpec {
|
|
|
12
12
|
readonly title: string;
|
|
13
13
|
readonly status?: ToolStatusKind | undefined;
|
|
14
14
|
readonly summary?: string | undefined;
|
|
15
|
+
readonly unstyledSummary?: boolean | undefined;
|
|
15
16
|
}
|
|
16
17
|
export interface ToolViewCopySpec {
|
|
17
18
|
readonly primary: string;
|
|
18
19
|
readonly full?: string | undefined;
|
|
19
20
|
}
|
|
21
|
+
/** Micro-contract for child views that can reset their internal expansion. */
|
|
22
|
+
export interface Collapsible {
|
|
23
|
+
collapseAll(): void;
|
|
24
|
+
}
|
|
25
|
+
export declare function isCollapsible(value: unknown): value is Collapsible;
|
|
20
26
|
export interface ToolViewSpec {
|
|
21
27
|
readonly theme: Theme;
|
|
22
28
|
readonly header: ToolViewHeaderSpec;
|
|
23
|
-
readonly collapsedRows?: readonly string[] | Component | undefined;
|
|
29
|
+
readonly collapsedRows?: readonly string[] | Component | (Component & Collapsible) | undefined;
|
|
24
30
|
readonly sections?: readonly RailSection[] | undefined;
|
|
31
|
+
readonly collapsibleChildren?: readonly Collapsible[] | undefined;
|
|
25
32
|
readonly copy?: ToolViewCopySpec | undefined;
|
|
26
33
|
readonly stateHolder?: unknown;
|
|
27
34
|
readonly outputPad?: number | undefined;
|
|
28
35
|
readonly defaultLevel?: FoldLevel | undefined;
|
|
36
|
+
readonly controlledLevel?: FoldLevel | undefined;
|
|
29
37
|
readonly globalExpanded?: boolean | undefined;
|
|
30
38
|
readonly invalidate?: (() => void) | undefined;
|
|
31
39
|
readonly copyToClipboard?: ((text: string) => Promise<void>) | undefined;
|
|
32
40
|
readonly copyFeedbackDurationMs?: number | undefined;
|
|
41
|
+
readonly noRail?: boolean | undefined;
|
|
42
|
+
readonly railToken?: RailColorToken | undefined;
|
|
33
43
|
}
|
|
34
44
|
/** Public composition entry point for extensions that need the shared view behavior. */
|
|
35
45
|
export declare function createToolView(spec: ToolViewSpec): ToolView;
|
|
36
46
|
export declare function transitionFoldLevel(current: FoldLevel, ctrlKey: boolean): FoldLevel;
|
|
47
|
+
export declare function formatStatusGlyph(status: ToolStatusKind | undefined, theme: Theme): string;
|
|
48
|
+
/**
|
|
49
|
+
* The glyph, name, and summary of one tool execution, colored the way every tool row colors them.
|
|
50
|
+
*
|
|
51
|
+
* A container tool draws one of these per child call, and so does the row of a tool that ran on its
|
|
52
|
+
* own. Both must agree, so the token assignment lives here rather than in each board: a board that
|
|
53
|
+
* assembled its own row painted the child's summary as a title, which read as one bright run and
|
|
54
|
+
* made a nested call look unlike the same call at top level. The caller owns the fold pin, the
|
|
55
|
+
* right-aligned action, and the width budget, which are the parts that actually differ.
|
|
56
|
+
*/
|
|
57
|
+
export declare function formatToolHeaderText(parts: {
|
|
58
|
+
readonly status?: ToolStatusKind | undefined;
|
|
59
|
+
readonly title: string;
|
|
60
|
+
readonly summary?: string | undefined;
|
|
61
|
+
readonly unstyledSummary?: boolean | undefined;
|
|
62
|
+
}, theme: Theme): string;
|
|
63
|
+
/**
|
|
64
|
+
* Collapses text that must occupy one row: header titles and summaries, board rows, and inline
|
|
65
|
+
* labels. A break inside one of them is a contract violation, because Pi writes one array element
|
|
66
|
+
* as one terminal row: `\n` moves to the next row without clearing it, and a lone `\r` returns to
|
|
67
|
+
* column 0 so later text overwrites the row's start. `visibleWidth` counts both as zero-width, so
|
|
68
|
+
* the host's per-row width guard cannot catch either, and the frame would silently grow taller
|
|
69
|
+
* than the rows it reports.
|
|
70
|
+
*/
|
|
71
|
+
export declare function toSingleRow(text: string): string;
|
|
72
|
+
/**
|
|
73
|
+
* Splits multi-line content into rows. A lone `\r` is dropped rather than treated as a break so
|
|
74
|
+
* CRLF and progress-style output stay cell-safe, and tabs expand to two spaces so indentation does
|
|
75
|
+
* not depend on the terminal's tab stops.
|
|
76
|
+
*/
|
|
77
|
+
export declare function toContentRows(text: string): readonly string[];
|
|
37
78
|
export declare class ToolView implements Component {
|
|
38
79
|
#private;
|
|
39
80
|
constructor(spec: ToolViewSpec);
|
|
@@ -45,9 +86,15 @@ export declare class ToolView implements Component {
|
|
|
45
86
|
toggle(ctrlKey?: boolean): void;
|
|
46
87
|
/** The result renderer of the same tool execution hides this row in place. */
|
|
47
88
|
hide(): void;
|
|
89
|
+
/**
|
|
90
|
+
* Renders only the interior content of the view without the header row.
|
|
91
|
+
* When `noRail: true` is specified, outer rail glyphs are omitted, allowing
|
|
92
|
+
* clean embedding inside composite parent components without regex text scraping.
|
|
93
|
+
*/
|
|
94
|
+
renderBody(width: number, options?: {
|
|
95
|
+
noRail?: boolean;
|
|
96
|
+
}): string[];
|
|
48
97
|
render(width: number): string[];
|
|
49
98
|
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined;
|
|
50
99
|
invalidate(): void;
|
|
51
100
|
}
|
|
52
|
-
export type { ToolViewSpec as CollapsibleToolFrameOptions };
|
|
53
|
-
export { ToolView as CollapsibleToolFrame };
|
package/dist/tool-view.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { copyToClipboard as copyToSystemClipboard, } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
|
|
3
|
+
export function isCollapsible(value) {
|
|
4
|
+
return (typeof value === "object" &&
|
|
5
|
+
value !== null &&
|
|
6
|
+
"collapseAll" in value &&
|
|
7
|
+
typeof value.collapseAll === "function");
|
|
8
|
+
}
|
|
3
9
|
/** Public composition entry point for extensions that need the shared view behavior. */
|
|
4
10
|
export function createToolView(spec) {
|
|
5
11
|
return new ToolView(spec);
|
|
@@ -8,7 +14,6 @@ const FOLD_LEVEL_STATE_KEY = "__piExtUiFoldLevel";
|
|
|
8
14
|
const COPIED_UNTIL_STATE_KEY = "__piExtUiCopiedUntil";
|
|
9
15
|
const COPY_FEEDBACK_TIMER_KEY = "__piExtUiCopyTimer";
|
|
10
16
|
const DEFAULT_MAX_SECTION_LINES = 9;
|
|
11
|
-
const DEFAULT_MAX_COLLAPSED_ROWS = 6;
|
|
12
17
|
const DEFAULT_COPY_FEEDBACK_MS = 1200;
|
|
13
18
|
function isRecord(value) {
|
|
14
19
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -42,31 +47,80 @@ function isCopiedActive(stateHolder, localCopiedUntil) {
|
|
|
42
47
|
const until = stateHolder[COPIED_UNTIL_STATE_KEY];
|
|
43
48
|
return typeof until === "number" && until > now;
|
|
44
49
|
}
|
|
45
|
-
function formatStatusGlyph(status, theme) {
|
|
50
|
+
export function formatStatusGlyph(status, theme) {
|
|
46
51
|
switch (status ?? "ok") {
|
|
47
52
|
case "streaming":
|
|
48
|
-
return
|
|
53
|
+
return theme.fg("muted", "⋯");
|
|
49
54
|
case "running":
|
|
50
|
-
return
|
|
55
|
+
return theme.fg("accent", "◐");
|
|
51
56
|
case "ok":
|
|
52
|
-
return
|
|
57
|
+
return theme.fg("success", "✓");
|
|
53
58
|
case "warn":
|
|
54
|
-
return
|
|
59
|
+
return theme.fg("warning", "!");
|
|
55
60
|
case "error":
|
|
56
|
-
return
|
|
61
|
+
return theme.fg("error", "✗");
|
|
57
62
|
case "none":
|
|
58
63
|
return "";
|
|
59
64
|
}
|
|
60
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* The glyph, name, and summary of one tool execution, colored the way every tool row colors them.
|
|
68
|
+
*
|
|
69
|
+
* A container tool draws one of these per child call, and so does the row of a tool that ran on its
|
|
70
|
+
* own. Both must agree, so the token assignment lives here rather than in each board: a board that
|
|
71
|
+
* assembled its own row painted the child's summary as a title, which read as one bright run and
|
|
72
|
+
* made a nested call look unlike the same call at top level. The caller owns the fold pin, the
|
|
73
|
+
* right-aligned action, and the width budget, which are the parts that actually differ.
|
|
74
|
+
*/
|
|
75
|
+
export function formatToolHeaderText(parts, theme) {
|
|
76
|
+
const statusPart = formatStatusGlyph(parts.status, theme);
|
|
77
|
+
const title = toSingleRow(parts.title);
|
|
78
|
+
const titlePart = theme.fg("toolTitle", theme.bold(title));
|
|
79
|
+
const summaryRaw = parts.summary === undefined ? "" : toSingleRow(parts.summary);
|
|
80
|
+
const isUnstyled = parts.unstyledSummary === true || title === "bash";
|
|
81
|
+
const summaryPart = summaryRaw === "" ? "" : isUnstyled ? ` ${summaryRaw}` : ` ${theme.fg("dim", summaryRaw)}`;
|
|
82
|
+
return `${statusPart === "" ? "" : `${statusPart} `}${titlePart}${summaryPart}`;
|
|
83
|
+
}
|
|
61
84
|
const truncate = (text, width) => truncateToWidth(text, width, "…");
|
|
62
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Collapses text that must occupy one row: header titles and summaries, board rows, and inline
|
|
87
|
+
* labels. A break inside one of them is a contract violation, because Pi writes one array element
|
|
88
|
+
* as one terminal row: `\n` moves to the next row without clearing it, and a lone `\r` returns to
|
|
89
|
+
* column 0 so later text overwrites the row's start. `visibleWidth` counts both as zero-width, so
|
|
90
|
+
* the host's per-row width guard cannot catch either, and the frame would silently grow taller
|
|
91
|
+
* than the rows it reports.
|
|
92
|
+
*/
|
|
93
|
+
export function toSingleRow(text) {
|
|
94
|
+
if (!text.includes("\r") && !text.includes("\n") && !text.includes("\t")) {
|
|
95
|
+
return text.trim();
|
|
96
|
+
}
|
|
97
|
+
return text.replace(/[\r\n\t]+/g, " ").trim();
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Splits multi-line content into rows. A lone `\r` is dropped rather than treated as a break so
|
|
101
|
+
* CRLF and progress-style output stay cell-safe, and tabs expand to two spaces so indentation does
|
|
102
|
+
* not depend on the terminal's tab stops.
|
|
103
|
+
*/
|
|
104
|
+
export function toContentRows(text) {
|
|
105
|
+
if (!text.includes("\r") && !text.includes("\t") && !text.includes("\n")) {
|
|
106
|
+
return [text];
|
|
107
|
+
}
|
|
108
|
+
return text.replace(/\r/g, "").replace(/\t/g, " ").split("\n");
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Resolves a row source into rows that are safe to hand to the host. A string element is one
|
|
112
|
+
* caller-provided label, so `normalize` decides whether it stays a single row or becomes several. A
|
|
113
|
+
* component returns rows it already laid out, so only its breaks are removed and its indentation is
|
|
114
|
+
* preserved.
|
|
115
|
+
*/
|
|
116
|
+
function resolveRows(source, width, normalize) {
|
|
63
117
|
if (typeof source === "string") {
|
|
64
|
-
return source === "" ? [] : source
|
|
118
|
+
return source === "" ? [] : normalize(source);
|
|
65
119
|
}
|
|
66
120
|
if (Array.isArray(source)) {
|
|
67
|
-
return source.flatMap((item) => typeof item === "string" ? item
|
|
121
|
+
return source.flatMap((item) => (typeof item === "string" ? normalize(item) : []));
|
|
68
122
|
}
|
|
69
|
-
return source.render(width);
|
|
123
|
+
return source.render(width).flatMap((row) => toContentRows(row));
|
|
70
124
|
}
|
|
71
125
|
export class ToolView {
|
|
72
126
|
#spec;
|
|
@@ -74,12 +128,42 @@ export class ToolView {
|
|
|
74
128
|
#localCopiedUntil = 0;
|
|
75
129
|
#localTimer;
|
|
76
130
|
#lastWidth = 80;
|
|
131
|
+
#lastHeight = 0;
|
|
132
|
+
#cache;
|
|
77
133
|
constructor(spec) {
|
|
78
134
|
this.#spec = spec;
|
|
79
|
-
const initial = spec.defaultLevel ?? 0;
|
|
80
|
-
this.#localLevel =
|
|
135
|
+
const initial = spec.controlledLevel ?? spec.defaultLevel ?? 0;
|
|
136
|
+
this.#localLevel =
|
|
137
|
+
spec.controlledLevel !== undefined
|
|
138
|
+
? spec.controlledLevel
|
|
139
|
+
: readFoldLevel(spec.stateHolder, initial);
|
|
140
|
+
if (isRecord(spec.stateHolder)) {
|
|
141
|
+
const until = spec.stateHolder[COPIED_UNTIL_STATE_KEY];
|
|
142
|
+
if (typeof until === "number" && until > Date.now()) {
|
|
143
|
+
this.#localCopiedUntil = until;
|
|
144
|
+
const remaining = until - Date.now();
|
|
145
|
+
if (!spec.stateHolder[COPY_FEEDBACK_TIMER_KEY]) {
|
|
146
|
+
const timer = setTimeout(() => {
|
|
147
|
+
this.#localCopiedUntil = 0;
|
|
148
|
+
if (isRecord(this.#spec.stateHolder)) {
|
|
149
|
+
this.#spec.stateHolder[COPIED_UNTIL_STATE_KEY] = 0;
|
|
150
|
+
this.#spec.stateHolder[COPY_FEEDBACK_TIMER_KEY] = undefined;
|
|
151
|
+
}
|
|
152
|
+
this.#localTimer = undefined;
|
|
153
|
+
this.#cache = undefined;
|
|
154
|
+
this.#spec.invalidate?.();
|
|
155
|
+
}, remaining);
|
|
156
|
+
timer.unref?.();
|
|
157
|
+
this.#localTimer = timer;
|
|
158
|
+
spec.stateHolder[COPY_FEEDBACK_TIMER_KEY] = timer;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
81
162
|
}
|
|
82
163
|
get foldLevel() {
|
|
164
|
+
if (this.#spec.controlledLevel !== undefined) {
|
|
165
|
+
return this.#spec.controlledLevel;
|
|
166
|
+
}
|
|
83
167
|
return readFoldLevel(this.#spec.stateHolder, this.#localLevel);
|
|
84
168
|
}
|
|
85
169
|
get effectiveLevel() {
|
|
@@ -93,11 +177,26 @@ export class ToolView {
|
|
|
93
177
|
get collapsed() {
|
|
94
178
|
return this.foldLevel === 0;
|
|
95
179
|
}
|
|
180
|
+
#collapseChildren() {
|
|
181
|
+
const { collapsedRows, collapsibleChildren } = this.#spec;
|
|
182
|
+
if (collapsibleChildren !== undefined) {
|
|
183
|
+
for (const child of collapsibleChildren) {
|
|
184
|
+
child.collapseAll();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (isCollapsible(collapsedRows)) {
|
|
188
|
+
collapsedRows.collapseAll();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
96
191
|
setFoldLevel(next) {
|
|
97
192
|
const prev = this.foldLevel;
|
|
98
193
|
this.#localLevel = next;
|
|
99
|
-
|
|
194
|
+
if (this.#spec.controlledLevel === undefined) {
|
|
195
|
+
writeFoldLevel(this.#spec.stateHolder, next);
|
|
196
|
+
}
|
|
100
197
|
if (prev !== next) {
|
|
198
|
+
this.#cache = undefined;
|
|
199
|
+
this.#collapseChildren();
|
|
101
200
|
this.#spec.invalidate?.();
|
|
102
201
|
}
|
|
103
202
|
}
|
|
@@ -137,6 +236,7 @@ export class ToolView {
|
|
|
137
236
|
this.#spec.stateHolder[COPY_FEEDBACK_TIMER_KEY] = undefined;
|
|
138
237
|
}
|
|
139
238
|
this.#localTimer = undefined;
|
|
239
|
+
this.#cache = undefined;
|
|
140
240
|
this.#spec.invalidate?.();
|
|
141
241
|
}, durationMs);
|
|
142
242
|
timer.unref?.();
|
|
@@ -150,11 +250,7 @@ export class ToolView {
|
|
|
150
250
|
const safeWidth = Math.max(1, width);
|
|
151
251
|
const pinGlyph = level === 0 ? "▸" : "▾";
|
|
152
252
|
const pin = theme.fg("dim", pinGlyph);
|
|
153
|
-
const
|
|
154
|
-
const titlePart = theme.fg("toolTitle", theme.bold(header.title));
|
|
155
|
-
const summaryRaw = header.summary?.trim() ?? "";
|
|
156
|
-
const summaryPart = summaryRaw === "" ? "" : ` ${theme.fg("dim", summaryRaw)}`;
|
|
157
|
-
const leftContent = `${pin} ${statusPart}${titlePart}${summaryPart}`;
|
|
253
|
+
const leftContent = `${pin} ${formatToolHeaderText(header, theme)}`;
|
|
158
254
|
if (copy === undefined || safeWidth < 6) {
|
|
159
255
|
return truncate(leftContent, safeWidth);
|
|
160
256
|
}
|
|
@@ -173,47 +269,40 @@ export class ToolView {
|
|
|
173
269
|
/** The result renderer of the same tool execution hides this row in place. */
|
|
174
270
|
hide() {
|
|
175
271
|
this.#hidden = true;
|
|
272
|
+
this.#cache = undefined;
|
|
176
273
|
}
|
|
177
|
-
|
|
274
|
+
/**
|
|
275
|
+
* Renders only the interior content of the view without the header row.
|
|
276
|
+
* When `noRail: true` is specified, outer rail glyphs are omitted, allowing
|
|
277
|
+
* clean embedding inside composite parent components without regex text scraping.
|
|
278
|
+
*/
|
|
279
|
+
renderBody(width, options) {
|
|
178
280
|
if (this.#hidden)
|
|
179
281
|
return [];
|
|
180
282
|
const pad = Math.max(0, this.#spec.outputPad ?? 0);
|
|
181
283
|
const safeWidth = Math.max(1, width - pad * 2);
|
|
182
|
-
this.#lastWidth = width;
|
|
183
284
|
const level = this.effectiveLevel;
|
|
184
285
|
const { theme, collapsedRows, sections } = this.#spec;
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
const leftPad = " ".repeat(pad);
|
|
189
|
-
return lines.map((line) => `${leftPad}${line}`);
|
|
190
|
-
}
|
|
191
|
-
return lines;
|
|
192
|
-
};
|
|
193
|
-
const innerWidth = Math.max(1, safeWidth - 2);
|
|
286
|
+
const noRail = options?.noRail ?? this.#spec.noRail ?? false;
|
|
287
|
+
const innerWidth = noRail ? safeWidth : Math.max(1, safeWidth - 2);
|
|
288
|
+
const output = [];
|
|
194
289
|
if (level === 0) {
|
|
195
290
|
if (collapsedRows !== undefined) {
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
: rows;
|
|
201
|
-
for (const row of visibleRows) {
|
|
291
|
+
const railColor = this.#spec.header.status === "error" ? "error" : "dim";
|
|
292
|
+
const rail = noRail ? "" : `${theme.fg(railColor, "▎")} `;
|
|
293
|
+
const rows = resolveRows(collapsedRows, innerWidth, (row) => [toSingleRow(row)]);
|
|
294
|
+
for (const row of rows) {
|
|
202
295
|
output.push(truncate(rail + row, safeWidth));
|
|
203
296
|
}
|
|
204
|
-
if (rows.length > visibleRows.length) {
|
|
205
|
-
const hint = theme.fg("dim", `… (${String(rows.length - visibleRows.length)} more)`);
|
|
206
|
-
output.push(truncate(rail + hint, safeWidth));
|
|
207
|
-
}
|
|
208
297
|
}
|
|
209
|
-
return
|
|
298
|
+
return output;
|
|
210
299
|
}
|
|
211
300
|
if (sections === undefined || sections.length === 0) {
|
|
212
|
-
return
|
|
301
|
+
return output;
|
|
213
302
|
}
|
|
214
303
|
for (const section of sections) {
|
|
215
|
-
const rail = `${theme.fg(section.railToken, "▎")} `;
|
|
216
|
-
const rawLines =
|
|
304
|
+
const rail = noRail ? "" : `${theme.fg(section.railToken, "▎")} `;
|
|
305
|
+
const rawLines = resolveRows(section.content, innerWidth, toContentRows);
|
|
217
306
|
const cap = level === 1 ? (section.maxLines ?? DEFAULT_MAX_SECTION_LINES) : Number.POSITIVE_INFINITY;
|
|
218
307
|
const visibleLines = Number.isFinite(cap) && rawLines.length > cap ? rawLines.slice(0, cap) : rawLines;
|
|
219
308
|
const omittedCount = rawLines.length - visibleLines.length;
|
|
@@ -225,13 +314,48 @@ export class ToolView {
|
|
|
225
314
|
output.push(truncate(rail + hint, safeWidth));
|
|
226
315
|
}
|
|
227
316
|
}
|
|
228
|
-
return
|
|
317
|
+
return output;
|
|
318
|
+
}
|
|
319
|
+
render(width) {
|
|
320
|
+
if (this.#hidden)
|
|
321
|
+
return [];
|
|
322
|
+
const level = this.effectiveLevel;
|
|
323
|
+
const copied = isCopiedActive(this.#spec.stateHolder, this.#localCopiedUntil);
|
|
324
|
+
if (this.#cache !== undefined &&
|
|
325
|
+
this.#cache.width === width &&
|
|
326
|
+
this.#cache.level === level &&
|
|
327
|
+
this.#cache.copied === copied) {
|
|
328
|
+
return this.#cache.lines;
|
|
329
|
+
}
|
|
330
|
+
const pad = Math.max(0, this.#spec.outputPad ?? 0);
|
|
331
|
+
const safeWidth = Math.max(1, width - pad * 2);
|
|
332
|
+
this.#lastWidth = width;
|
|
333
|
+
if (level !== 0) {
|
|
334
|
+
this.#collapseChildren();
|
|
335
|
+
}
|
|
336
|
+
const rawLines = [this.#renderHeader(safeWidth, level), ...this.renderBody(width)];
|
|
337
|
+
const safeLines = [];
|
|
338
|
+
const leftPad = pad > 0 ? " ".repeat(pad) : "";
|
|
339
|
+
for (const rawLine of rawLines) {
|
|
340
|
+
if (rawLine.includes("\r") || rawLine.includes("\n")) {
|
|
341
|
+
for (const line of toContentRows(rawLine)) {
|
|
342
|
+
const truncated = truncate(line, safeWidth);
|
|
343
|
+
safeLines.push(leftPad !== "" ? `${leftPad}${truncated}` : truncated);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
safeLines.push(leftPad !== "" ? `${leftPad}${rawLine}` : rawLine);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
this.#lastHeight = safeLines.length;
|
|
351
|
+
this.#cache = { width, level, copied, lines: safeLines };
|
|
352
|
+
return safeLines;
|
|
229
353
|
}
|
|
230
354
|
handleMouse(event) {
|
|
231
|
-
if (event.type === "wheel") {
|
|
355
|
+
if (event.type === "wheel" || this.#hidden) {
|
|
232
356
|
return undefined;
|
|
233
357
|
}
|
|
234
|
-
if (event.type !== "click" || event.button !== "left"
|
|
358
|
+
if (event.type !== "click" || event.button !== "left") {
|
|
235
359
|
return undefined;
|
|
236
360
|
}
|
|
237
361
|
const effectiveWidth = event.width > 0 ? event.width : this.#lastWidth;
|
|
@@ -241,17 +365,48 @@ export class ToolView {
|
|
|
241
365
|
if (innerX < 0 || innerX >= innerWidth) {
|
|
242
366
|
return undefined;
|
|
243
367
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
368
|
+
// The last render's row count bounds the clickable area; a view that has not rendered yet has no
|
|
369
|
+
// extent to test against, and Pi always renders before it dispatches a mouse event.
|
|
370
|
+
if (this.#lastHeight > 0 && (event.y < 0 || event.y >= this.#lastHeight)) {
|
|
371
|
+
return undefined;
|
|
372
|
+
}
|
|
373
|
+
if (event.y === 0) {
|
|
374
|
+
if (this.#spec.copy !== undefined &&
|
|
375
|
+
innerWidth >= 6 &&
|
|
376
|
+
innerX >= innerWidth - 3 &&
|
|
377
|
+
innerX < innerWidth) {
|
|
378
|
+
this.#triggerCopy(event.shift === true);
|
|
379
|
+
return { handled: true, render: true };
|
|
380
|
+
}
|
|
381
|
+
this.toggle(event.ctrl === true);
|
|
249
382
|
return { handled: true, render: true };
|
|
250
383
|
}
|
|
251
|
-
this.
|
|
252
|
-
|
|
384
|
+
const level = this.effectiveLevel;
|
|
385
|
+
if (level === 0 && this.#spec.collapsedRows !== undefined) {
|
|
386
|
+
const rows = this.#spec.collapsedRows;
|
|
387
|
+
if (typeof rows === "object" &&
|
|
388
|
+
rows !== null &&
|
|
389
|
+
"handleMouse" in rows &&
|
|
390
|
+
typeof rows.handleMouse === "function") {
|
|
391
|
+
const handler = rows.handleMouse;
|
|
392
|
+
const railOffset = this.#spec.noRail ? 0 : 2;
|
|
393
|
+
const subEvent = {
|
|
394
|
+
...event,
|
|
395
|
+
y: event.y - 1,
|
|
396
|
+
x: Math.max(0, innerX - railOffset),
|
|
397
|
+
width: Math.max(1, innerWidth - railOffset),
|
|
398
|
+
};
|
|
399
|
+
const res = handler.call(rows, subEvent);
|
|
400
|
+
if (res?.handled) {
|
|
401
|
+
this.#cache = undefined;
|
|
402
|
+
return res;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return { handled: true };
|
|
253
407
|
}
|
|
254
408
|
invalidate() {
|
|
409
|
+
this.#cache = undefined;
|
|
255
410
|
const { collapsedRows, sections } = this.#spec;
|
|
256
411
|
if (collapsedRows !== undefined &&
|
|
257
412
|
typeof collapsedRows === "object" &&
|
|
@@ -267,4 +422,3 @@ export class ToolView {
|
|
|
267
422
|
}
|
|
268
423
|
}
|
|
269
424
|
}
|
|
270
|
-
export { ToolView as CollapsibleToolFrame };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
export interface CapturedToolCall {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly toolName: string;
|
|
5
|
+
readonly args?: unknown;
|
|
6
|
+
readonly result?: AgentToolResult<unknown> | undefined;
|
|
7
|
+
readonly isError?: boolean | undefined;
|
|
8
|
+
readonly durationMs?: number | undefined;
|
|
9
|
+
readonly cost?: number | undefined;
|
|
10
|
+
}
|
|
11
|
+
export declare function recordToolExecutionStart(event: {
|
|
12
|
+
toolCallId: string;
|
|
13
|
+
toolName: string;
|
|
14
|
+
args: unknown;
|
|
15
|
+
parentToolCallId?: string | undefined;
|
|
16
|
+
}): void;
|
|
17
|
+
export declare function recordToolExecutionEnd(event: {
|
|
18
|
+
toolCallId: string;
|
|
19
|
+
toolName: string;
|
|
20
|
+
result: unknown;
|
|
21
|
+
isError: boolean;
|
|
22
|
+
durationMs?: number | undefined;
|
|
23
|
+
parentToolCallId?: string | undefined;
|
|
24
|
+
}): void;
|
|
25
|
+
export declare function getCapturedNestedCalls(parentToolCallId: string | undefined): readonly CapturedToolCall[] | undefined;
|
|
26
|
+
export declare function clearCapturedNestedCalls(): void;
|
package/dist/tracker.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const capturedByParentId = new Map();
|
|
2
|
+
const MAX_STORED_PARENTS = 200;
|
|
3
|
+
const MAX_CAPTURED_PAYLOAD_CHARS = 32_768;
|
|
4
|
+
function boundCapturedValue(val) {
|
|
5
|
+
if (typeof val === "string") {
|
|
6
|
+
return val.length > MAX_CAPTURED_PAYLOAD_CHARS
|
|
7
|
+
? `${val.slice(0, MAX_CAPTURED_PAYLOAD_CHARS)}… (truncated)`
|
|
8
|
+
: val;
|
|
9
|
+
}
|
|
10
|
+
return val;
|
|
11
|
+
}
|
|
12
|
+
export function recordToolExecutionStart(event) {
|
|
13
|
+
if (!event.parentToolCallId)
|
|
14
|
+
return;
|
|
15
|
+
let list = capturedByParentId.get(event.parentToolCallId);
|
|
16
|
+
if (!list) {
|
|
17
|
+
if (capturedByParentId.size >= MAX_STORED_PARENTS) {
|
|
18
|
+
const firstKey = capturedByParentId.keys().next().value;
|
|
19
|
+
if (firstKey !== undefined)
|
|
20
|
+
capturedByParentId.delete(firstKey);
|
|
21
|
+
}
|
|
22
|
+
list = [];
|
|
23
|
+
capturedByParentId.set(event.parentToolCallId, list);
|
|
24
|
+
}
|
|
25
|
+
const idx = list.findIndex((c) => c.id === event.toolCallId);
|
|
26
|
+
const entry = {
|
|
27
|
+
id: event.toolCallId,
|
|
28
|
+
toolName: event.toolName,
|
|
29
|
+
args: boundCapturedValue(event.args),
|
|
30
|
+
};
|
|
31
|
+
if (idx >= 0) {
|
|
32
|
+
list[idx] = { ...list[idx], ...entry };
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
list.push(entry);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function recordToolExecutionEnd(event) {
|
|
39
|
+
if (!event.parentToolCallId)
|
|
40
|
+
return;
|
|
41
|
+
let list = capturedByParentId.get(event.parentToolCallId);
|
|
42
|
+
if (!list) {
|
|
43
|
+
if (capturedByParentId.size >= MAX_STORED_PARENTS) {
|
|
44
|
+
const firstKey = capturedByParentId.keys().next().value;
|
|
45
|
+
if (firstKey !== undefined)
|
|
46
|
+
capturedByParentId.delete(firstKey);
|
|
47
|
+
}
|
|
48
|
+
list = [];
|
|
49
|
+
capturedByParentId.set(event.parentToolCallId, list);
|
|
50
|
+
}
|
|
51
|
+
const idx = list.findIndex((c) => c.id === event.toolCallId);
|
|
52
|
+
const typedResult = event.result;
|
|
53
|
+
if (idx >= 0) {
|
|
54
|
+
const prev = list[idx];
|
|
55
|
+
if (!prev)
|
|
56
|
+
return;
|
|
57
|
+
list[idx] = {
|
|
58
|
+
...prev,
|
|
59
|
+
result: typedResult,
|
|
60
|
+
isError: event.isError,
|
|
61
|
+
durationMs: event.durationMs,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
list.push({
|
|
66
|
+
id: event.toolCallId,
|
|
67
|
+
toolName: event.toolName,
|
|
68
|
+
result: typedResult,
|
|
69
|
+
isError: event.isError,
|
|
70
|
+
durationMs: event.durationMs,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function getCapturedNestedCalls(parentToolCallId) {
|
|
75
|
+
if (!parentToolCallId)
|
|
76
|
+
return undefined;
|
|
77
|
+
return capturedByParentId.get(parentToolCallId);
|
|
78
|
+
}
|
|
79
|
+
export function clearCapturedNestedCalls() {
|
|
80
|
+
capturedByParentId.clear();
|
|
81
|
+
}
|