pi-pretty-tui 0.2.9 → 0.3.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 +52 -15
- package/README.zh-CN.md +52 -15
- package/extensions/ansi-style.ts +29 -0
- package/extensions/card-surface.ts +38 -0
- package/extensions/full-frame.ts +145 -0
- package/extensions/index.ts +437 -259
- package/extensions/nested-tools.ts +59 -13
- package/extensions/selection-ranges.ts +245 -0
- package/extensions/shell-highlight.ts +36 -0
- package/extensions/thought-presentation.ts +38 -0
- package/extensions/tool-presentation.ts +292 -0
- package/package.json +10 -4
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { highlightNativeShellCall } from "./shell-highlight.js";
|
|
2
|
+
import { isNativeImageRow, stripAnsiBackgrounds } from "./ansi-style.js";
|
|
3
|
+
import { cardMargin, frameNativeRows, frameNativeToolRows, nativeFrameWidth } from "./full-frame.js";
|
|
4
|
+
import { Spacer, Text, sliceByColumn, stripTerminalSequences, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
5
|
+
import type { NestedTools } from "./nested-tools.js";
|
|
6
|
+
|
|
7
|
+
// Tool content and hit regions are independent of activity grouping. The host
|
|
8
|
+
// decides whether to decorate this projection with a clean activity tree.
|
|
9
|
+
export const SPECIALIZED_TOOL_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls"]);
|
|
10
|
+
// A third-party renderer can also use renderShell: "self". The shell style is
|
|
11
|
+
// not proof that it is one of our specialized built-in renderers.
|
|
12
|
+
export const THIRD_PARTY_TOOL = Symbol("pretty-tui.third-party-tool");
|
|
13
|
+
export const isThirdPartyTool = (owner: any): boolean =>
|
|
14
|
+
Boolean(owner.toolDefinition?.[THIRD_PARTY_TOOL]) ||
|
|
15
|
+
!SPECIALIZED_TOOL_NAMES.has(owner.toolName) || owner.toolDefinition?.renderShell !== "self";
|
|
16
|
+
|
|
17
|
+
export const toolLabel = (toolName: string, label?: string): string => label || toolName;
|
|
18
|
+
|
|
19
|
+
const conciseThirdPartyArgs = (args: any): string => {
|
|
20
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return "";
|
|
21
|
+
const sensitive = /(?:token|secret|password|authorization|credential|api[_-]?key)/iu;
|
|
22
|
+
const preferred = ["path", "query", "url", "offset", "id"];
|
|
23
|
+
const entries = Object.entries(args)
|
|
24
|
+
.filter(([key, value]) => !sensitive.test(key) && ["string", "number", "boolean"].includes(typeof value))
|
|
25
|
+
.sort(([left], [right]) => {
|
|
26
|
+
const leftRank = preferred.indexOf(left);
|
|
27
|
+
const rightRank = preferred.indexOf(right);
|
|
28
|
+
return (leftRank < 0 ? preferred.length : leftRank) -
|
|
29
|
+
(rightRank < 0 ? preferred.length : rightRank);
|
|
30
|
+
})
|
|
31
|
+
.slice(0, 2)
|
|
32
|
+
.map(([key, value]) => {
|
|
33
|
+
const safeValue = String(value)
|
|
34
|
+
.replace(/(https?:\/\/)[^\s/@:]+:[^\s/@]+@/giu, "$1…:…@")
|
|
35
|
+
.replace(/([?&](?:token|secret|password|api[_-]?key)=)[^&\s]+/giu, "$1…");
|
|
36
|
+
// Argument previews are plain text. The truncator emits SGR resets even
|
|
37
|
+
// for unstyled input; remove those before applying the shared muted role.
|
|
38
|
+
const rendered = stripTerminalSequences(truncateToWidth(stripTerminalSequences(safeValue), 36, "…"));
|
|
39
|
+
return `${key}=${rendered}`;
|
|
40
|
+
});
|
|
41
|
+
return entries.join(" · ");
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const thirdPartyResultSummary = (component: any): string => {
|
|
45
|
+
const detailsError = component.result?.details?.error;
|
|
46
|
+
const firstText = component.result?.content?.find(
|
|
47
|
+
(item: any) => item?.type === "text" && typeof item.text === "string" && item.text.length > 0,
|
|
48
|
+
)?.text;
|
|
49
|
+
const text = String(typeof detailsError === "string" ? detailsError : firstText ?? "");
|
|
50
|
+
const boundedText = text.slice(0, 512);
|
|
51
|
+
const newline = boundedText.indexOf("\n");
|
|
52
|
+
const boundedFirstLine = boundedText.slice(0, newline < 0 ? boundedText.length : newline);
|
|
53
|
+
const firstLine = stripTerminalSequences(boundedFirstLine).trim();
|
|
54
|
+
if (firstLine) return stripTerminalSequences(truncateToWidth(firstLine, 72, "…"));
|
|
55
|
+
if (component.result) return component.result.isError ? "Failed" : "Done";
|
|
56
|
+
return component.executionStarted ? "Running…" : "Pending";
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const renderThirdPartyCompact = (component: any, width: number, theme: any): string[] => {
|
|
60
|
+
const label = toolLabel(component.toolName, component.toolDefinition?.label);
|
|
61
|
+
const args = conciseThirdPartyArgs(component.args);
|
|
62
|
+
const dotColor = component.result?.isError
|
|
63
|
+
? "error"
|
|
64
|
+
: component.result && !component.isPartial
|
|
65
|
+
? "success"
|
|
66
|
+
: "dim";
|
|
67
|
+
const title = theme.fg(dotColor, "● ") +
|
|
68
|
+
theme.fg("text", theme.bold(label)) +
|
|
69
|
+
(args ? theme.fg("muted", `(${args})`) : "");
|
|
70
|
+
const result = thirdPartyResultSummary(component);
|
|
71
|
+
const resultColor = component.result?.isError ? "error" : "muted";
|
|
72
|
+
return [
|
|
73
|
+
truncateToWidth(title, Math.max(1, width), "…"),
|
|
74
|
+
theme.fg(resultColor, truncateToWidth(` └ ${result}`, Math.max(1, width), "…")),
|
|
75
|
+
];
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type ToolProjection = {
|
|
79
|
+
lines: string[];
|
|
80
|
+
actions: Map<number, string>;
|
|
81
|
+
toggleRows: Set<number>;
|
|
82
|
+
// Coordinates of the original renderer inside the projection. sourceY
|
|
83
|
+
// accounts for Pi's spacer, removed only by compact/clean presentation.
|
|
84
|
+
native?: { x: number; y: number; width: number; height: number; sourceY: number };
|
|
85
|
+
sourceRows?: Map<number, number>;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Native shell/codemode timers are distinct final renderer components. Locate
|
|
89
|
+
// that component, never a matching line in user output, before moving it.
|
|
90
|
+
const nativeTimerRegion = (owner: any, resultStart: number, contentWidth: number, width: number, rows: string[]) => {
|
|
91
|
+
if (!["bash", "powershell", "codemode"].includes(owner.toolName)) return undefined;
|
|
92
|
+
const state = owner.rendererState?.nativeFull?.state ?? owner.rendererState;
|
|
93
|
+
if (typeof state?.startedAt !== "number" && typeof state?.endedAt !== "number") return undefined;
|
|
94
|
+
const children: any[] | undefined = owner.resultRendererComponent?.children;
|
|
95
|
+
if (!children?.length) return undefined;
|
|
96
|
+
const tail = children[children.length - 1];
|
|
97
|
+
if (!(tail instanceof Text) || typeof (tail as any).text !== "string") return undefined;
|
|
98
|
+
const label = stripTerminalSequences((tail as any).text).trim();
|
|
99
|
+
if (!/^(?:Took|Elapsed) \d+(?:\.\d+)?[smh](?: \d+(?:\.\d+)?[smh])*$/.test(label) || visibleWidth(label) + 6 > width) return undefined;
|
|
100
|
+
let first = children.length - 1;
|
|
101
|
+
if (children[first - 1] instanceof Spacer) first--;
|
|
102
|
+
const height = (items: any[]) => items.reduce((sum, item) => sum + item.render(contentWidth).length, 0);
|
|
103
|
+
const start = resultStart + height(children.slice(0, first));
|
|
104
|
+
const end = resultStart + height(children);
|
|
105
|
+
if (rows.slice(start, end).map(stripTerminalSequences).join("\n").trim() !== label) return undefined;
|
|
106
|
+
return { label, start, end };
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
function renderToolContent(
|
|
110
|
+
owner: any,
|
|
111
|
+
width: number,
|
|
112
|
+
theme: any,
|
|
113
|
+
options: {
|
|
114
|
+
mode: "full" | "compact";
|
|
115
|
+
nested: NestedTools;
|
|
116
|
+
nestedExpanded: boolean;
|
|
117
|
+
panels?: boolean;
|
|
118
|
+
cardTitle?: string;
|
|
119
|
+
renderNative: (width: number) => string[];
|
|
120
|
+
},
|
|
121
|
+
): ToolProjection | undefined {
|
|
122
|
+
const actions = new Map<number, string>();
|
|
123
|
+
const toggleRows = new Set<number>();
|
|
124
|
+
if (options.mode === "full") {
|
|
125
|
+
const innerWidth = nativeFrameWidth(width);
|
|
126
|
+
highlightNativeShellCall(owner, theme);
|
|
127
|
+
const nativeLines = options.renderNative(innerWidth);
|
|
128
|
+
if (!nativeLines.length) return { lines: [], actions, toggleRows };
|
|
129
|
+
const self = owner.toolDefinition?.renderShell === "self";
|
|
130
|
+
const shell = self ? owner.selfRenderContainer : owner.contentBox;
|
|
131
|
+
const call = shell?.children?.[0];
|
|
132
|
+
const callWidth = self ? innerWidth : Math.max(1, innerWidth - (owner.outputPad ?? 1) * 2);
|
|
133
|
+
const callStart = self ? 1 : 2;
|
|
134
|
+
const callHeight = call?.render(callWidth).length ?? 0;
|
|
135
|
+
const resultBoundaries: number[] = [];
|
|
136
|
+
if (!owner.toolDefinition && typeof owner.formatToolExecution === "function") {
|
|
137
|
+
// Unknown historical tools use Pi's generic Text component. Measure the
|
|
138
|
+
// native prefix rather than reconstructing or parsing the displayed args.
|
|
139
|
+
const output = owner.getTextOutput();
|
|
140
|
+
const formatted = owner.formatToolExecution();
|
|
141
|
+
if (output && formatted.endsWith("\n" + output)) {
|
|
142
|
+
const prefix = formatted.slice(0, -output.length - 1);
|
|
143
|
+
const resultY = new Text(prefix, owner.outputPad ?? 1, 1).render(innerWidth).length;
|
|
144
|
+
resultBoundaries.push(resultY);
|
|
145
|
+
} else if (owner.imageComponents?.length && owner.contentText) {
|
|
146
|
+
// With image display enabled, the generic text output may be empty;
|
|
147
|
+
// the native image spacers and payload follow the generic text region.
|
|
148
|
+
resultBoundaries.push(1 + owner.contentText.render(innerWidth).length);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Native edit puts its authoritative diff in the call component so a
|
|
152
|
+
// pre-execution preview can update in place. Split the displayed rows at
|
|
153
|
+
// that region without rebuilding or duplicating its content.
|
|
154
|
+
const preview = owner.callRendererComponent;
|
|
155
|
+
if ((typeof preview?.preview?.diff === "string" || typeof preview?.preview?.error === "string") && preview.children?.length >= 3) {
|
|
156
|
+
const previewWidth = Math.max(1, callWidth - (preview.paddingX ?? 0) * 2);
|
|
157
|
+
const diffStart = callStart + (preview.paddingY ?? 0) +
|
|
158
|
+
preview.children.slice(0, 2).reduce((sum: number, child: any) => sum + child.render(previewWidth).length, 0);
|
|
159
|
+
resultBoundaries.push(diffStart);
|
|
160
|
+
}
|
|
161
|
+
if (owner.result && shell?.children?.length > 1) {
|
|
162
|
+
const resultStart = callStart + callHeight;
|
|
163
|
+
if (resultStart < nativeLines.length) resultBoundaries.push(resultStart);
|
|
164
|
+
}
|
|
165
|
+
// Only split a real result. Native edit previews before execution stay in
|
|
166
|
+
// the call card; empty/collapsed results must not create an empty card.
|
|
167
|
+
const timer = nativeTimerRegion(owner, callStart + callHeight, callWidth, width - cardMargin(width) * 2, nativeLines);
|
|
168
|
+
const sourceIndexes = nativeLines.map((_, y) => y).filter((y) => !timer || y < timer.start || y >= timer.end);
|
|
169
|
+
const displayed = sourceIndexes.map((y) => nativeLines[y]);
|
|
170
|
+
const candidates = owner.result ? resultBoundaries.sort((a, b) => a - b)
|
|
171
|
+
.map((start) => sourceIndexes.findIndex((y) => y >= start)) : [];
|
|
172
|
+
const resultStart = candidates.find((start) => start > 0 && start < displayed.length &&
|
|
173
|
+
displayed.slice(start).some((line) => stripTerminalSequences(line).trim() ||
|
|
174
|
+
line.includes("\x1b_G") || line.includes("\x1b]1337;File=")));
|
|
175
|
+
const frame = frameNativeToolRows(
|
|
176
|
+
options.cardTitle ?? toolLabel(owner.toolName, owner.toolDefinition?.label), displayed, width, theme,
|
|
177
|
+
resultStart, owner.result?.isError ? "error" : owner.isPartial ? "muted" : "success", owner, timer?.label,
|
|
178
|
+
);
|
|
179
|
+
const sourceRows = new Map([...frame.sourceRows].map(([y, original]) => [y, sourceIndexes[original]]));
|
|
180
|
+
if (timer) sourceRows.set(frame.lines.length - 1, timer.start);
|
|
181
|
+
return {
|
|
182
|
+
lines: frame.lines, actions, toggleRows, sourceRows,
|
|
183
|
+
native: { x: frame.inset, y: 0, width: innerWidth, height: nativeLines.length, sourceY: 0 },
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
if (options.nested.count(owner.toolCallId) > 0) {
|
|
187
|
+
return { ...options.nested.render({ ...owner, expanded: options.nestedExpanded }, width, theme), toggleRows };
|
|
188
|
+
}
|
|
189
|
+
if (!isThirdPartyTool(owner)) return undefined;
|
|
190
|
+
const summary = renderThirdPartyCompact(owner, width, theme);
|
|
191
|
+
toggleRows.add(0);
|
|
192
|
+
if (!owner.expanded) {
|
|
193
|
+
toggleRows.add(1);
|
|
194
|
+
return { lines: summary, actions, toggleRows };
|
|
195
|
+
}
|
|
196
|
+
const prefix = " │ ";
|
|
197
|
+
const nativeWidth = Math.max(1, width - visibleWidth(prefix));
|
|
198
|
+
const nativeLines = options.renderNative(nativeWidth);
|
|
199
|
+
const sourceY = nativeLines[0] === "" ? 1 : 0;
|
|
200
|
+
const content = nativeLines.slice(sourceY);
|
|
201
|
+
return {
|
|
202
|
+
lines: [summary[0], ...content.map((line, i) => isNativeImageRow(line) ? line :
|
|
203
|
+
truncateToWidth(
|
|
204
|
+
theme.fg("dim", i === content.length - 1 ? " └ " : prefix) + stripAnsiBackgrounds(line),
|
|
205
|
+
Math.max(1, width), "",
|
|
206
|
+
))],
|
|
207
|
+
actions, toggleRows,
|
|
208
|
+
native: { x: visibleWidth(prefix), y: 1, width: nativeWidth, height: content.length, sourceY },
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Compact shares panel geometry with full, retaining its concise projection. */
|
|
213
|
+
export function renderToolProjection(
|
|
214
|
+
owner: any, width: number, theme: any,
|
|
215
|
+
options: Parameters<typeof renderToolContent>[3],
|
|
216
|
+
): ToolProjection | undefined {
|
|
217
|
+
if (options.mode === "full" || options.panels === false) return renderToolContent(owner, width, theme, options);
|
|
218
|
+
if (owner.expanded && !options.nested.count(owner.toolCallId)) {
|
|
219
|
+
const projection = renderToolContent(owner, width, theme, { ...options, mode: "full", cardTitle: `${toolLabel(owner.toolName, owner.toolDefinition?.label)} ▾` });
|
|
220
|
+
if (projection) {
|
|
221
|
+
projection.toggleRows.add(0);
|
|
222
|
+
if (width < 8) projection.lines = projection.lines.map((line) => isNativeImageRow(line) ? line : truncateToWidth(line, Math.max(1, width), ""));
|
|
223
|
+
}
|
|
224
|
+
return projection;
|
|
225
|
+
}
|
|
226
|
+
const innerWidth = nativeFrameWidth(width);
|
|
227
|
+
let content = renderToolContent(owner, innerWidth, theme, options);
|
|
228
|
+
if (!content) {
|
|
229
|
+
const raw = options.renderNative(innerWidth);
|
|
230
|
+
if (!raw.length) return { lines: [], actions: new Map(), toggleRows: new Set() };
|
|
231
|
+
const sourceY = raw[0] === "" ? 1 : 0;
|
|
232
|
+
content = { lines: raw.slice(sourceY), actions: new Map(), toggleRows: new Set(),
|
|
233
|
+
native: { x: 0, y: 0, width: innerWidth, height: raw.length - sourceY, sourceY } };
|
|
234
|
+
}
|
|
235
|
+
const sections = new Map<number, { label: string; color: string }>();
|
|
236
|
+
const native = content.native;
|
|
237
|
+
if (native && owner.result && owner.expanded) {
|
|
238
|
+
const self = owner.toolDefinition?.renderShell === "self";
|
|
239
|
+
const call = (self ? owner.selfRenderContainer : owner.contentBox)?.children?.[0];
|
|
240
|
+
if (call) {
|
|
241
|
+
const callWidth = self ? native.width : Math.max(1, native.width - (owner.outputPad ?? 1) * 2);
|
|
242
|
+
const start = native.y + (self ? 1 : 2) + call.render(callWidth).length - native.sourceY;
|
|
243
|
+
if (start < content.lines.length) sections.set(start, { label: "Output", color: owner.result.isError ? "error" : "success" });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const color = owner.result?.isError ? "error" : owner.result && !owner.isPartial ? "success" : "accent";
|
|
247
|
+
const duration = !owner.isPartial && Number.isFinite(owner.result?.durationMs) ? owner.result.durationMs : undefined;
|
|
248
|
+
const seconds = duration === undefined ? undefined : Math.max(0, duration) / 1000;
|
|
249
|
+
const footer = seconds === undefined ? "" : seconds < 60 ? `Took ${seconds.toFixed(1)}s`
|
|
250
|
+
: seconds < 3600 ? `Took ${Math.floor(seconds / 60)}m ${Math.floor(seconds % 60)}s`
|
|
251
|
+
: `Took ${Math.floor(seconds / 3600)}h ${Math.floor(seconds / 60) % 60}m ${Math.floor(seconds % 60)}s`;
|
|
252
|
+
const body = content.lines.slice();
|
|
253
|
+
if (!owner.expanded && body.length) {
|
|
254
|
+
const label = toolLabel(owner.toolName, owner.toolDefinition?.label);
|
|
255
|
+
const plain = stripTerminalSequences(body[0]);
|
|
256
|
+
const prefix = `● ${label}(`;
|
|
257
|
+
if (plain.startsWith(prefix) && plain.endsWith(")")) {
|
|
258
|
+
const start = visibleWidth(prefix);
|
|
259
|
+
body[0] = (owner.toolName === "bash" ? theme.fg("toolTitle", "$ ") : "") +
|
|
260
|
+
sliceByColumn(body[0], start, Math.max(0, visibleWidth(plain) - start - 1));
|
|
261
|
+
} else if (plain === `● ${label}`) {
|
|
262
|
+
body[0] = theme.fg("muted", "No arguments");
|
|
263
|
+
}
|
|
264
|
+
for (let row = 1; row < body.length; row++) {
|
|
265
|
+
const result = /^(\s*└\s*)/.exec(stripTerminalSequences(body[row]));
|
|
266
|
+
if (result) {
|
|
267
|
+
const prefixWidth = visibleWidth(result[1]);
|
|
268
|
+
const status = owner.result?.isError ? "✕ " : owner.result && !owner.isPartial ? "✓ " : "· ";
|
|
269
|
+
body[row] = theme.fg(color === "accent" ? "dim" : color, status) +
|
|
270
|
+
sliceByColumn(body[row], prefixWidth, Math.max(0, visibleWidth(body[row]) - prefixWidth));
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
const collapsible = !options.nested.count(owner.toolCallId);
|
|
276
|
+
const title = toolLabel(owner.toolName, owner.toolDefinition?.label) + (collapsible ? ` ${owner.expanded ? "▾" : "▸"}` : "");
|
|
277
|
+
const frame = frameNativeRows(title, body, width, theme,
|
|
278
|
+
sections, owner, color, footer, color, color === "error" ? "toolErrorBg" : "toolPendingBg");
|
|
279
|
+
const actions = new Map<number, string>();
|
|
280
|
+
const toggleRows = new Set<number>();
|
|
281
|
+
const sourceRows = new Map<number, number>();
|
|
282
|
+
for (const [y, source] of frame.sourceRows) {
|
|
283
|
+
const action = content.actions.get(source);
|
|
284
|
+
if (action) actions.set(y, action);
|
|
285
|
+
if (content.toggleRows.has(source)) toggleRows.add(y);
|
|
286
|
+
if (native && source >= native.y && source < native.y + native.height) sourceRows.set(y, source - native.y + native.sourceY);
|
|
287
|
+
}
|
|
288
|
+
if (!options.nested.count(owner.toolCallId)) toggleRows.add(0);
|
|
289
|
+
const lines = width < 8 ? frame.lines.map((line) => isNativeImageRow(line) ? line : truncateToWidth(line, Math.max(1, width), "")) : frame.lines;
|
|
290
|
+
return { lines, actions, toggleRows, sourceRows: native ? sourceRows : undefined,
|
|
291
|
+
native: native ? { ...native, x: frame.inset + native.x, y: 0 } : undefined };
|
|
292
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-pretty-tui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "A beautiful, polished TUI for the Pi coding agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Kainan Yang <ykn0309@whu.edu.cn>",
|
|
@@ -15,7 +15,13 @@
|
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"typecheck": "tsc -p tsconfig.json",
|
|
18
|
-
"test": "npm run typecheck && node tests/regression.mjs"
|
|
18
|
+
"test": "npm run typecheck && node tests/regression.mjs && node tests/coexistence.mjs && node tests/selection-ranges.mjs && node tests/selection-options.mjs",
|
|
19
|
+
"test:selection-pty": "python3 tests/selection-pty.py",
|
|
20
|
+
"bench:selection": "node tests/selection-benchmark.mjs",
|
|
21
|
+
"test:selection-cli": "python3 tests/selection-cli.py",
|
|
22
|
+
"bench:selection-frame": "node tests/selection-frame-benchmark.mjs",
|
|
23
|
+
"bench:selection-memory": "node --expose-gc tests/selection-memory-benchmark.mjs",
|
|
24
|
+
"test:selection-gc": "node --expose-gc tests/selection-gc.mjs && node --expose-gc tests/selection-reload-gc.mjs"
|
|
19
25
|
},
|
|
20
26
|
"keywords": [
|
|
21
27
|
"pi-package",
|
|
@@ -39,8 +45,8 @@
|
|
|
39
45
|
"@earendil-works/pi-tui": "*"
|
|
40
46
|
},
|
|
41
47
|
"devDependencies": {
|
|
42
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
43
|
-
"@earendil-works/pi-tui": "1.
|
|
48
|
+
"@earendil-works/pi-coding-agent": "1.4.0",
|
|
49
|
+
"@earendil-works/pi-tui": "1.4.0",
|
|
44
50
|
"@types/node": "^24.0.0",
|
|
45
51
|
"jiti": "^2.7.0",
|
|
46
52
|
"typescript": "^5.9.0"
|