@heyhuynhgiabuu/pi-pretty 0.6.2 → 0.6.3
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/package.json +2 -2
- package/src/index.ts +21 -0
- package/src/render.ts +41 -56
- package/src/tools/bash.ts +4 -7
- package/src/tools/find.ts +52 -23
- package/src/tools/grep.ts +27 -31
- package/src/tools/ls.ts +11 -5
- package/src/tools/multi-grep.ts +9 -5
- package/test/bash-rendering.test.ts +3 -3
- package/test/fff-integration.test.ts +57 -47
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@heyhuynhgiabuu/pi-pretty",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "Pretty terminal output for pi — syntax-highlighted file reads, colored bash output, tree-view directory listings, and more.",
|
|
5
5
|
"author": "huynhgiabuu",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"pi": {
|
|
49
49
|
"extensions": [
|
|
50
|
-
"./
|
|
50
|
+
"./dist/index.js"
|
|
51
51
|
]
|
|
52
52
|
}
|
|
53
53
|
}
|
package/src/index.ts
CHANGED
|
@@ -246,4 +246,25 @@ export default function piPrettyExtension(pi: ExtensionAPI, deps?: PiPrettyDeps)
|
|
|
246
246
|
pi.on("session_shutdown", async () => {
|
|
247
247
|
fffService?.destroy();
|
|
248
248
|
});
|
|
249
|
+
|
|
250
|
+
// Fallback padding for SDK-rendered tool bodies. The SDK reads
|
|
251
|
+
// result.content[0].text and slices collapsed output to roughly the first
|
|
252
|
+
// 10 lines, so insert bottom padding inside that visible slice.
|
|
253
|
+
const PADDED_TOOLS = new Set(["read", "grep", "bash"]);
|
|
254
|
+
const RESULT_LEFT_PAD = " ";
|
|
255
|
+
const BOTTOM_PADDING_BY_TOOL: Record<string, number> = { read: 2, grep: 2, bash: 0 };
|
|
256
|
+
pi.on("tool_result", (event, _ctx) => {
|
|
257
|
+
if (!PADDED_TOOLS.has(event.toolName)) return undefined;
|
|
258
|
+
const first = event.content[0];
|
|
259
|
+
if (!first || first.type !== "text") return undefined;
|
|
260
|
+
const lines = first.text.split("\n").map((line) => `${RESULT_LEFT_PAD}${line}`);
|
|
261
|
+
if (lines.length === 0) return undefined;
|
|
262
|
+
|
|
263
|
+
const padCount = BOTTOM_PADDING_BY_TOOL[event.toolName] ?? 0;
|
|
264
|
+
lines.push(...Array.from({ length: padCount }, () => RESULT_LEFT_PAD));
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
content: [{ type: "text" as const, text: lines.join("\n") }, ...event.content.slice(1)],
|
|
268
|
+
};
|
|
269
|
+
});
|
|
249
270
|
}
|
package/src/render.ts
CHANGED
|
@@ -184,36 +184,21 @@ export function renderToolError(error: string, theme: ThemeLike): string {
|
|
|
184
184
|
export async function renderFileContent(
|
|
185
185
|
content: string,
|
|
186
186
|
filePath: string,
|
|
187
|
-
offset =
|
|
187
|
+
offset = 0,
|
|
188
188
|
maxLines = MAX_PREVIEW_LINES,
|
|
189
189
|
width?: number,
|
|
190
190
|
): Promise<string> {
|
|
191
191
|
const normalizedContent = normalizeLineEndings(content);
|
|
192
192
|
const lines = normalizedContent.split("\n");
|
|
193
|
-
const total = lines.length;
|
|
194
193
|
const show = lines.slice(0, maxLines);
|
|
195
194
|
const lg = detectLang(filePath);
|
|
196
195
|
const hl = await hlBlock(show.join("\n"), lg);
|
|
197
196
|
|
|
198
197
|
const tw = width ?? termWidth();
|
|
199
|
-
const startLine = offset;
|
|
200
|
-
const endLine = startLine + show.length - 1;
|
|
201
|
-
const nw = Math.max(3, String(endLine).length);
|
|
202
|
-
const gw = nw + 3;
|
|
203
|
-
const cw = Math.max(1, tw - gw);
|
|
204
198
|
|
|
205
199
|
const out: string[] = [];
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
for (let i = 0; i < hl.length; i++) {
|
|
209
|
-
const ln = startLine + i;
|
|
210
|
-
const code = hl[i] ?? show[i] ?? "";
|
|
211
|
-
const display = truncateToWidth(code, cw, `${FG_DIM}›`);
|
|
212
|
-
out.push(`${lnum(ln, nw)} ${FG_RULE}│${RST} ${display}${RST}`);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (total > maxLines) {
|
|
216
|
-
out.push(`${FG_DIM} … ${total - maxLines} more lines (${total} total)${RST}`);
|
|
200
|
+
for (const line of hl) {
|
|
201
|
+
out.push(truncateToWidth(line ?? "", Math.max(1, tw), `${FG_DIM}›`));
|
|
217
202
|
}
|
|
218
203
|
return out.join("\n");
|
|
219
204
|
}
|
|
@@ -270,44 +255,44 @@ export function renderTree(text: string, _basePath: string): string {
|
|
|
270
255
|
return out.join("\n");
|
|
271
256
|
}
|
|
272
257
|
|
|
273
|
-
// ---------------------------------------------------------------------------
|
|
274
|
-
// Find — grouped file list
|
|
275
|
-
// ---------------------------------------------------------------------------
|
|
276
|
-
|
|
277
|
-
export function renderFindResults(text: string): string {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
// Find — grouped file list (plain, no tree characters or icons)
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
|
|
262
|
+
export function renderFindResults(text: string, theme?: ThemeLike): string {
|
|
263
|
+
const lines = text.trim().split("\n").filter(Boolean);
|
|
264
|
+
if (!lines.length) return theme ? theme.fg("dim", "(no matches)") : `${FG_DIM}(no matches)${RST}`;
|
|
265
|
+
|
|
266
|
+
const groups = new Map<string, string[]>();
|
|
267
|
+
for (const line of lines) {
|
|
268
|
+
const trimmed = line.trim();
|
|
269
|
+
const dir = dirname(trimmed) || ".";
|
|
270
|
+
const file = basename(trimmed);
|
|
271
|
+
if (!groups.has(dir)) groups.set(dir, []);
|
|
272
|
+
const bucket = groups.get(dir);
|
|
273
|
+
if (bucket) bucket.push(file);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const out: string[] = [];
|
|
277
|
+
let count = 0;
|
|
278
|
+
|
|
279
|
+
for (const [dir, files] of groups) {
|
|
280
|
+
if (count > 0) out.push("");
|
|
281
|
+
const dirColored = theme ? theme.fg("accent", theme.bold(`${dir}/`)) : `${FG_BLUE}\x1b[1m${dir}/${RST}`;
|
|
282
|
+
out.push(dirColored);
|
|
283
|
+
for (let i = 0; i < files.length; i++) {
|
|
284
|
+
if (count >= MAX_PREVIEW_LINES) {
|
|
285
|
+
const more = theme ? theme.fg("dim", `… ${lines.length - count} more files`) : `${FG_DIM}… ${lines.length - count} more files${RST}`;
|
|
286
|
+
out.push(` ${more}`);
|
|
287
|
+
return out.join("\n");
|
|
288
|
+
}
|
|
289
|
+
out.push(` ${files[i]}`);
|
|
290
|
+
count++;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return out.join("\n");
|
|
295
|
+
}
|
|
311
296
|
|
|
312
297
|
// ---------------------------------------------------------------------------
|
|
313
298
|
// Grep — highlighted matches with line numbers
|
package/src/tools/bash.ts
CHANGED
|
@@ -37,18 +37,15 @@ export function registerBashTool(
|
|
|
37
37
|
}
|
|
38
38
|
}),
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
renderCall(args: any, theme: ThemeLike, ctx: RenderCtxLike) {
|
|
41
41
|
resolveBaseBackground(theme);
|
|
42
42
|
const text = ctx.lastComponent ?? new TC("", 0, 0);
|
|
43
|
-
const t = typeof args.timeout === "number" ? ` ${theme.fg("muted", `(${args.timeout}s
|
|
43
|
+
const t = typeof args.timeout === "number" ? ` ${theme.fg("muted", `(timeout ${args.timeout}s)`)}` : "";
|
|
44
44
|
const tw = termWidth() || 80;
|
|
45
45
|
const rawCmd = String(args.command ?? "");
|
|
46
|
-
const cmd = (()
|
|
47
|
-
const ml = Math.max(1, tw - 20);
|
|
48
|
-
return rawCmd.length > ml ? rawCmd.slice(0, ml) + "\u2026" : rawCmd;
|
|
49
|
-
})();
|
|
46
|
+
const cmd = rawCmd.length === 0 ? theme.fg("toolOutput", "...") : (rawCmd.length > tw - 20 ? rawCmd.slice(0, Math.max(1, tw - 20)) + "…" : rawCmd);
|
|
50
47
|
const toolWidth = ctx.expanded ? undefined : tw;
|
|
51
|
-
text.setText(fillToolBackground(`\n ${theme.fg("toolTitle", theme.bold(
|
|
48
|
+
text.setText(fillToolBackground(`\n ${theme.fg("toolTitle", theme.bold(`$ ${cmd}`))}${t}`, ctx.isError ? BG_ERROR : undefined, toolWidth));
|
|
52
49
|
return text;
|
|
53
50
|
},
|
|
54
51
|
|
package/src/tools/find.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* pi-pretty: find tool -- FFF-backed file search with SDK fallback. */
|
|
2
2
|
|
|
3
3
|
import { type ToolDefinition, type ExtensionAPI, type ExtensionContext, type AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { isAbsolute, relative as toRelative } from "node:path";
|
|
4
5
|
import type { SdkToolDef, FindDetails, FffServiceLike, FileItem, TextContent, ThemeLike, RenderCtxLike, ComponentLike } from "../types.js";
|
|
5
6
|
import { resolveBaseBackground, BG_ERROR, FG_DIM, RST } from "../config.js";
|
|
6
7
|
import { shortPath } from "../helpers.js";
|
|
@@ -32,24 +33,35 @@ export function registerFindTool(
|
|
|
32
33
|
execute: wrapExecuteWithMetrics(async (tid, params, sig, _upd, ctx: ExtensionContext) => {
|
|
33
34
|
const pattern = String((params as any).pattern ?? "");
|
|
34
35
|
const path = (params as any).path ? String((params as any).path) : undefined;
|
|
35
|
-
const limit =
|
|
36
|
+
const limit = (params as any).limit;
|
|
37
|
+
const glob = (params as any).glob;
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
const fff = fffService?.isAvailable ? fffService.getFinder() : null;
|
|
40
|
+
if (fff) {
|
|
38
41
|
try {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
const notices: string[] = [];
|
|
46
|
-
if (fffService.partialIndex) notices.push("Warning: partial file index");
|
|
47
|
-
if (items.length >= effectiveLimit) notices.push(`${effectiveLimit} limit reached`);
|
|
48
|
-
if (searchResult.value.totalMatched > items.length) notices.push(`${searchResult.value.totalMatched} total matches`);
|
|
49
|
-
const text = appendNotices(items.map((i) => i.relativePath).join("\n"), notices);
|
|
50
|
-
return { content: [{ type: "text" as const, text }], details: { _type: "findResult", text, pattern, matchCount: items.length } as FindDetails };
|
|
42
|
+
const effectiveLimit = Math.max(1, typeof limit === "number" ? limit : 100);
|
|
43
|
+
const basePathResult = fff.getBasePath();
|
|
44
|
+
const basePath = basePathResult.ok ? basePathResult.value : null;
|
|
45
|
+
let cleanPath = path ?? "";
|
|
46
|
+
if (cleanPath && isAbsolute(cleanPath) && basePath) {
|
|
47
|
+
cleanPath = toRelative(basePath, cleanPath) || "";
|
|
51
48
|
}
|
|
52
|
-
|
|
49
|
+
cleanPath = cleanPath.replace(/\/$/, "");
|
|
50
|
+
const cleanPattern = pattern.startsWith("/") ? pattern.slice(1) : pattern;
|
|
51
|
+
const globPattern = cleanPath
|
|
52
|
+
? `${cleanPath}/**/${cleanPattern}`
|
|
53
|
+
: `**/${cleanPattern}`;
|
|
54
|
+
const searchResult = fff.glob(globPattern, { pageSize: effectiveLimit });
|
|
55
|
+
if (searchResult.ok) {
|
|
56
|
+
const items: FileItem[] = searchResult.value.items.slice(0, effectiveLimit);
|
|
57
|
+
const notices: string[] = [];
|
|
58
|
+
if (fffService?.partialIndex) notices.push("Warning: partial file index");
|
|
59
|
+
if (items.length >= effectiveLimit) notices.push(`${effectiveLimit} limit reached`);
|
|
60
|
+
if (searchResult.value.totalMatched > items.length) notices.push(`${searchResult.value.totalMatched} total matches`);
|
|
61
|
+
const paths = items.map((i) => i.relativePath).join("\n");
|
|
62
|
+
return { content: [{ type: "text" as const, text: paths }], details: { _type: "findResult", text: paths, pattern, matchCount: items.length, notices } as FindDetails };
|
|
63
|
+
}
|
|
64
|
+
} catch { /* fall through to SDK */ }
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
const result = await sdkTool.execute(tid, params, sig, undefined, ctx) as Result;
|
|
@@ -58,11 +70,20 @@ export function registerFindTool(
|
|
|
58
70
|
return result;
|
|
59
71
|
}),
|
|
60
72
|
|
|
61
|
-
|
|
73
|
+
renderCall(args: any, theme: ThemeLike, ctx: RenderCtxLike) {
|
|
62
74
|
resolveBaseBackground(theme);
|
|
63
75
|
const text = ctx.lastComponent ?? new TextComp!("", 0, 0);
|
|
64
|
-
const
|
|
65
|
-
|
|
76
|
+
const pattern = args.pattern === null || args.pattern === undefined ? "" : String(args.pattern);
|
|
77
|
+
const path = args.path === null || args.path === undefined ? "<missing>" : shortPath(cwd, home, String(args.path));
|
|
78
|
+
const limit = args.limit;
|
|
79
|
+
const glob = args.glob;
|
|
80
|
+
const findLabel = theme.fg("toolTitle", theme.bold("find"));
|
|
81
|
+
const patternPart = pattern ? theme.fg("accent", pattern) : "";
|
|
82
|
+
const inPart = theme.fg("dim", " in ");
|
|
83
|
+
const pathPart = theme.fg("toolOutput", path);
|
|
84
|
+
const limitPart = limit !== undefined && limit !== null ? theme.fg("dim", ` limit ${limit}`) : "";
|
|
85
|
+
const out = `${findLabel} ${patternPart}${inPart}${pathPart}${limitPart}`;
|
|
86
|
+
text.setText(fillToolBackground(` \n ${out}`, ctx.isError ? BG_ERROR : undefined));
|
|
66
87
|
return text;
|
|
67
88
|
},
|
|
68
89
|
|
|
@@ -71,13 +92,21 @@ export function registerFindTool(
|
|
|
71
92
|
const text = ctx.lastComponent ?? new TextComp!("", 0, 0);
|
|
72
93
|
if (ctx.isError) { text.setText(renderToolError(getText(result) || "Error", theme)); return text; }
|
|
73
94
|
const d = result.details as FindDetails | undefined;
|
|
74
|
-
if (d?._type === "findResult"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
95
|
+
if (d?._type === "findResult") {
|
|
96
|
+
if (!d.text.trim()) {
|
|
97
|
+
const notices = (d as any).notices as string[] | undefined;
|
|
98
|
+
const noticeStr = notices?.length ? `\n ${theme.fg("warning", `[${notices.join(". ")}]`)}` : "";
|
|
99
|
+
text.setText(fillToolBackground(` \n ${theme.fg("dim", "0 files")}${noticeStr}\n `));
|
|
100
|
+
return text;
|
|
101
|
+
}
|
|
102
|
+
const rendered = renderFindResults(d.text, theme).split("\n").map(l => ` ${l}`).join("\n");
|
|
103
|
+
const notices = (d as any).notices as string[] | undefined;
|
|
104
|
+
const noticeStr = notices?.length ? `\n ${theme.fg("warning", `[${notices.join(". ")}]`)}` : "";
|
|
105
|
+
text.setText(fillToolBackground(` \n ${theme.fg("dim", `${d.matchCount} files`)}${renderToolMetrics(result)}\n${rendered}${noticeStr}\n `));
|
|
106
|
+
return text;
|
|
78
107
|
}
|
|
79
108
|
const fc = result.content?.[0];
|
|
80
|
-
text.setText(fillToolBackground(` ${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "
|
|
109
|
+
text.setText(fillToolBackground(` \n ${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "0 files")}\n `));
|
|
81
110
|
return text;
|
|
82
111
|
},
|
|
83
112
|
} as unknown as ToolDefinition<any, any, any>);
|
package/src/tools/grep.ts
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import { type ToolDefinition, type ExtensionAPI, type ExtensionContext, type AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { SdkToolDef, GrepDetails, FffServiceWithCursor, TextContent, ThemeLike, RenderCtxLike } from "../types.js";
|
|
5
|
-
import {
|
|
5
|
+
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { MAX_PREVIEW_LINES, BG_ERROR, resolveBaseBackground } from "../config.js";
|
|
6
7
|
import { shortPath, normalizeLineEndings } from "../helpers.js";
|
|
7
8
|
import { wrapExecuteWithMetrics } from "./metrics.js";
|
|
8
|
-
import { renderToolError,
|
|
9
|
+
import { renderToolError, fillToolBackground } from "../render.js";
|
|
9
10
|
import { fffFormatGrepText } from "../fff-helpers.js";
|
|
10
11
|
|
|
12
|
+
const invalidArg = "<missing>";
|
|
13
|
+
|
|
11
14
|
type Result = AgentToolResult<Record<string, unknown>>;
|
|
12
15
|
|
|
13
16
|
export function registerGrepTool(
|
|
@@ -70,9 +73,18 @@ export function registerGrepTool(
|
|
|
70
73
|
renderCall(args: any, theme: ThemeLike, ctx: RenderCtxLike) {
|
|
71
74
|
resolveBaseBackground(theme);
|
|
72
75
|
const text = ctx.lastComponent ?? new T("", 0, 0);
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
+
const pattern = args.pattern === null || args.pattern === undefined ? invalidArg : String(args.pattern);
|
|
77
|
+
const path = args.path === null || args.path === undefined ? invalidArg : shortPath(cwd, home, String(args.path));
|
|
78
|
+
const glob = args.glob;
|
|
79
|
+
const limit = args.limit;
|
|
80
|
+
const literal = args.literal === true;
|
|
81
|
+
const caseInsensitive = args.caseInsensitive === true || args.ignoreCase === true;
|
|
82
|
+
let out = `${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", `/${pattern || ""}/`)}${theme.fg("toolOutput", ` in ${path}`)}`;
|
|
83
|
+
if (glob) out += theme.fg("dim", ` (${String(glob)})`);
|
|
84
|
+
if (limit !== undefined && limit !== null) out += theme.fg("dim", ` limit ${limit}`);
|
|
85
|
+
if (literal) out += theme.fg("dim", ` (literal)`);
|
|
86
|
+
if (caseInsensitive) out += theme.fg("dim", ` (case-insensitive)`);
|
|
87
|
+
text.setText(fillToolBackground(`\n ${out}`, ctx.isError ? BG_ERROR : undefined));
|
|
76
88
|
return text;
|
|
77
89
|
},
|
|
78
90
|
|
|
@@ -85,38 +97,22 @@ export function registerGrepTool(
|
|
|
85
97
|
const lines = d.text.split("\n");
|
|
86
98
|
const maxShow = ctx.expanded ? lines.length : Math.min(lines.length, MAX_PREVIEW_LINES);
|
|
87
99
|
const show = lines.slice(0, maxShow);
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
// Build highlight regex from pattern
|
|
91
|
-
let hlRe: RegExp | null = null;
|
|
92
|
-
try { hlRe = new RegExp(`(${d.pattern})`, "gi"); } catch {}
|
|
93
|
-
|
|
100
|
+
const remaining = lines.length - maxShow;
|
|
94
101
|
const out: string[] = [];
|
|
95
|
-
let currentFile = "";
|
|
96
102
|
for (const line of show) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
out.push(` ${theme.fg("accent", theme.bold(file))}`);
|
|
103
|
-
currentFile = file;
|
|
104
|
-
}
|
|
105
|
-
let display = content;
|
|
106
|
-
if (hlRe) display = content.replace(hlRe, (m) => `${RST}${theme.fg("warning", theme.bold(m))}${RST}`);
|
|
107
|
-
const padded = `${FG_LNUM}${String(lineNo).padStart(nw)}${RST} ${FG_RULE}│${RST} ${display}${RST}`;
|
|
108
|
-
out.push(` ${padded}`);
|
|
109
|
-
} else if (line.trim()) {
|
|
110
|
-
out.push(` ${FG_DIM} ${line.trim()}${RST}`);
|
|
111
|
-
}
|
|
103
|
+
if (!line) continue;
|
|
104
|
+
out.push(theme.fg("toolOutput", line));
|
|
105
|
+
}
|
|
106
|
+
if (remaining > 0) {
|
|
107
|
+
out.push(theme.fg("muted", `… (${remaining} more ${remaining === 1 ? "line" : "lines"}, ${keyHint("app.tools.expand", "to expand")})`));
|
|
112
108
|
}
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
text.setText(fillToolBackground(` ${FG_DIM}${d.matchCount} matches${RST}${renderToolMetrics(result)}\n${preview}${more}`));
|
|
109
|
+
const body = out.map((l) => ` ${l}`).join("\n") + "\n\n";
|
|
110
|
+
text.setText(fillToolBackground(body, ctx.isError ? BG_ERROR : undefined));
|
|
116
111
|
return text;
|
|
117
112
|
}
|
|
118
113
|
const fc = result.content?.[0];
|
|
119
|
-
|
|
114
|
+
const fallback = fc && "text" in fc ? String(fc.text).slice(0, 120) : "no matches";
|
|
115
|
+
text.setText(fillToolBackground(` ${theme.fg("dim", fallback)}`, ctx.isError ? BG_ERROR : undefined));
|
|
120
116
|
return text;
|
|
121
117
|
},
|
|
122
118
|
} as unknown as ToolDefinition<any, any, any>);
|
package/src/tools/ls.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { type ToolDefinition, type ExtensionAPI, type ExtensionContext, type AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import type { SdkToolDef, LsDetails, TextContent, ComponentLike, ThemeLike, RenderCtxLike } from "../types.js";
|
|
5
5
|
import { resolveBaseBackground, termWidth, MAX_PREVIEW_LINES, BG_BASE, BG_ERROR, FG_DIM, RST } from "../config.js";
|
|
6
|
+
import { shortPath } from "../helpers.js";
|
|
6
7
|
import { wrapExecuteWithMetrics } from "./metrics.js";
|
|
7
8
|
import { renderTree, renderToolError, renderToolMetrics, fillToolBackground } from "../render.js";
|
|
8
9
|
|
|
@@ -10,11 +11,12 @@ type Result = AgentToolResult<Record<string, unknown>>;
|
|
|
10
11
|
|
|
11
12
|
export function registerLsTool(
|
|
12
13
|
pi: ExtensionAPI,
|
|
13
|
-
|
|
14
|
+
cwd: string,
|
|
14
15
|
_fffService: unknown,
|
|
15
16
|
sdkTool: SdkToolDef,
|
|
16
17
|
TextComp?: new (t?: string, x?: number, y?: number) => { setText(v: string): void },
|
|
17
18
|
): void {
|
|
19
|
+
const home = process.env.HOME ?? "";
|
|
18
20
|
const TC = TextComp ?? (() => {
|
|
19
21
|
const { Text } = require("@earendil-works/pi-tui") as { Text: new (t?: string, x?: number, y?: number) => { setText(v: string): void } };
|
|
20
22
|
return Text;
|
|
@@ -36,10 +38,14 @@ export function registerLsTool(
|
|
|
36
38
|
|
|
37
39
|
renderCall(args: any, theme: ThemeLike, ctx: RenderCtxLike) {
|
|
38
40
|
resolveBaseBackground(theme);
|
|
39
|
-
|
|
40
41
|
const text = ctx.lastComponent ?? new TC("", 0, 0);
|
|
41
|
-
const
|
|
42
|
-
|
|
42
|
+
const rawPath = args.path;
|
|
43
|
+
const path = rawPath === null || rawPath === undefined || String(rawPath).length === 0 ? "" : shortPath(cwd, home, String(rawPath));
|
|
44
|
+
const limit = args.limit;
|
|
45
|
+
let out = theme.fg("toolTitle", theme.bold("ls"));
|
|
46
|
+
if (path) out += ` ${theme.fg("accent", path)}`;
|
|
47
|
+
if (limit !== undefined && limit !== null) out += theme.fg("toolOutput", ` (limit ${limit})`);
|
|
48
|
+
text.setText(fillToolBackground(` \n ${out}`, ctx.isError ? BG_ERROR : undefined));
|
|
43
49
|
return text;
|
|
44
50
|
},
|
|
45
51
|
|
|
@@ -51,7 +57,7 @@ export function registerLsTool(
|
|
|
51
57
|
const d = result.details as LsDetails | undefined;
|
|
52
58
|
if (d?._type === "lsResult" && d.text) {
|
|
53
59
|
const rendered = renderTree(d.text, d.path).split("\n").map(l => ` ${l}`).join("\n");
|
|
54
|
-
text.setText(fillToolBackground(` ${FG_DIM}${d.entryCount} entries${RST}${renderToolMetrics(result)}\n${rendered}`));
|
|
60
|
+
text.setText(fillToolBackground(` \n ${FG_DIM}${d.entryCount} entries${RST}${renderToolMetrics(result)}\n${rendered}\n `));
|
|
55
61
|
return text;
|
|
56
62
|
}
|
|
57
63
|
const fc = result.content?.[0];
|
package/src/tools/multi-grep.ts
CHANGED
|
@@ -136,12 +136,16 @@ export function registerMultiGrepTool(
|
|
|
136
136
|
|
|
137
137
|
renderCall(args: any, theme: ThemeLike, ctx: RenderCtxLike) {
|
|
138
138
|
resolveBaseBackground(theme);
|
|
139
|
-
|
|
140
139
|
const text = ctx.lastComponent ?? new TC("", 0, 0);
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
|
|
140
|
+
const patterns: string[] = Array.isArray(args.patterns) ? args.patterns.map((p: unknown) => String(p)) : [];
|
|
141
|
+
const limit = typeof args.limit === "number" ? args.limit : undefined;
|
|
142
|
+
const path = args.path === null || args.path === undefined ? "<missing>" : shortPath(cwd, home, String(args.path));
|
|
143
|
+
const literal = args.literal === true;
|
|
144
|
+
const patternStr = patterns.length === 0 ? "" : patterns.length === 1 ? patterns[0]! : patterns.length === 2 ? `${patterns[0]}|${patterns[1]}` : `${patterns[0]}|${patterns[1]}|+${patterns.length - 2}`;
|
|
145
|
+
let out = `${theme.fg("toolTitle", theme.bold("mgrep"))} ${theme.fg("accent", `/${patternStr || ""}/`)}${theme.fg("toolOutput", ` in ${path}`)}`;
|
|
146
|
+
if (literal) out += theme.fg("dim", ` (literal)`);
|
|
147
|
+
if (limit !== undefined) out += theme.fg("dim", ` limit ${limit}`);
|
|
148
|
+
text.setText(fillToolBackground(`\n ${out}`, ctx.isError ? BG_ERROR : undefined));
|
|
145
149
|
return text;
|
|
146
150
|
},
|
|
147
151
|
|
|
@@ -130,8 +130,8 @@ describe("bash renderCall expansion", () => {
|
|
|
130
130
|
invalidate: () => {},
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
-
expect(collapsed.getText()).toContain("5s
|
|
134
|
-
expect(expanded.getText()).toContain("5s
|
|
133
|
+
expect(collapsed.getText()).toContain("(timeout 5s)");
|
|
134
|
+
expect(expanded.getText()).toContain("(timeout 5s)");
|
|
135
135
|
});
|
|
136
136
|
|
|
137
137
|
it("truncates ANSI tool headers that exceed the terminal width", () => {
|
|
@@ -184,7 +184,7 @@ describe("bash renderCall expansion", () => {
|
|
|
184
184
|
});
|
|
185
185
|
|
|
186
186
|
const lines = stripAnsi(rendered.getText()).split("\n");
|
|
187
|
-
expect(lines[1]).toMatch(/^
|
|
187
|
+
expect(lines[1]).toMatch(/^ \$ false/);
|
|
188
188
|
});
|
|
189
189
|
});
|
|
190
190
|
|
|
@@ -249,13 +249,21 @@ function mkFinder(overrides?: Record<string, any>) {
|
|
|
249
249
|
return {
|
|
250
250
|
isDestroyed: false,
|
|
251
251
|
waitForScan: vi.fn().mockResolvedValue({ ok: true, value: true }),
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
252
|
+
fileSearch: vi.fn().mockReturnValue({
|
|
253
|
+
ok: true,
|
|
254
|
+
value: {
|
|
255
|
+
items: [{ relativePath: "src/index.ts" }, { relativePath: "src/main.ts" }],
|
|
256
|
+
totalMatched: 2,
|
|
257
|
+
},
|
|
258
|
+
}),
|
|
259
|
+
glob: vi.fn().mockReturnValue({
|
|
260
|
+
ok: true,
|
|
261
|
+
value: {
|
|
262
|
+
items: [{ relativePath: "src/index.ts" }, { relativePath: "src/main.ts" }],
|
|
263
|
+
totalMatched: 2,
|
|
264
|
+
},
|
|
265
|
+
}),
|
|
266
|
+
getBasePath: vi.fn().mockReturnValue({ ok: true, value: "/Users/test/proj" }),
|
|
259
267
|
grep: vi.fn().mockReturnValue({
|
|
260
268
|
ok: true,
|
|
261
269
|
value: {
|
|
@@ -489,52 +497,54 @@ describe("piPrettyExtension integration", () => {
|
|
|
489
497
|
expect(r.content[0].text).toContain("src/index.ts");
|
|
490
498
|
});
|
|
491
499
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
500
|
+
it("falls back to SDK on FFF { ok: false }", async () => {
|
|
501
|
+
await loadWithFFF({
|
|
502
|
+
glob: vi.fn().mockReturnValue({ ok: false, error: "fail" }),
|
|
503
|
+
});
|
|
504
|
+
await tools.get("find")!.execute("t1", { pattern: "*.ts" }, null, null, {});
|
|
505
|
+
expect(findExec).toHaveBeenCalledOnce();
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it("falls back to SDK on FFF throw", async () => {
|
|
509
|
+
await loadWithFFF({
|
|
510
|
+
glob: vi.fn().mockImplementation(() => { throw new Error("crash"); }),
|
|
511
|
+
});
|
|
512
|
+
await tools.get("find")!.execute("t1", { pattern: "*.ts" }, null, null, {});
|
|
513
|
+
expect(findExec).toHaveBeenCalledOnce();
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it("respects limit param", async () => {
|
|
517
|
+
const glob = vi.fn().mockReturnValue({
|
|
518
|
+
ok: true,
|
|
519
|
+
value: { items: Array.from({ length: 50 }, (_, i) => ({ relativePath: `f${i}.ts` })), totalMatched: 50 },
|
|
520
|
+
});
|
|
521
|
+
await loadWithFFF({ glob });
|
|
522
|
+
await tools.get("find")!.execute("t1", { pattern: "*.ts", limit: 5 }, null, null, {});
|
|
523
|
+
expect(glob).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ pageSize: 5 }));
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it("combines path and pattern into glob pattern", async () => {
|
|
527
|
+
const glob = vi.fn().mockReturnValue({ ok: true, value: { items: [], totalMatched: 0 } });
|
|
528
|
+
await loadWithFFF({ glob });
|
|
529
|
+
await tools.get("find")!.execute("t1", { pattern: "*.ts", path: "src" }, null, null, {});
|
|
530
|
+
expect(glob).toHaveBeenCalledWith("src/**/*.ts", expect.any(Object));
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it("shows partial-index + limit notices", async () => {
|
|
526
534
|
await loadWithFFF({
|
|
527
535
|
waitForScan: vi.fn().mockResolvedValue({ ok: true, value: false }),
|
|
528
|
-
|
|
536
|
+
glob: vi.fn().mockReturnValue({
|
|
529
537
|
ok: true,
|
|
530
538
|
value: { items: Array.from({ length: 200 }, (_, i) => ({ relativePath: `f${i}` })), totalMatched: 500 },
|
|
531
539
|
}),
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
expect(
|
|
536
|
-
expect(
|
|
540
|
+
});
|
|
541
|
+
const result = await tools.get("find")!.execute("t1", { pattern: "*", limit: 200 }, null, null, {});
|
|
542
|
+
const notices = (result.details as any).notices as string[];
|
|
543
|
+
expect(notices).toContain("Warning: partial file index");
|
|
544
|
+
expect(notices).toContain("200 limit reached");
|
|
545
|
+
expect(notices).toContain("500 total matches");
|
|
537
546
|
});
|
|
547
|
+
// ---- grep: FFF path ------------------------------------------------
|
|
538
548
|
});
|
|
539
549
|
|
|
540
550
|
// ---- grep: FFF path ------------------------------------------------
|