@heyhuynhgiabuu/pi-pretty 0.6.26 → 0.6.28
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/CHANGELOG.md +25 -0
- package/README.md +34 -17
- package/dist/config.d.ts +16 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +30 -0
- package/dist/config.js.map +1 -1
- package/dist/fff.d.ts +16 -1
- package/dist/fff.d.ts.map +1 -1
- package/dist/fff.js +139 -33
- package/dist/fff.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +70 -24
- package/dist/index.js.map +1 -1
- package/dist/prompt-editor.d.ts +11 -0
- package/dist/prompt-editor.d.ts.map +1 -0
- package/dist/prompt-editor.js +78 -0
- package/dist/prompt-editor.js.map +1 -0
- package/dist/render.d.ts +9 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +25 -9
- package/dist/render.js.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +79 -32
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/find.d.ts.map +1 -1
- package/dist/tools/find.js +29 -10
- package/dist/tools/find.js.map +1 -1
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +20 -10
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/ls.d.ts.map +1 -1
- package/dist/tools/ls.js +11 -5
- package/dist/tools/ls.js.map +1 -1
- package/dist/tools/metrics.d.ts +6 -1
- package/dist/tools/metrics.d.ts.map +1 -1
- package/dist/tools/metrics.js +21 -8
- package/dist/tools/metrics.js.map +1 -1
- package/dist/tools/read.d.ts +2 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +97 -31
- package/dist/tools/read.js.map +1 -1
- package/dist/types.d.ts +13 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/working-indicator.d.ts +39 -31
- package/dist/working-indicator.d.ts.map +1 -1
- package/dist/working-indicator.js +114 -87
- package/dist/working-indicator.js.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +51 -0
- package/src/fff.ts +158 -29
- package/src/index.ts +77 -24
- package/src/prompt-editor.ts +102 -0
- package/src/render.ts +30 -11
- package/src/tools/bash.ts +100 -34
- package/src/tools/find.ts +39 -17
- package/src/tools/grep.ts +26 -15
- package/src/tools/ls.ts +20 -10
- package/src/tools/metrics.ts +28 -8
- package/src/tools/read.ts +126 -31
- package/src/types.ts +14 -4
- package/src/working-indicator.ts +123 -95
package/src/tools/read.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/* pi-pretty: read tool -- file reading with syntax highlighting and inline image support. */
|
|
2
2
|
|
|
3
|
-
import { basename, dirname } from "node:path";
|
|
3
|
+
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
4
|
import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { getReadmePath } from "@earendil-works/pi-coding-agent";
|
|
5
6
|
import {
|
|
6
7
|
BG_BASE,
|
|
7
8
|
BG_ERROR,
|
|
@@ -14,13 +15,24 @@ import {
|
|
|
14
15
|
termWidth,
|
|
15
16
|
} from "../config.js";
|
|
16
17
|
import { normalizeLineEndings, shortPath } from "../helpers.js";
|
|
17
|
-
import { fillToolBackground, renderFileContent, renderToolError } from "../render.js";
|
|
18
|
+
import { fillToolBackground, fillToolBody, renderFileContent, renderToolError } from "../render.js";
|
|
18
19
|
import { resolveTextCtor } from "../tui-text.js";
|
|
19
20
|
import type { ReadDetails, RenderCtxLike, SdkToolDef, TextContent, ThemeLike } from "../types.js";
|
|
20
21
|
import { wrapExecuteWithMetrics } from "./metrics.js";
|
|
21
22
|
|
|
22
23
|
type Result = AgentToolResult<Record<string, unknown>>;
|
|
23
24
|
|
|
25
|
+
function getPiDocsLabel(filePath: string, cwd: string): string | undefined {
|
|
26
|
+
const packageRoot = dirname(getReadmePath());
|
|
27
|
+
const relativePath = relative(resolve(packageRoot), resolve(cwd, filePath));
|
|
28
|
+
if (relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const label = relativePath.split(sep).join("/");
|
|
33
|
+
return label === "README.md" || label.startsWith("docs/") || label.startsWith("examples/") ? label : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
function getSkillName(filePath: string, content: string): string | undefined {
|
|
25
37
|
if (basename(filePath) !== "SKILL.md") return undefined;
|
|
26
38
|
|
|
@@ -54,6 +66,7 @@ export function registerReadTool(
|
|
|
54
66
|
_fffService: unknown,
|
|
55
67
|
sdkTool: SdkToolDef,
|
|
56
68
|
TextComp?: new (t?: string, x?: number, y?: number) => { setText(v: string): void },
|
|
69
|
+
renderContent: typeof renderFileContent = renderFileContent,
|
|
57
70
|
): void {
|
|
58
71
|
const TC = resolveTextCtor(TextComp);
|
|
59
72
|
const home = process.env.HOME ?? "";
|
|
@@ -63,6 +76,9 @@ export function registerReadTool(
|
|
|
63
76
|
label: "Read",
|
|
64
77
|
description: sdkTool.description ?? "Read file contents",
|
|
65
78
|
parameters: sdkTool.parameters,
|
|
79
|
+
promptSnippet: sdkTool.promptSnippet,
|
|
80
|
+
promptGuidelines: sdkTool.promptGuidelines,
|
|
81
|
+
constrainedSampling: sdkTool.constrainedSampling,
|
|
66
82
|
renderShell: "self",
|
|
67
83
|
|
|
68
84
|
execute: wrapExecuteWithMetrics(async (tid, params, sig, _upd, ctx: ExtensionContext) => {
|
|
@@ -100,17 +116,18 @@ export function registerReadTool(
|
|
|
100
116
|
|
|
101
117
|
const path = String(args.path ?? "");
|
|
102
118
|
const label = theme.fg("error", theme.bold("→ read"));
|
|
103
|
-
text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${label} ${theme.fg("toolTitle", path)}`, BG_ERROR));
|
|
119
|
+
text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${label} ${theme.fg("toolTitle", path)}\n`, BG_ERROR));
|
|
104
120
|
return text;
|
|
105
121
|
},
|
|
106
122
|
|
|
107
123
|
renderResult(result: Result, _opt: unknown, theme: ThemeLike, ctx: RenderCtxLike) {
|
|
108
124
|
resolveBaseBackground(theme);
|
|
109
125
|
|
|
126
|
+
const renderToken = nextReadRenderToken(ctx);
|
|
110
127
|
const text = ctx.lastComponent ?? new TC("", 0, 0);
|
|
111
128
|
|
|
112
129
|
if (ctx.isError) {
|
|
113
|
-
text.setText(
|
|
130
|
+
text.setText(renderToolError(getText(result) || "Error", theme));
|
|
114
131
|
return text;
|
|
115
132
|
}
|
|
116
133
|
|
|
@@ -121,7 +138,8 @@ export function registerReadTool(
|
|
|
121
138
|
// or unsupported by the terminal.
|
|
122
139
|
if (d?._type === "readImage") {
|
|
123
140
|
const note = getText(result);
|
|
124
|
-
text
|
|
141
|
+
// Keep fallback text visually separated from the preceding tool row.
|
|
142
|
+
text.setText(note ? fillToolBody(`\n${note}`, BG_BASE) : "");
|
|
125
143
|
return text;
|
|
126
144
|
}
|
|
127
145
|
|
|
@@ -137,12 +155,16 @@ export function registerReadTool(
|
|
|
137
155
|
if (!ctx.expanded) {
|
|
138
156
|
if (skillName) {
|
|
139
157
|
const header = renderSkillHeader(skillName, false, theme);
|
|
140
|
-
text.setText(
|
|
158
|
+
text.setText(fillToolBody(`\n${TOOL_RESULT_INDENT}${header}\n`, BG_BASE));
|
|
141
159
|
return text;
|
|
142
160
|
}
|
|
161
|
+
const docsLabel = getPiDocsLabel(filePath, cwd);
|
|
162
|
+
const title = docsLabel
|
|
163
|
+
? `${theme.fg("toolTitle", theme.bold("→ read docs"))} ${theme.fg("toolTitle", docsLabel)}`
|
|
164
|
+
: `${theme.fg("toolTitle", theme.bold("→ read"))} ${theme.fg("toolTitle", p2)}`;
|
|
143
165
|
text.setText(
|
|
144
|
-
|
|
145
|
-
`\n${TOOL_RESULT_INDENT}${
|
|
166
|
+
fillToolBody(
|
|
167
|
+
`\n${TOOL_RESULT_INDENT}${title}${theme.fg("dim", off2)} ${FG_DIM}${total} lines — ctrl+o to expand${RST}`,
|
|
146
168
|
BG_BASE,
|
|
147
169
|
),
|
|
148
170
|
);
|
|
@@ -150,19 +172,34 @@ export function registerReadTool(
|
|
|
150
172
|
}
|
|
151
173
|
const maxShow = lines.length;
|
|
152
174
|
const show = lines.slice(0, maxShow);
|
|
153
|
-
const
|
|
175
|
+
const offset = d.offset || 0;
|
|
176
|
+
const nw = Math.max(3, String(offset + total).length);
|
|
154
177
|
const gw = nw + 3;
|
|
155
178
|
const cw = Math.max(1, tw - gw);
|
|
156
179
|
|
|
157
180
|
const header = skillName
|
|
158
181
|
? renderSkillHeader(skillName, true, theme)
|
|
159
182
|
: `${theme.fg("toolTitle", theme.bold("→ read"))} ${theme.fg("toolTitle", p2)}${theme.fg("dim", off2)}`;
|
|
160
|
-
const
|
|
183
|
+
const highlightRequest: ReadHighlightRequest = {
|
|
184
|
+
content: d.content,
|
|
185
|
+
filePath: d.filePath,
|
|
186
|
+
offset,
|
|
187
|
+
maxLines: maxShow,
|
|
188
|
+
width: cw,
|
|
189
|
+
};
|
|
190
|
+
const cachedHighlight = getCachedReadHighlight(ctx, highlightRequest);
|
|
191
|
+
if (cachedHighlight !== undefined) {
|
|
192
|
+
const highlighted = buildHighlightedRead(cachedHighlight, header, skillName, offset, nw, tw);
|
|
193
|
+
text.setText(fillToolBody(highlighted, BG_BASE));
|
|
194
|
+
return text;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const out: string[] = ["", `${TOOL_RESULT_INDENT}${header}`, ""];
|
|
161
198
|
out.push(`${TOOL_RESULT_INDENT}${FG_RULE}${"─".repeat(tw - 1)}${RST}`);
|
|
162
199
|
for (let i = 0; i < show.length; i++) {
|
|
163
|
-
const ln =
|
|
200
|
+
const ln = offset + i + 1;
|
|
164
201
|
const code = show[i] ?? "";
|
|
165
|
-
const display = code.length > cw ? code.slice(0, Math.max(0, cw - 1))
|
|
202
|
+
const display = code.length > cw ? `${code.slice(0, Math.max(0, cw - 1))}${FG_DIM}›${RST}` : code;
|
|
166
203
|
const lineNo = String(ln);
|
|
167
204
|
out.push(
|
|
168
205
|
`${TOOL_RESULT_INDENT}${FG_LNUM}${" ".repeat(Math.max(0, nw - lineNo.length))}${lineNo}${RST} ${FG_RULE}│${RST} ${display}${RST}`,
|
|
@@ -171,27 +208,18 @@ export function registerReadTool(
|
|
|
171
208
|
if (total > maxShow) {
|
|
172
209
|
out.push(`${TOOL_RESULT_INDENT}${FG_DIM}… ${total - maxShow} more lines (${total} total)${RST}`);
|
|
173
210
|
}
|
|
174
|
-
out.push("");
|
|
175
211
|
const rendered = out.join("\n");
|
|
176
|
-
text.setText(
|
|
177
|
-
(ctx as any).state._rt = rendered;
|
|
212
|
+
text.setText(fillToolBody(rendered, BG_BASE));
|
|
178
213
|
|
|
179
|
-
// Async syntax highlighting via Shiki
|
|
180
|
-
|
|
214
|
+
// Async syntax highlighting via Shiki. The component is reused across
|
|
215
|
+
// expansion changes, so stale work must not restore an older view.
|
|
216
|
+
renderContent(d.content, d.filePath, offset, maxShow, cw)
|
|
181
217
|
.then((hl) => {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
})
|
|
188
|
-
.join("\n");
|
|
189
|
-
const divider = skillName
|
|
190
|
-
? `${TOOL_RESULT_INDENT}${FG_RULE}${"─".repeat(Math.max(1, tw - 1))}${RST}\n`
|
|
191
|
-
: "";
|
|
192
|
-
const rendered = `\n${TOOL_RESULT_INDENT}${header}\n${divider}${padded}\n`;
|
|
193
|
-
text.setText(fillToolBackground(rendered, BG_BASE));
|
|
194
|
-
(ctx as any).state._rt = rendered;
|
|
218
|
+
if (!isCurrentReadRender(ctx, renderToken)) return;
|
|
219
|
+
setCachedReadHighlight(ctx, highlightRequest, hl);
|
|
220
|
+
const highlighted = buildHighlightedRead(hl, header, skillName, offset, nw, tw);
|
|
221
|
+
text.setText(fillToolBody(highlighted, BG_BASE));
|
|
222
|
+
ctx.invalidate?.();
|
|
195
223
|
})
|
|
196
224
|
.catch(() => {});
|
|
197
225
|
|
|
@@ -200,7 +228,7 @@ export function registerReadTool(
|
|
|
200
228
|
|
|
201
229
|
const fc = result.content?.[0];
|
|
202
230
|
text.setText(
|
|
203
|
-
|
|
231
|
+
fillToolBody(
|
|
204
232
|
`${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`,
|
|
205
233
|
BG_BASE,
|
|
206
234
|
),
|
|
@@ -210,6 +238,73 @@ export function registerReadTool(
|
|
|
210
238
|
} as unknown as ToolDefinition<any, any, any>);
|
|
211
239
|
}
|
|
212
240
|
|
|
241
|
+
const READ_RENDER_TOKEN = "__piPrettyReadRenderToken";
|
|
242
|
+
const READ_HIGHLIGHT_CACHE = "__piPrettyReadHighlightCache";
|
|
243
|
+
|
|
244
|
+
type ReadHighlightRequest = {
|
|
245
|
+
content: string;
|
|
246
|
+
filePath: string;
|
|
247
|
+
offset: number;
|
|
248
|
+
maxLines: number;
|
|
249
|
+
width: number;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
type ReadHighlightCache = ReadHighlightRequest & {
|
|
253
|
+
highlighted: string;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
function nextReadRenderToken(ctx: RenderCtxLike): number {
|
|
257
|
+
const state = ctx.state;
|
|
258
|
+
const token = typeof state[READ_RENDER_TOKEN] === "number" ? state[READ_RENDER_TOKEN] + 1 : 1;
|
|
259
|
+
state[READ_RENDER_TOKEN] = token;
|
|
260
|
+
return token;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function isCurrentReadRender(ctx: RenderCtxLike, token: number): boolean {
|
|
264
|
+
return ctx.state[READ_RENDER_TOKEN] === token;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function getCachedReadHighlight(ctx: RenderCtxLike, request: ReadHighlightRequest): string | undefined {
|
|
268
|
+
const cache = ctx.state[READ_HIGHLIGHT_CACHE] as ReadHighlightCache | undefined;
|
|
269
|
+
if (!cache) return undefined;
|
|
270
|
+
if (
|
|
271
|
+
cache.content !== request.content ||
|
|
272
|
+
cache.filePath !== request.filePath ||
|
|
273
|
+
cache.offset !== request.offset ||
|
|
274
|
+
cache.maxLines !== request.maxLines ||
|
|
275
|
+
cache.width !== request.width
|
|
276
|
+
) {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
return cache.highlighted;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function setCachedReadHighlight(ctx: RenderCtxLike, request: ReadHighlightRequest, highlighted: string): void {
|
|
283
|
+
ctx.state[READ_HIGHLIGHT_CACHE] = {
|
|
284
|
+
...request,
|
|
285
|
+
highlighted,
|
|
286
|
+
} satisfies ReadHighlightCache;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function buildHighlightedRead(
|
|
290
|
+
highlighted: string,
|
|
291
|
+
header: string,
|
|
292
|
+
skillName: string | undefined,
|
|
293
|
+
offset: number,
|
|
294
|
+
nw: number,
|
|
295
|
+
tw: number,
|
|
296
|
+
): string {
|
|
297
|
+
const padded = highlighted
|
|
298
|
+
.split("\n")
|
|
299
|
+
.map((line, index) => {
|
|
300
|
+
const lineNo = String(offset + index + 1);
|
|
301
|
+
return `${TOOL_RESULT_INDENT}${FG_LNUM}${" ".repeat(Math.max(0, nw - lineNo.length))}${lineNo}${RST} ${FG_RULE}│${RST} ${line}${RST}`;
|
|
302
|
+
})
|
|
303
|
+
.join("\n");
|
|
304
|
+
const divider = skillName ? `${TOOL_RESULT_INDENT}${FG_RULE}${"─".repeat(Math.max(1, tw - 1))}${RST}\n` : "";
|
|
305
|
+
return `\n${TOOL_RESULT_INDENT}${header}\n\n${divider}${padded}`;
|
|
306
|
+
}
|
|
307
|
+
|
|
213
308
|
function getText(result: Result): string {
|
|
214
309
|
return (
|
|
215
310
|
((result.content ?? []) as TextContent[])
|
package/src/types.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* pi-pretty shared types.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
|
|
6
|
-
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { ConstrainedSamplingConfig, ImageContent, TextContent } from "@earendil-works/pi-ai";
|
|
6
|
+
import type { AgentToolResult, CustomEditor } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type { FffInitOptions } from "./fff.js";
|
|
7
8
|
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
9
10
|
// Re-export FFF types needed by tools
|
|
@@ -29,9 +30,11 @@ export interface ThemeLike {
|
|
|
29
30
|
|
|
30
31
|
export interface RenderCtxLike {
|
|
31
32
|
lastComponent?: ComponentLike;
|
|
33
|
+
toolCallId?: string;
|
|
32
34
|
isError?: boolean;
|
|
33
|
-
state: Record<string,
|
|
35
|
+
state: Record<string, unknown>;
|
|
34
36
|
expanded?: boolean;
|
|
37
|
+
invalidate?: () => void;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
export interface TextLike {
|
|
@@ -133,6 +136,9 @@ export interface SdkToolDef {
|
|
|
133
136
|
description?: string;
|
|
134
137
|
label?: string;
|
|
135
138
|
parameters?: unknown;
|
|
139
|
+
promptSnippet?: string;
|
|
140
|
+
promptGuidelines?: string[];
|
|
141
|
+
constrainedSampling?: false | ConstrainedSamplingConfig;
|
|
136
142
|
execute: (...args: any[]) => Promise<AgentToolResult<any>>;
|
|
137
143
|
}
|
|
138
144
|
|
|
@@ -168,7 +174,7 @@ export interface FffServiceWithCursor extends FffServiceLike {
|
|
|
168
174
|
|
|
169
175
|
/** FFF lifecycle interface (used by session lifecycle code). */
|
|
170
176
|
export interface FffService extends FffServiceWithCursor {
|
|
171
|
-
ensureFinder(cwd: string): Promise<void>;
|
|
177
|
+
ensureFinder(cwd: string, options?: FffInitOptions): Promise<void>;
|
|
172
178
|
destroy(): void;
|
|
173
179
|
isModuleLoaded(): boolean;
|
|
174
180
|
tryLoadModule(): Promise<boolean>;
|
|
@@ -183,6 +189,8 @@ export interface CursorStore {
|
|
|
183
189
|
// DI
|
|
184
190
|
// ---------------------------------------------------------------------------
|
|
185
191
|
|
|
192
|
+
export type CustomEditorConstructor = new (...args: ConstructorParameters<typeof CustomEditor>) => CustomEditor;
|
|
193
|
+
|
|
186
194
|
export interface PiPrettyDeps {
|
|
187
195
|
sdk?: SdkTools;
|
|
188
196
|
TextComponent?: new (text?: string, x?: number, y?: number) => ComponentLike;
|
|
@@ -191,4 +199,6 @@ export interface PiPrettyDeps {
|
|
|
191
199
|
* to the package export. Injectable so tests can verify against the exact
|
|
192
200
|
* class instance they construct rows from. */
|
|
193
201
|
assistantMessageComponent?: unknown;
|
|
202
|
+
/** Host CustomEditor class for the live prompt icon; defaults to the package export. */
|
|
203
|
+
customEditorClass?: CustomEditorConstructor;
|
|
194
204
|
}
|