@heyhuynhgiabuu/pi-pretty 0.6.26 → 0.6.27
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 +18 -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 +73 -30
- 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 +10 -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 +94 -31
- package/dist/tools/read.js.map +1 -1
- package/dist/types.d.ts +9 -3
- 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 +94 -32
- package/src/tools/find.ts +39 -17
- package/src/tools/grep.ts +26 -15
- package/src/tools/ls.ts +19 -10
- package/src/tools/metrics.ts +28 -8
- package/src/tools/read.ts +123 -31
- package/src/types.ts +10 -3
- 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 ?? "";
|
|
@@ -100,17 +113,18 @@ export function registerReadTool(
|
|
|
100
113
|
|
|
101
114
|
const path = String(args.path ?? "");
|
|
102
115
|
const label = theme.fg("error", theme.bold("→ read"));
|
|
103
|
-
text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${label} ${theme.fg("toolTitle", path)}`, BG_ERROR));
|
|
116
|
+
text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${label} ${theme.fg("toolTitle", path)}\n`, BG_ERROR));
|
|
104
117
|
return text;
|
|
105
118
|
},
|
|
106
119
|
|
|
107
120
|
renderResult(result: Result, _opt: unknown, theme: ThemeLike, ctx: RenderCtxLike) {
|
|
108
121
|
resolveBaseBackground(theme);
|
|
109
122
|
|
|
123
|
+
const renderToken = nextReadRenderToken(ctx);
|
|
110
124
|
const text = ctx.lastComponent ?? new TC("", 0, 0);
|
|
111
125
|
|
|
112
126
|
if (ctx.isError) {
|
|
113
|
-
text.setText(
|
|
127
|
+
text.setText(renderToolError(getText(result) || "Error", theme));
|
|
114
128
|
return text;
|
|
115
129
|
}
|
|
116
130
|
|
|
@@ -121,7 +135,8 @@ export function registerReadTool(
|
|
|
121
135
|
// or unsupported by the terminal.
|
|
122
136
|
if (d?._type === "readImage") {
|
|
123
137
|
const note = getText(result);
|
|
124
|
-
text
|
|
138
|
+
// Keep fallback text visually separated from the preceding tool row.
|
|
139
|
+
text.setText(note ? fillToolBody(`\n${note}`, BG_BASE) : "");
|
|
125
140
|
return text;
|
|
126
141
|
}
|
|
127
142
|
|
|
@@ -137,12 +152,16 @@ export function registerReadTool(
|
|
|
137
152
|
if (!ctx.expanded) {
|
|
138
153
|
if (skillName) {
|
|
139
154
|
const header = renderSkillHeader(skillName, false, theme);
|
|
140
|
-
text.setText(
|
|
155
|
+
text.setText(fillToolBody(`\n${TOOL_RESULT_INDENT}${header}\n`, BG_BASE));
|
|
141
156
|
return text;
|
|
142
157
|
}
|
|
158
|
+
const docsLabel = getPiDocsLabel(filePath, cwd);
|
|
159
|
+
const title = docsLabel
|
|
160
|
+
? `${theme.fg("toolTitle", theme.bold("→ read docs"))} ${theme.fg("toolTitle", docsLabel)}`
|
|
161
|
+
: `${theme.fg("toolTitle", theme.bold("→ read"))} ${theme.fg("toolTitle", p2)}`;
|
|
143
162
|
text.setText(
|
|
144
|
-
|
|
145
|
-
`\n${TOOL_RESULT_INDENT}${
|
|
163
|
+
fillToolBody(
|
|
164
|
+
`\n${TOOL_RESULT_INDENT}${title}${theme.fg("dim", off2)} ${FG_DIM}${total} lines — ctrl+o to expand${RST}`,
|
|
146
165
|
BG_BASE,
|
|
147
166
|
),
|
|
148
167
|
);
|
|
@@ -150,19 +169,34 @@ export function registerReadTool(
|
|
|
150
169
|
}
|
|
151
170
|
const maxShow = lines.length;
|
|
152
171
|
const show = lines.slice(0, maxShow);
|
|
153
|
-
const
|
|
172
|
+
const offset = d.offset || 0;
|
|
173
|
+
const nw = Math.max(3, String(offset + total).length);
|
|
154
174
|
const gw = nw + 3;
|
|
155
175
|
const cw = Math.max(1, tw - gw);
|
|
156
176
|
|
|
157
177
|
const header = skillName
|
|
158
178
|
? renderSkillHeader(skillName, true, theme)
|
|
159
179
|
: `${theme.fg("toolTitle", theme.bold("→ read"))} ${theme.fg("toolTitle", p2)}${theme.fg("dim", off2)}`;
|
|
160
|
-
const
|
|
180
|
+
const highlightRequest: ReadHighlightRequest = {
|
|
181
|
+
content: d.content,
|
|
182
|
+
filePath: d.filePath,
|
|
183
|
+
offset,
|
|
184
|
+
maxLines: maxShow,
|
|
185
|
+
width: cw,
|
|
186
|
+
};
|
|
187
|
+
const cachedHighlight = getCachedReadHighlight(ctx, highlightRequest);
|
|
188
|
+
if (cachedHighlight !== undefined) {
|
|
189
|
+
const highlighted = buildHighlightedRead(cachedHighlight, header, skillName, offset, nw, tw);
|
|
190
|
+
text.setText(fillToolBody(highlighted, BG_BASE));
|
|
191
|
+
return text;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const out: string[] = ["", `${TOOL_RESULT_INDENT}${header}`, ""];
|
|
161
195
|
out.push(`${TOOL_RESULT_INDENT}${FG_RULE}${"─".repeat(tw - 1)}${RST}`);
|
|
162
196
|
for (let i = 0; i < show.length; i++) {
|
|
163
|
-
const ln =
|
|
197
|
+
const ln = offset + i + 1;
|
|
164
198
|
const code = show[i] ?? "";
|
|
165
|
-
const display = code.length > cw ? code.slice(0, Math.max(0, cw - 1))
|
|
199
|
+
const display = code.length > cw ? `${code.slice(0, Math.max(0, cw - 1))}${FG_DIM}›${RST}` : code;
|
|
166
200
|
const lineNo = String(ln);
|
|
167
201
|
out.push(
|
|
168
202
|
`${TOOL_RESULT_INDENT}${FG_LNUM}${" ".repeat(Math.max(0, nw - lineNo.length))}${lineNo}${RST} ${FG_RULE}│${RST} ${display}${RST}`,
|
|
@@ -171,27 +205,18 @@ export function registerReadTool(
|
|
|
171
205
|
if (total > maxShow) {
|
|
172
206
|
out.push(`${TOOL_RESULT_INDENT}${FG_DIM}… ${total - maxShow} more lines (${total} total)${RST}`);
|
|
173
207
|
}
|
|
174
|
-
out.push("");
|
|
175
208
|
const rendered = out.join("\n");
|
|
176
|
-
text.setText(
|
|
177
|
-
(ctx as any).state._rt = rendered;
|
|
209
|
+
text.setText(fillToolBody(rendered, BG_BASE));
|
|
178
210
|
|
|
179
|
-
// Async syntax highlighting via Shiki
|
|
180
|
-
|
|
211
|
+
// Async syntax highlighting via Shiki. The component is reused across
|
|
212
|
+
// expansion changes, so stale work must not restore an older view.
|
|
213
|
+
renderContent(d.content, d.filePath, offset, maxShow, cw)
|
|
181
214
|
.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;
|
|
215
|
+
if (!isCurrentReadRender(ctx, renderToken)) return;
|
|
216
|
+
setCachedReadHighlight(ctx, highlightRequest, hl);
|
|
217
|
+
const highlighted = buildHighlightedRead(hl, header, skillName, offset, nw, tw);
|
|
218
|
+
text.setText(fillToolBody(highlighted, BG_BASE));
|
|
219
|
+
ctx.invalidate?.();
|
|
195
220
|
})
|
|
196
221
|
.catch(() => {});
|
|
197
222
|
|
|
@@ -200,7 +225,7 @@ export function registerReadTool(
|
|
|
200
225
|
|
|
201
226
|
const fc = result.content?.[0];
|
|
202
227
|
text.setText(
|
|
203
|
-
|
|
228
|
+
fillToolBody(
|
|
204
229
|
`${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`,
|
|
205
230
|
BG_BASE,
|
|
206
231
|
),
|
|
@@ -210,6 +235,73 @@ export function registerReadTool(
|
|
|
210
235
|
} as unknown as ToolDefinition<any, any, any>);
|
|
211
236
|
}
|
|
212
237
|
|
|
238
|
+
const READ_RENDER_TOKEN = "__piPrettyReadRenderToken";
|
|
239
|
+
const READ_HIGHLIGHT_CACHE = "__piPrettyReadHighlightCache";
|
|
240
|
+
|
|
241
|
+
type ReadHighlightRequest = {
|
|
242
|
+
content: string;
|
|
243
|
+
filePath: string;
|
|
244
|
+
offset: number;
|
|
245
|
+
maxLines: number;
|
|
246
|
+
width: number;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
type ReadHighlightCache = ReadHighlightRequest & {
|
|
250
|
+
highlighted: string;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
function nextReadRenderToken(ctx: RenderCtxLike): number {
|
|
254
|
+
const state = ctx.state;
|
|
255
|
+
const token = typeof state[READ_RENDER_TOKEN] === "number" ? state[READ_RENDER_TOKEN] + 1 : 1;
|
|
256
|
+
state[READ_RENDER_TOKEN] = token;
|
|
257
|
+
return token;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function isCurrentReadRender(ctx: RenderCtxLike, token: number): boolean {
|
|
261
|
+
return ctx.state[READ_RENDER_TOKEN] === token;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function getCachedReadHighlight(ctx: RenderCtxLike, request: ReadHighlightRequest): string | undefined {
|
|
265
|
+
const cache = ctx.state[READ_HIGHLIGHT_CACHE] as ReadHighlightCache | undefined;
|
|
266
|
+
if (!cache) return undefined;
|
|
267
|
+
if (
|
|
268
|
+
cache.content !== request.content ||
|
|
269
|
+
cache.filePath !== request.filePath ||
|
|
270
|
+
cache.offset !== request.offset ||
|
|
271
|
+
cache.maxLines !== request.maxLines ||
|
|
272
|
+
cache.width !== request.width
|
|
273
|
+
) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
return cache.highlighted;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function setCachedReadHighlight(ctx: RenderCtxLike, request: ReadHighlightRequest, highlighted: string): void {
|
|
280
|
+
ctx.state[READ_HIGHLIGHT_CACHE] = {
|
|
281
|
+
...request,
|
|
282
|
+
highlighted,
|
|
283
|
+
} satisfies ReadHighlightCache;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function buildHighlightedRead(
|
|
287
|
+
highlighted: string,
|
|
288
|
+
header: string,
|
|
289
|
+
skillName: string | undefined,
|
|
290
|
+
offset: number,
|
|
291
|
+
nw: number,
|
|
292
|
+
tw: number,
|
|
293
|
+
): string {
|
|
294
|
+
const padded = highlighted
|
|
295
|
+
.split("\n")
|
|
296
|
+
.map((line, index) => {
|
|
297
|
+
const lineNo = String(offset + index + 1);
|
|
298
|
+
return `${TOOL_RESULT_INDENT}${FG_LNUM}${" ".repeat(Math.max(0, nw - lineNo.length))}${lineNo}${RST} ${FG_RULE}│${RST} ${line}${RST}`;
|
|
299
|
+
})
|
|
300
|
+
.join("\n");
|
|
301
|
+
const divider = skillName ? `${TOOL_RESULT_INDENT}${FG_RULE}${"─".repeat(Math.max(1, tw - 1))}${RST}\n` : "";
|
|
302
|
+
return `\n${TOOL_RESULT_INDENT}${header}\n\n${divider}${padded}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
213
305
|
function getText(result: Result): string {
|
|
214
306
|
return (
|
|
215
307
|
((result.content ?? []) as TextContent[])
|
package/src/types.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
|
|
6
|
-
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
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 {
|
|
@@ -168,7 +171,7 @@ export interface FffServiceWithCursor extends FffServiceLike {
|
|
|
168
171
|
|
|
169
172
|
/** FFF lifecycle interface (used by session lifecycle code). */
|
|
170
173
|
export interface FffService extends FffServiceWithCursor {
|
|
171
|
-
ensureFinder(cwd: string): Promise<void>;
|
|
174
|
+
ensureFinder(cwd: string, options?: FffInitOptions): Promise<void>;
|
|
172
175
|
destroy(): void;
|
|
173
176
|
isModuleLoaded(): boolean;
|
|
174
177
|
tryLoadModule(): Promise<boolean>;
|
|
@@ -183,6 +186,8 @@ export interface CursorStore {
|
|
|
183
186
|
// DI
|
|
184
187
|
// ---------------------------------------------------------------------------
|
|
185
188
|
|
|
189
|
+
export type CustomEditorConstructor = new (...args: ConstructorParameters<typeof CustomEditor>) => CustomEditor;
|
|
190
|
+
|
|
186
191
|
export interface PiPrettyDeps {
|
|
187
192
|
sdk?: SdkTools;
|
|
188
193
|
TextComponent?: new (text?: string, x?: number, y?: number) => ComponentLike;
|
|
@@ -191,4 +196,6 @@ export interface PiPrettyDeps {
|
|
|
191
196
|
* to the package export. Injectable so tests can verify against the exact
|
|
192
197
|
* class instance they construct rows from. */
|
|
193
198
|
assistantMessageComponent?: unknown;
|
|
199
|
+
/** Host CustomEditor class for the live prompt icon; defaults to the package export. */
|
|
200
|
+
customEditorClass?: CustomEditorConstructor;
|
|
194
201
|
}
|