@heyhuynhgiabuu/pi-pretty 0.6.6 → 0.6.8
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 +31 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +15 -5
- package/dist/render.js.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +37 -15
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/find.d.ts.map +1 -1
- package/dist/tools/find.js +38 -16
- package/dist/tools/find.js.map +1 -1
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +39 -11
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/ls.d.ts.map +1 -1
- package/dist/tools/ls.js +20 -9
- package/dist/tools/ls.js.map +1 -1
- package/dist/tools/multi-grep.d.ts.map +1 -1
- package/dist/tools/multi-grep.js +118 -22
- package/dist/tools/multi-grep.js.map +1 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +33 -13
- package/dist/tools/read.js.map +1 -1
- package/dist/tui-text.d.ts +25 -0
- package/dist/tui-text.d.ts.map +1 -0
- package/dist/tui-text.js +55 -0
- package/dist/tui-text.js.map +1 -0
- package/package.json +1 -1
- package/src/render.ts +90 -53
- package/src/tools/bash.ts +77 -22
- package/src/tools/find.ts +91 -40
- package/src/tools/grep.ts +57 -13
- package/src/tools/ls.ts +42 -14
- package/src/tools/multi-grep.ts +162 -32
- package/src/tools/read.ts +79 -25
- package/src/tui-text.ts +56 -0
package/src/render.ts
CHANGED
|
@@ -7,22 +7,48 @@
|
|
|
7
7
|
|
|
8
8
|
import type { BundledLanguage } from "shiki";
|
|
9
9
|
import { codeToANSI } from "@shikijs/cli";
|
|
10
|
-
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
11
10
|
import { basename, dirname } from "node:path";
|
|
12
11
|
|
|
13
12
|
import {
|
|
14
|
-
RST,
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
RST,
|
|
14
|
+
FG_LNUM,
|
|
15
|
+
FG_DIM,
|
|
16
|
+
FG_RULE,
|
|
17
|
+
FG_GREEN,
|
|
18
|
+
FG_RED,
|
|
19
|
+
FG_YELLOW,
|
|
20
|
+
FG_BLUE,
|
|
21
|
+
BG_BASE,
|
|
22
|
+
BG_ERROR,
|
|
23
|
+
dirIcon,
|
|
24
|
+
detectLang,
|
|
25
|
+
termWidth,
|
|
26
|
+
MAX_PREVIEW_LINES,
|
|
27
|
+
MAX_HL_CHARS,
|
|
28
|
+
CACHE_LIMIT,
|
|
17
29
|
resolveBaseBackground,
|
|
18
30
|
} from "./config.js";
|
|
19
31
|
import {
|
|
20
|
-
normalizeLineEndings,
|
|
21
|
-
|
|
32
|
+
normalizeLineEndings,
|
|
33
|
+
humanSize,
|
|
34
|
+
formatElapsedMs,
|
|
35
|
+
formatCharCount,
|
|
36
|
+
ELAPSED_KEY,
|
|
37
|
+
CHARS_KEY,
|
|
38
|
+
compactErrorLines,
|
|
22
39
|
} from "./helpers.js";
|
|
23
40
|
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
24
41
|
import type { ThemeLike, RenderCtxLike as RenderContext } from "./types.js";
|
|
25
42
|
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Lazy imports — avoid top-level require() that blocks module loading
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
/** Lazy accessor to pi-tui's truncateToWidth */
|
|
48
|
+
function _truncateToWidth(text: string, maxWidth: number, ellipsis?: string, pad?: boolean): string {
|
|
49
|
+
return require("@earendil-works/pi-tui").truncateToWidth(text, maxWidth, ellipsis, pad);
|
|
50
|
+
}
|
|
51
|
+
|
|
26
52
|
// ---------------------------------------------------------------------------
|
|
27
53
|
// Shiki ANSI cache
|
|
28
54
|
// ---------------------------------------------------------------------------
|
|
@@ -113,9 +139,15 @@ export function preserveBoxBackground(ansi: string): string {
|
|
|
113
139
|
if (code === 38) {
|
|
114
140
|
// Foreground extended — keep entire sequence
|
|
115
141
|
kept.push(parts[i]);
|
|
116
|
-
if (parts[i + 1] === "5") {
|
|
117
|
-
|
|
118
|
-
|
|
142
|
+
if (parts[i + 1] === "5") {
|
|
143
|
+
kept.push(parts[i + 1]);
|
|
144
|
+
i += 2;
|
|
145
|
+
} else if (parts[i + 1] === "2") {
|
|
146
|
+
kept.push(parts[i + 1], parts[i + 2], parts[i + 3], parts[i + 4]);
|
|
147
|
+
i += 5;
|
|
148
|
+
} else {
|
|
149
|
+
i++;
|
|
150
|
+
}
|
|
119
151
|
} else if (code === 48) {
|
|
120
152
|
// Background extended — skip entirely
|
|
121
153
|
if (parts[i + 1] === "5") i += 3;
|
|
@@ -136,7 +168,7 @@ export function fillToolBackground(text: string, bg = BG_BASE, width?: number):
|
|
|
136
168
|
return text
|
|
137
169
|
.split("\n")
|
|
138
170
|
.map((line) => {
|
|
139
|
-
const fitted = width ?
|
|
171
|
+
const fitted = width ? _truncateToWidth(line, width, "") : line;
|
|
140
172
|
const stripped = preserveBoxBackground(fitted);
|
|
141
173
|
// Apply background to the entire line
|
|
142
174
|
return bg ? bg + stripped : stripped;
|
|
@@ -198,7 +230,7 @@ export async function renderFileContent(
|
|
|
198
230
|
|
|
199
231
|
const out: string[] = [];
|
|
200
232
|
for (const line of hl) {
|
|
201
|
-
out.push(
|
|
233
|
+
out.push(_truncateToWidth(line ?? "", Math.max(1, tw), `${FG_DIM}›`));
|
|
202
234
|
}
|
|
203
235
|
return out.join("\n");
|
|
204
236
|
}
|
|
@@ -210,9 +242,10 @@ export async function renderFileContent(
|
|
|
210
242
|
export function renderBashOutput(text: string, exitCode: number | null): { summary: string; body: string } {
|
|
211
243
|
const isOk = exitCode === 0;
|
|
212
244
|
const statusIcon = isOk ? "✓" : "✗";
|
|
213
|
-
const codeStr =
|
|
214
|
-
|
|
215
|
-
|
|
245
|
+
const codeStr =
|
|
246
|
+
exitCode !== null
|
|
247
|
+
? `${isOk ? FG_GREEN : FG_RED}${statusIcon} exit ${exitCode}${RST}`
|
|
248
|
+
: `${FG_YELLOW}⚡ killed${RST}`;
|
|
216
249
|
|
|
217
250
|
const lines = text.split("\n");
|
|
218
251
|
const maxShow = MAX_PREVIEW_LINES;
|
|
@@ -255,44 +288,46 @@ export function renderTree(text: string, _basePath: string): string {
|
|
|
255
288
|
return out.join("\n");
|
|
256
289
|
}
|
|
257
290
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// Find — grouped file list (plain, no tree characters or icons)
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
export function renderFindResults(text: string, theme?: ThemeLike): string {
|
|
296
|
+
const lines = text.trim().split("\n").filter(Boolean);
|
|
297
|
+
if (!lines.length) return theme ? theme.fg("dim", "(no matches)") : `${FG_DIM}(no matches)${RST}`;
|
|
298
|
+
|
|
299
|
+
const groups = new Map<string, string[]>();
|
|
300
|
+
for (const line of lines) {
|
|
301
|
+
const trimmed = line.trim();
|
|
302
|
+
const dir = dirname(trimmed) || ".";
|
|
303
|
+
const file = basename(trimmed);
|
|
304
|
+
if (!groups.has(dir)) groups.set(dir, []);
|
|
305
|
+
const bucket = groups.get(dir);
|
|
306
|
+
if (bucket) bucket.push(file);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const out: string[] = [];
|
|
310
|
+
let count = 0;
|
|
311
|
+
|
|
312
|
+
for (const [dir, files] of groups) {
|
|
313
|
+
if (count > 0) out.push("");
|
|
314
|
+
const dirColored = theme ? theme.fg("accent", theme.bold(`${dir}/`)) : `${FG_BLUE}\x1b[1m${dir}/${RST}`;
|
|
315
|
+
out.push(dirColored);
|
|
316
|
+
for (let i = 0; i < files.length; i++) {
|
|
317
|
+
if (count >= MAX_PREVIEW_LINES) {
|
|
318
|
+
const more = theme
|
|
319
|
+
? theme.fg("dim", `… ${lines.length - count} more files`)
|
|
320
|
+
: `${FG_DIM}… ${lines.length - count} more files${RST}`;
|
|
321
|
+
out.push(` ${more}`);
|
|
322
|
+
return out.join("\n");
|
|
323
|
+
}
|
|
324
|
+
out.push(` ${files[i]}`);
|
|
325
|
+
count++;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return out.join("\n");
|
|
330
|
+
}
|
|
296
331
|
|
|
297
332
|
// ---------------------------------------------------------------------------
|
|
298
333
|
// Grep — highlighted matches with line numbers
|
|
@@ -309,7 +344,9 @@ export async function renderGrepResults(text: string, pattern: string): Promise<
|
|
|
309
344
|
let re: RegExp | null = null;
|
|
310
345
|
try {
|
|
311
346
|
re = new RegExp(`(${pattern})`, "gi");
|
|
312
|
-
} catch {
|
|
347
|
+
} catch {
|
|
348
|
+
/* skip highlighting */
|
|
349
|
+
}
|
|
313
350
|
|
|
314
351
|
for (const line of lines) {
|
|
315
352
|
if (count >= MAX_PREVIEW_LINES) {
|
package/src/tools/bash.ts
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
/* pi-pretty: bash tool -- command execution with styled output. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type ToolDefinition,
|
|
5
|
+
type ExtensionAPI,
|
|
6
|
+
type ExtensionContext,
|
|
7
|
+
type AgentToolResult,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
4
9
|
import type { SdkToolDef, BashDetails, TextContent, ComponentLike, ThemeLike, RenderCtxLike } from "../types.js";
|
|
5
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
resolveBaseBackground,
|
|
12
|
+
termWidth,
|
|
13
|
+
MAX_PREVIEW_LINES,
|
|
14
|
+
BG_BASE,
|
|
15
|
+
BG_ERROR,
|
|
16
|
+
FG_DIM,
|
|
17
|
+
FG_RULE,
|
|
18
|
+
RST,
|
|
19
|
+
} from "../config.js";
|
|
6
20
|
import { wrapExecuteWithMetrics } from "./metrics.js";
|
|
7
21
|
import { renderBashOutput, renderToolError, renderToolMetrics, fillToolBackground } from "../render.js";
|
|
22
|
+
import { resolveTextCtor } from "../tui-text.js";
|
|
8
23
|
import { stripBashExitStatusLine, inferBashExitCode, compactErrorLines } from "../helpers.js";
|
|
9
24
|
|
|
10
25
|
type Result = AgentToolResult<Record<string, unknown>>;
|
|
@@ -16,10 +31,7 @@ export function registerBashTool(
|
|
|
16
31
|
sdkTool: SdkToolDef,
|
|
17
32
|
TextComp?: new (t?: string, x?: number, y?: number) => { setText(v: string): void },
|
|
18
33
|
): void {
|
|
19
|
-
const TC = TextComp
|
|
20
|
-
const { Text } = require("@earendil-works/pi-tui") as { Text: new (t?: string, x?: number, y?: number) => { setText(v: string): void } };
|
|
21
|
-
return Text;
|
|
22
|
-
})();
|
|
34
|
+
const TC = resolveTextCtor(TextComp);
|
|
23
35
|
|
|
24
36
|
pi.registerTool({
|
|
25
37
|
name: "bash",
|
|
@@ -30,10 +42,19 @@ export function registerBashTool(
|
|
|
30
42
|
|
|
31
43
|
execute: wrapExecuteWithMetrics(async (tid, params, sig, _upd, ctx: ExtensionContext) => {
|
|
32
44
|
try {
|
|
33
|
-
return await sdkTool.execute(tid, params, sig, undefined, ctx) as Result;
|
|
45
|
+
return (await sdkTool.execute(tid, params, sig, undefined, ctx)) as Result;
|
|
34
46
|
} catch (error: unknown) {
|
|
35
47
|
const msg = error instanceof Error ? error.message : String(error);
|
|
36
|
-
return {
|
|
48
|
+
return {
|
|
49
|
+
content: [{ type: "text" as const, text: msg }],
|
|
50
|
+
isError: true,
|
|
51
|
+
details: {
|
|
52
|
+
_type: "bashResult",
|
|
53
|
+
text: msg,
|
|
54
|
+
exitCode: 1,
|
|
55
|
+
command: String((params as any).command ?? ""),
|
|
56
|
+
} as BashDetails,
|
|
57
|
+
};
|
|
37
58
|
}
|
|
38
59
|
}),
|
|
39
60
|
|
|
@@ -43,23 +64,40 @@ export function registerBashTool(
|
|
|
43
64
|
const t = typeof args.timeout === "number" ? ` ${theme.fg("muted", `(timeout ${args.timeout}s)`)}` : "";
|
|
44
65
|
const tw = termWidth() || 80;
|
|
45
66
|
const rawCmd = String(args.command ?? "");
|
|
46
|
-
const cmd =
|
|
67
|
+
const cmd =
|
|
68
|
+
rawCmd.length === 0
|
|
69
|
+
? theme.fg("toolOutput", "...")
|
|
70
|
+
: rawCmd.length > tw - 20
|
|
71
|
+
? rawCmd.slice(0, Math.max(1, tw - 20)) + "…"
|
|
72
|
+
: rawCmd;
|
|
47
73
|
const toolWidth = ctx.expanded ? undefined : tw;
|
|
48
|
-
text.setText(
|
|
74
|
+
text.setText(
|
|
75
|
+
fillToolBackground(
|
|
76
|
+
`\n ${theme.fg("toolTitle", theme.bold(`$ ${cmd}`))}${t}`,
|
|
77
|
+
ctx.isError ? BG_ERROR : undefined,
|
|
78
|
+
toolWidth,
|
|
79
|
+
),
|
|
80
|
+
);
|
|
49
81
|
return text;
|
|
50
82
|
},
|
|
51
83
|
|
|
52
84
|
renderResult(result: Result, _opt: unknown, theme: ThemeLike, ctx: RenderCtxLike) {
|
|
53
85
|
resolveBaseBackground(theme);
|
|
54
|
-
|
|
86
|
+
|
|
55
87
|
const text = ctx.lastComponent ?? new TC("", 0, 0);
|
|
56
88
|
|
|
57
89
|
const details = result.details;
|
|
58
90
|
const tc = getText(result);
|
|
59
91
|
const d: BashDetails | undefined =
|
|
60
|
-
(details as BashDetails)?._type === "bashResult"
|
|
92
|
+
(details as BashDetails)?._type === "bashResult"
|
|
93
|
+
? (details as BashDetails)
|
|
61
94
|
: tc || ctx.isError
|
|
62
|
-
? {
|
|
95
|
+
? {
|
|
96
|
+
_type: "bashResult",
|
|
97
|
+
text: tc || "Error",
|
|
98
|
+
exitCode: inferBashExitCode(tc, ctx.isError ? 1 : 0),
|
|
99
|
+
command: "",
|
|
100
|
+
}
|
|
63
101
|
: undefined;
|
|
64
102
|
|
|
65
103
|
if (d?._type === "bashResult") {
|
|
@@ -69,7 +107,10 @@ export function registerBashTool(
|
|
|
69
107
|
const output = isErr ? compactErrorLines(cleaned).join("\n") : cleaned;
|
|
70
108
|
const { summary } = renderBashOutput(output, d.exitCode);
|
|
71
109
|
const lineCount = output.split("\n").length;
|
|
72
|
-
const info =
|
|
110
|
+
const info =
|
|
111
|
+
lineCount > 1
|
|
112
|
+
? ` ${FG_DIM}(${lineCount} lines)${RST} ${renderToolMetrics(result)}`
|
|
113
|
+
: ` ${renderToolMetrics(result)}`;
|
|
73
114
|
const header = ` ${summary}${info}`;
|
|
74
115
|
const rw = termWidth();
|
|
75
116
|
|
|
@@ -83,30 +124,44 @@ export function registerBashTool(
|
|
|
83
124
|
};
|
|
84
125
|
|
|
85
126
|
text.setText(renderFn(rw));
|
|
86
|
-
const baseRender =
|
|
87
|
-
? (text as ComponentLike).render.bind(text)
|
|
88
|
-
: null;
|
|
127
|
+
const baseRender =
|
|
128
|
+
typeof (text as ComponentLike).render === "function" ? (text as ComponentLike).render.bind(text) : null;
|
|
89
129
|
if (baseRender) {
|
|
90
130
|
let key: string | undefined;
|
|
91
131
|
(text as unknown as Record<string, unknown>).render = (w: number) => {
|
|
92
132
|
const width = Math.max(1, Math.floor(w || termWidth()));
|
|
93
133
|
const k = `bash:${ctx.expanded ? "1" : "0"}:${width}:${d.exitCode ?? "killed"}:${output.length}:${renderToolMetrics(result)}`;
|
|
94
|
-
if (key !== k) {
|
|
134
|
+
if (key !== k) {
|
|
135
|
+
text.setText(renderFn(width));
|
|
136
|
+
key = k;
|
|
137
|
+
}
|
|
95
138
|
return baseRender(width);
|
|
96
139
|
};
|
|
97
140
|
}
|
|
98
141
|
return text;
|
|
99
142
|
}
|
|
100
143
|
|
|
101
|
-
if (ctx.isError) {
|
|
144
|
+
if (ctx.isError) {
|
|
145
|
+
text.setText(renderToolError(tc || "Error", theme));
|
|
146
|
+
return text;
|
|
147
|
+
}
|
|
102
148
|
const fc = result.content?.[0];
|
|
103
|
-
text.setText(
|
|
149
|
+
text.setText(
|
|
150
|
+
fillToolBackground(` ${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`),
|
|
151
|
+
);
|
|
104
152
|
return text;
|
|
105
153
|
},
|
|
106
154
|
} as unknown as ToolDefinition<any, any, any>);
|
|
107
155
|
}
|
|
108
156
|
|
|
109
|
-
function rule(w: number): string {
|
|
157
|
+
function rule(w: number): string {
|
|
158
|
+
return `${FG_RULE}${"\u2500".repeat(w)}${RST}`;
|
|
159
|
+
}
|
|
110
160
|
function getText(result: Result): string {
|
|
111
|
-
return (
|
|
161
|
+
return (
|
|
162
|
+
((result.content ?? []) as TextContent[])
|
|
163
|
+
.filter((c) => c.type === "text")
|
|
164
|
+
.map((c) => c.text)
|
|
165
|
+
.join("\n") ?? ""
|
|
166
|
+
);
|
|
112
167
|
}
|
package/src/tools/find.ts
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
/* pi-pretty: find tool -- FFF-backed file search with SDK fallback. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type ToolDefinition,
|
|
5
|
+
type ExtensionAPI,
|
|
6
|
+
type ExtensionContext,
|
|
7
|
+
type AgentToolResult,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
4
9
|
import { isAbsolute, relative as toRelative } from "node:path";
|
|
5
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
SdkToolDef,
|
|
12
|
+
FindDetails,
|
|
13
|
+
FffServiceLike,
|
|
14
|
+
FileItem,
|
|
15
|
+
TextContent,
|
|
16
|
+
ThemeLike,
|
|
17
|
+
RenderCtxLike,
|
|
18
|
+
ComponentLike,
|
|
19
|
+
} from "../types.js";
|
|
6
20
|
import { resolveBaseBackground, BG_ERROR, FG_DIM, RST } from "../config.js";
|
|
7
21
|
import { shortPath } from "../helpers.js";
|
|
8
22
|
import { wrapExecuteWithMetrics } from "./metrics.js";
|
|
9
23
|
import { renderFindResults, renderToolError, renderToolMetrics, fillToolBackground } from "../render.js";
|
|
24
|
+
import { resolveTextCtor } from "../tui-text.js";
|
|
10
25
|
import { NOTICE_PARTIAL_FILE_INDEX } from "../notices.js";
|
|
11
26
|
|
|
12
27
|
type Result = AgentToolResult<Record<string, unknown>>;
|
|
@@ -18,10 +33,7 @@ export function registerFindTool(
|
|
|
18
33
|
sdkTool: SdkToolDef,
|
|
19
34
|
TextComp?: new (t?: string, x?: number, y?: number) => { setText(v: string): void },
|
|
20
35
|
): void {
|
|
21
|
-
|
|
22
|
-
const { Text } = require("@earendil-works/pi-tui") as { Text: new (t?: string, x?: number, y?: number) => { setText(v: string): void; render(w: number): string[]; invalidate(): void } };
|
|
23
|
-
TextComp = Text;
|
|
24
|
-
}
|
|
36
|
+
const TC = resolveTextCtor(TextComp);
|
|
25
37
|
const home = process.env.HOME ?? "";
|
|
26
38
|
|
|
27
39
|
pi.registerTool({
|
|
@@ -49,33 +61,49 @@ export function registerFindTool(
|
|
|
49
61
|
}
|
|
50
62
|
cleanPath = cleanPath.replace(/\/$/, "");
|
|
51
63
|
const cleanPattern = pattern.startsWith("/") ? pattern.slice(1) : pattern;
|
|
52
|
-
const globPattern = cleanPath
|
|
53
|
-
? `${cleanPath}/**/${cleanPattern}`
|
|
54
|
-
: `**/${cleanPattern}`;
|
|
64
|
+
const globPattern = cleanPath ? `${cleanPath}/**/${cleanPattern}` : `**/${cleanPattern}`;
|
|
55
65
|
const searchResult = fff.glob(globPattern, { pageSize: effectiveLimit });
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
+
if (searchResult.ok) {
|
|
67
|
+
const items: FileItem[] = searchResult.value.items.slice(0, effectiveLimit);
|
|
68
|
+
const notices: string[] = [];
|
|
69
|
+
if (fffService?.partialIndex) notices.push(NOTICE_PARTIAL_FILE_INDEX);
|
|
70
|
+
if (items.length >= effectiveLimit) notices.push(`${effectiveLimit} limit reached`);
|
|
71
|
+
if (searchResult.value.totalMatched > items.length)
|
|
72
|
+
notices.push(`${searchResult.value.totalMatched} total matches`);
|
|
73
|
+
const paths = items.map((i) => i.relativePath).join("\n");
|
|
74
|
+
return {
|
|
75
|
+
content: [{ type: "text" as const, text: paths }],
|
|
76
|
+
details: {
|
|
77
|
+
_type: "findResult",
|
|
78
|
+
text: paths,
|
|
79
|
+
pattern,
|
|
80
|
+
matchCount: items.length,
|
|
81
|
+
notices,
|
|
82
|
+
} as FindDetails,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
/* fall through to SDK */
|
|
87
|
+
}
|
|
66
88
|
}
|
|
67
89
|
|
|
68
|
-
const result = await sdkTool.execute(tid, params, sig, undefined, ctx) as Result;
|
|
90
|
+
const result = (await sdkTool.execute(tid, params, sig, undefined, ctx)) as Result;
|
|
69
91
|
const tc = getText(result);
|
|
70
|
-
result.details = {
|
|
92
|
+
result.details = {
|
|
93
|
+
_type: "findResult",
|
|
94
|
+
text: tc,
|
|
95
|
+
pattern,
|
|
96
|
+
matchCount: tc ? tc.trim().split("\n").filter(Boolean).length : 0,
|
|
97
|
+
} as FindDetails;
|
|
71
98
|
return result;
|
|
72
99
|
}),
|
|
73
100
|
|
|
74
|
-
|
|
101
|
+
renderCall(args: any, theme: ThemeLike, ctx: RenderCtxLike) {
|
|
75
102
|
resolveBaseBackground(theme);
|
|
76
|
-
const text = ctx.lastComponent ?? new
|
|
103
|
+
const text = ctx.lastComponent ?? new TC("", 0, 0);
|
|
77
104
|
const pattern = args.pattern === null || args.pattern === undefined ? "" : String(args.pattern);
|
|
78
|
-
const path =
|
|
105
|
+
const path =
|
|
106
|
+
args.path === null || args.path === undefined ? "<missing>" : shortPath(cwd, home, String(args.path));
|
|
79
107
|
const limit = args.limit;
|
|
80
108
|
const glob = args.glob;
|
|
81
109
|
const findLabel = theme.fg("toolTitle", theme.bold("find"));
|
|
@@ -90,28 +118,51 @@ export function registerFindTool(
|
|
|
90
118
|
|
|
91
119
|
renderResult(result: Result, _opt: unknown, theme: ThemeLike, ctx: RenderCtxLike) {
|
|
92
120
|
resolveBaseBackground(theme);
|
|
93
|
-
const text = ctx.lastComponent ?? new
|
|
94
|
-
if (ctx.isError) {
|
|
121
|
+
const text = ctx.lastComponent ?? new TC("", 0, 0);
|
|
122
|
+
if (ctx.isError) {
|
|
123
|
+
text.setText(renderToolError(getText(result) || "Error", theme));
|
|
124
|
+
return text;
|
|
125
|
+
}
|
|
95
126
|
const d = result.details as FindDetails | undefined;
|
|
96
127
|
if (d?._type === "findResult") {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
128
|
+
if (!d.text.trim()) {
|
|
129
|
+
const notices = (d as any).notices as string[] | undefined;
|
|
130
|
+
const noticeStr = notices?.length ? `\n ${theme.fg("warning", `[${notices.join(". ")}]`)}` : "";
|
|
131
|
+
text.setText(fillToolBackground(` \n ${theme.fg("dim", "0 files")}${noticeStr}\n `));
|
|
132
|
+
return text;
|
|
133
|
+
}
|
|
134
|
+
const rendered = renderFindResults(d.text, theme)
|
|
135
|
+
.split("\n")
|
|
136
|
+
.map((l) => ` ${l}`)
|
|
137
|
+
.join("\n");
|
|
138
|
+
const notices = (d as any).notices as string[] | undefined;
|
|
139
|
+
const noticeStr = notices?.length ? `\n ${theme.fg("warning", `[${notices.join(". ")}]`)}` : "";
|
|
140
|
+
text.setText(
|
|
141
|
+
fillToolBackground(
|
|
142
|
+
` \n ${theme.fg("dim", `${d.matchCount} files`)}${renderToolMetrics(result)}\n${rendered}${noticeStr}\n `,
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
return text;
|
|
108
146
|
}
|
|
109
147
|
const fc = result.content?.[0];
|
|
110
|
-
text.setText(
|
|
148
|
+
text.setText(
|
|
149
|
+
fillToolBackground(
|
|
150
|
+
` \n ${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "0 files")}\n `,
|
|
151
|
+
),
|
|
152
|
+
);
|
|
111
153
|
return text;
|
|
112
154
|
},
|
|
113
155
|
} as unknown as ToolDefinition<any, any, any>);
|
|
114
156
|
}
|
|
115
157
|
|
|
116
|
-
function appendNotices(text: string, notices: string[]): string {
|
|
117
|
-
|
|
158
|
+
function appendNotices(text: string, notices: string[]): string {
|
|
159
|
+
return notices.length ? `${text}\n\n[${notices.join(". ")}]` : text;
|
|
160
|
+
}
|
|
161
|
+
function getText(result: Result): string {
|
|
162
|
+
return (
|
|
163
|
+
((result.content ?? []) as TextContent[])
|
|
164
|
+
.filter((c) => c.type === "text")
|
|
165
|
+
.map((c) => c.text ?? "")
|
|
166
|
+
.join("\n") ?? ""
|
|
167
|
+
);
|
|
168
|
+
}
|
package/src/tools/grep.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
/* pi-pretty: grep tool -- FFF-backed text search with SDK fallback. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type ToolDefinition,
|
|
5
|
+
type ExtensionAPI,
|
|
6
|
+
type ExtensionContext,
|
|
7
|
+
type AgentToolResult,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
4
9
|
import type { SdkToolDef, GrepDetails, FffServiceWithCursor, TextContent, ThemeLike, RenderCtxLike } from "../types.js";
|
|
5
|
-
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
6
10
|
import { MAX_PREVIEW_LINES, BG_ERROR, resolveBaseBackground } from "../config.js";
|
|
7
11
|
import { shortPath, normalizeLineEndings } from "../helpers.js";
|
|
8
12
|
import { wrapExecuteWithMetrics } from "./metrics.js";
|
|
9
13
|
import { renderToolError, fillToolBackground } from "../render.js";
|
|
14
|
+
import { resolveTextCtor } from "../tui-text.js";
|
|
10
15
|
import { fffFormatGrepText } from "../fff-helpers.js";
|
|
11
16
|
import { NOTICE_PARTIAL_FILE_INDEX } from "../notices.js";
|
|
12
17
|
|
|
@@ -21,7 +26,7 @@ export function registerGrepTool(
|
|
|
21
26
|
sdkTool: SdkToolDef,
|
|
22
27
|
TextComp?: new (t?: string, x?: number, y?: number) => { setText(v: string): void },
|
|
23
28
|
): void {
|
|
24
|
-
const T = TextComp
|
|
29
|
+
const T = resolveTextCtor(TextComp);
|
|
25
30
|
const home = process.env.HOME ?? "";
|
|
26
31
|
|
|
27
32
|
pi.registerTool({
|
|
@@ -45,7 +50,12 @@ export function registerGrepTool(
|
|
|
45
50
|
const fff = fffService.getFinder();
|
|
46
51
|
if (!fff) throw new Error("FFF finder not available");
|
|
47
52
|
const effectiveLimit = Math.max(1, limit);
|
|
48
|
-
const grepResult = fff.grep(pattern, {
|
|
53
|
+
const grepResult = fff.grep(pattern, {
|
|
54
|
+
pageSize: effectiveLimit,
|
|
55
|
+
mode: literal ? "plain" : "regex",
|
|
56
|
+
beforeContext: context,
|
|
57
|
+
afterContext: context,
|
|
58
|
+
});
|
|
49
59
|
if (grepResult.ok) {
|
|
50
60
|
const grep = grepResult.value;
|
|
51
61
|
const items = grep.items.slice(0, effectiveLimit);
|
|
@@ -59,15 +69,36 @@ export function registerGrepTool(
|
|
|
59
69
|
notices.push(`More results available: cursor="${cursorId}"`);
|
|
60
70
|
}
|
|
61
71
|
const text = appendNotices(fffFormatGrepText(items, effectiveLimit), notices);
|
|
62
|
-
return {
|
|
72
|
+
return {
|
|
73
|
+
content: [{ type: "text" as const, text }],
|
|
74
|
+
details: {
|
|
75
|
+
_type: "grepResult",
|
|
76
|
+
text,
|
|
77
|
+
pattern,
|
|
78
|
+
matchCount: items.length,
|
|
79
|
+
} as GrepDetails,
|
|
80
|
+
};
|
|
63
81
|
}
|
|
64
|
-
} catch {
|
|
82
|
+
} catch {
|
|
83
|
+
/* fall through */
|
|
84
|
+
}
|
|
65
85
|
}
|
|
66
86
|
|
|
67
|
-
const result = await sdkTool.execute(tid, p, sig, undefined, ctx) as Result;
|
|
68
|
-
for (const c of (result.content ?? []) as any[]) {
|
|
69
|
-
|
|
70
|
-
|
|
87
|
+
const result = (await sdkTool.execute(tid, p, sig, undefined, ctx)) as Result;
|
|
88
|
+
for (const c of (result.content ?? []) as any[]) {
|
|
89
|
+
if (c.type === "text") c.text = normalizeLineEndings(c.text);
|
|
90
|
+
}
|
|
91
|
+
const tc =
|
|
92
|
+
((result.content ?? []) as TextContent[])
|
|
93
|
+
.filter((c) => c.type === "text")
|
|
94
|
+
.map((c) => c.text)
|
|
95
|
+
.join("\n") ?? "";
|
|
96
|
+
result.details = {
|
|
97
|
+
_type: "grepResult",
|
|
98
|
+
text: tc,
|
|
99
|
+
pattern,
|
|
100
|
+
matchCount: tc ? tc.trim().split("\n").filter(Boolean).length : 0,
|
|
101
|
+
} as GrepDetails;
|
|
71
102
|
return result;
|
|
72
103
|
}),
|
|
73
104
|
|
|
@@ -92,7 +123,18 @@ export function registerGrepTool(
|
|
|
92
123
|
renderResult(result: Result, _opt: unknown, theme: ThemeLike, ctx: RenderCtxLike) {
|
|
93
124
|
resolveBaseBackground(theme);
|
|
94
125
|
const text = ctx.lastComponent ?? new T("", 0, 0);
|
|
95
|
-
if (ctx.isError) {
|
|
126
|
+
if (ctx.isError) {
|
|
127
|
+
text.setText(
|
|
128
|
+
renderToolError(
|
|
129
|
+
((result.content ?? []) as TextContent[])
|
|
130
|
+
.filter((c) => c.type === "text")
|
|
131
|
+
.map((c) => c.text)
|
|
132
|
+
.join("\n") || "Error",
|
|
133
|
+
theme,
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
return text;
|
|
137
|
+
}
|
|
96
138
|
const d = result.details as GrepDetails | undefined;
|
|
97
139
|
if (d?._type === "grepResult" && d.text) {
|
|
98
140
|
const lines = d.text.split("\n");
|
|
@@ -105,7 +147,7 @@ export function registerGrepTool(
|
|
|
105
147
|
out.push(theme.fg("toolOutput", line));
|
|
106
148
|
}
|
|
107
149
|
if (remaining > 0) {
|
|
108
|
-
out.push(theme.fg("muted", `… (${remaining} more ${remaining === 1 ? "line" : "lines"},
|
|
150
|
+
out.push(theme.fg("muted", `… (${remaining} more ${remaining === 1 ? "line" : "lines"}, to expand)`));
|
|
109
151
|
}
|
|
110
152
|
const body = out.map((l) => ` ${l}`).join("\n") + "\n\n";
|
|
111
153
|
text.setText(fillToolBackground(body, ctx.isError ? BG_ERROR : undefined));
|
|
@@ -119,4 +161,6 @@ export function registerGrepTool(
|
|
|
119
161
|
} as unknown as ToolDefinition<any, any, any>);
|
|
120
162
|
}
|
|
121
163
|
|
|
122
|
-
function appendNotices(text: string, notices: string[]): string {
|
|
164
|
+
function appendNotices(text: string, notices: string[]): string {
|
|
165
|
+
return notices.length ? `${text}\n\n[${notices.join(". ")}]` : text;
|
|
166
|
+
}
|