@heyhuynhgiabuu/pi-pretty 0.6.15 → 0.6.17
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 +19 -116
- package/README.md +8 -16
- package/dist/collapsed-hint.d.ts +7 -0
- package/dist/collapsed-hint.d.ts.map +1 -0
- package/dist/collapsed-hint.js +17 -0
- package/dist/collapsed-hint.js.map +1 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +64 -19
- package/dist/config.js.map +1 -1
- package/dist/expand.d.ts +7 -0
- package/dist/expand.d.ts.map +1 -0
- package/dist/expand.js +11 -0
- package/dist/expand.js.map +1 -0
- package/dist/find-glob.d.ts +9 -0
- package/dist/find-glob.d.ts.map +1 -0
- package/dist/find-glob.js +29 -0
- package/dist/find-glob.js.map +1 -0
- package/dist/image.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -19
- package/dist/index.js.map +1 -1
- package/dist/render.d.ts +3 -5
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +24 -19
- package/dist/render.js.map +1 -1
- package/dist/tools/bash.d.ts +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +22 -26
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/find.d.ts +3 -3
- package/dist/tools/find.d.ts.map +1 -1
- package/dist/tools/find.js +99 -50
- package/dist/tools/find.js.map +1 -1
- package/dist/tools/grep.d.ts +2 -2
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +12 -8
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/labels.d.ts +2 -0
- package/dist/tools/labels.d.ts.map +1 -0
- package/dist/tools/labels.js +7 -0
- package/dist/tools/labels.js.map +1 -0
- package/dist/tools/ls.d.ts +1 -1
- package/dist/tools/ls.d.ts.map +1 -1
- package/dist/tools/ls.js +9 -5
- package/dist/tools/ls.js.map +1 -1
- package/dist/tools/read.d.ts +1 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +34 -48
- package/dist/tools/read.js.map +1 -1
- package/dist/tui-text.d.ts.map +1 -1
- package/dist/tui-text.js +1 -5
- package/dist/tui-text.js.map +1 -1
- package/dist/types.d.ts +4 -27
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -4
- package/src/collapsed-hint.ts +14 -0
- package/src/config.ts +70 -22
- package/src/expand.ts +10 -0
- package/src/find-glob.ts +21 -0
- package/src/image.ts +1 -1
- package/src/index.ts +25 -32
- package/src/notices.ts +1 -1
- package/src/render.ts +50 -41
- package/src/tools/bash.ts +27 -48
- package/src/tools/find.ts +131 -86
- package/src/tools/grep.ts +23 -17
- package/src/tools/labels.ts +3 -0
- package/src/tools/ls.ts +27 -13
- package/src/tools/metrics.ts +1 -1
- package/src/tools/read.ts +55 -66
- package/src/tui-text.ts +1 -5
- package/src/types.ts +4 -35
- package/dist/multi-grep-fallback.d.ts +0 -30
- package/dist/multi-grep-fallback.d.ts.map +0 -1
- package/dist/multi-grep-fallback.js +0 -205
- package/dist/multi-grep-fallback.js.map +0 -1
- package/dist/tools/multi-grep.d.ts +0 -7
- package/dist/tools/multi-grep.d.ts.map +0 -1
- package/dist/tools/multi-grep.js +0 -294
- package/dist/tools/multi-grep.js.map +0 -1
- package/src/multi-grep-fallback.ts +0 -248
- package/src/tools/multi-grep.ts +0 -331
package/src/notices.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/** Stable user-facing strings for FFF/grep tool footers (not source-line titles). */
|
|
2
2
|
|
|
3
|
-
export const NOTICE_PARTIAL_FILE_INDEX = "FFF index incomplete; results may be missing files";
|
|
3
|
+
export const NOTICE_PARTIAL_FILE_INDEX = "FFF index incomplete; results may be missing files";
|
package/src/render.ts
CHANGED
|
@@ -5,40 +5,40 @@
|
|
|
5
5
|
* They are async only when Shiki syntax highlighting is involved.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { BundledLanguage } from "shiki";
|
|
9
|
-
import { codeToANSI } from "@shikijs/cli";
|
|
10
8
|
import { basename, dirname } from "node:path";
|
|
11
|
-
|
|
9
|
+
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { codeToANSI } from "@shikijs/cli";
|
|
11
|
+
import type { BundledLanguage } from "shiki";
|
|
12
12
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
BG_BASE,
|
|
14
|
+
BG_ERROR,
|
|
15
|
+
CACHE_LIMIT,
|
|
16
|
+
detectLang,
|
|
17
|
+
dirIcon,
|
|
18
|
+
FG_BLUE,
|
|
15
19
|
FG_DIM,
|
|
16
|
-
FG_RULE,
|
|
17
20
|
FG_GREEN,
|
|
21
|
+
FG_LNUM,
|
|
18
22
|
FG_RED,
|
|
23
|
+
FG_RULE,
|
|
19
24
|
FG_YELLOW,
|
|
20
|
-
FG_BLUE,
|
|
21
|
-
BG_BASE,
|
|
22
|
-
BG_ERROR,
|
|
23
|
-
dirIcon,
|
|
24
|
-
detectLang,
|
|
25
|
-
termWidth,
|
|
26
|
-
MAX_PREVIEW_LINES,
|
|
27
25
|
MAX_HL_CHARS,
|
|
28
|
-
|
|
26
|
+
MAX_PREVIEW_LINES,
|
|
27
|
+
RST,
|
|
29
28
|
resolveBaseBackground,
|
|
29
|
+
TOOL_RESULT_INDENT,
|
|
30
|
+
termWidth,
|
|
30
31
|
} from "./config.js";
|
|
31
32
|
import {
|
|
32
|
-
normalizeLineEndings,
|
|
33
|
-
humanSize,
|
|
34
|
-
formatElapsedMs,
|
|
35
|
-
formatCharCount,
|
|
36
|
-
ELAPSED_KEY,
|
|
37
33
|
CHARS_KEY,
|
|
38
34
|
compactErrorLines,
|
|
35
|
+
ELAPSED_KEY,
|
|
36
|
+
formatCharCount,
|
|
37
|
+
formatElapsedMs,
|
|
38
|
+
humanSize,
|
|
39
|
+
normalizeLineEndings,
|
|
39
40
|
} from "./helpers.js";
|
|
40
|
-
import type {
|
|
41
|
-
import type { ThemeLike, RenderCtxLike as RenderContext } from "./types.js";
|
|
41
|
+
import type { RenderCtxLike as RenderContext, ThemeLike } from "./types.js";
|
|
42
42
|
|
|
43
43
|
// ---------------------------------------------------------------------------
|
|
44
44
|
// Lazy imports — avoid top-level require() that blocks module loading
|
|
@@ -72,7 +72,7 @@ function resolveTheme(): BundledTheme {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
const THEME: BundledTheme = resolveTheme();
|
|
76
76
|
const _cache = new Map<string, string[]>();
|
|
77
77
|
|
|
78
78
|
function _touch(k: string, v: string[]): string[] {
|
|
@@ -168,9 +168,14 @@ export function fillToolBackground(text: string, bg = BG_BASE, width?: number):
|
|
|
168
168
|
return text
|
|
169
169
|
.split("\n")
|
|
170
170
|
.map((line) => {
|
|
171
|
-
|
|
171
|
+
if (!width) {
|
|
172
|
+
const stripped = preserveBoxBackground(line);
|
|
173
|
+
return bg ? bg + stripped : stripped;
|
|
174
|
+
}
|
|
175
|
+
const plainLead = line.replace(/\x1b\[[0-9;]*m/g, "");
|
|
176
|
+
const skipPad = line.startsWith(TOOL_RESULT_INDENT) || plainLead.startsWith(TOOL_RESULT_INDENT);
|
|
177
|
+
const fitted = _truncateToWidth(line, width, "", !skipPad);
|
|
172
178
|
const stripped = preserveBoxBackground(fitted);
|
|
173
|
-
// Apply background to the entire line
|
|
174
179
|
return bg ? bg + stripped : stripped;
|
|
175
180
|
})
|
|
176
181
|
.join("\n");
|
|
@@ -198,13 +203,18 @@ export function renderToolMetrics(result: AgentToolResult<Record<string, unknown
|
|
|
198
203
|
return `${FG_DIM}· ${[elapsed, chars].filter(Boolean).join(" · ")}${RST}`;
|
|
199
204
|
}
|
|
200
205
|
|
|
206
|
+
export function renderToolDuration(result: AgentToolResult<Record<string, unknown>>): string {
|
|
207
|
+
const details = result.details as Record<string, unknown> | undefined;
|
|
208
|
+
return formatElapsedMs(details?.[ELAPSED_KEY] as number | undefined);
|
|
209
|
+
}
|
|
210
|
+
|
|
201
211
|
// ---------------------------------------------------------------------------
|
|
202
212
|
// Error renderer
|
|
203
213
|
// ---------------------------------------------------------------------------
|
|
204
214
|
|
|
205
215
|
export function renderToolError(error: string, theme: ThemeLike): string {
|
|
206
216
|
const body = compactErrorLines(error)
|
|
207
|
-
.map((line) =>
|
|
217
|
+
.map((line) => `${TOOL_RESULT_INDENT}${line ? theme.fg("error", line) : ""}`)
|
|
208
218
|
.join("\n");
|
|
209
219
|
return fillToolBackground(body, BG_ERROR);
|
|
210
220
|
}
|
|
@@ -239,23 +249,16 @@ export async function renderFileContent(
|
|
|
239
249
|
// Bash — colored exit status
|
|
240
250
|
// ---------------------------------------------------------------------------
|
|
241
251
|
|
|
242
|
-
export function renderBashOutput(text: string
|
|
243
|
-
const isOk = exitCode === 0;
|
|
244
|
-
const statusIcon = isOk ? "✓" : "✗";
|
|
245
|
-
const codeStr =
|
|
246
|
-
exitCode !== null
|
|
247
|
-
? `${isOk ? FG_GREEN : FG_RED}${statusIcon} exit ${exitCode}${RST}`
|
|
248
|
-
: `${FG_YELLOW}⚡ killed${RST}`;
|
|
249
|
-
|
|
252
|
+
export function renderBashOutput(text: string): string {
|
|
250
253
|
const lines = text.split("\n");
|
|
251
254
|
const maxShow = MAX_PREVIEW_LINES;
|
|
252
255
|
const show = lines.slice(0, maxShow);
|
|
253
256
|
const remaining = lines.length - maxShow;
|
|
254
257
|
|
|
255
258
|
let body = show.join("\n");
|
|
256
|
-
if (remaining > 0) body += `\n${FG_DIM}
|
|
259
|
+
if (remaining > 0) body += `\n${TOOL_RESULT_INDENT}${FG_DIM}… ${remaining} more lines${RST}`;
|
|
257
260
|
|
|
258
|
-
return
|
|
261
|
+
return body;
|
|
259
262
|
}
|
|
260
263
|
|
|
261
264
|
// ---------------------------------------------------------------------------
|
|
@@ -318,10 +321,10 @@ export function renderFindResults(text: string, theme?: ThemeLike): string {
|
|
|
318
321
|
const more = theme
|
|
319
322
|
? theme.fg("dim", `… ${lines.length - count} more files`)
|
|
320
323
|
: `${FG_DIM}… ${lines.length - count} more files${RST}`;
|
|
321
|
-
out.push(
|
|
324
|
+
out.push(`${TOOL_RESULT_INDENT}${more}`);
|
|
322
325
|
return out.join("\n");
|
|
323
326
|
}
|
|
324
|
-
out.push(
|
|
327
|
+
out.push(`${TOOL_RESULT_INDENT}${files[i]}`);
|
|
325
328
|
count++;
|
|
326
329
|
}
|
|
327
330
|
}
|
|
@@ -364,10 +367,10 @@ export async function renderGrepResults(text: string, pattern: string): Promise<
|
|
|
364
367
|
const nw = Math.max(3, lineNo.length);
|
|
365
368
|
let display = content;
|
|
366
369
|
if (re) display = content.replace(re, `${RST}${FG_YELLOW}\x1b[1m$1${RST}`);
|
|
367
|
-
out.push(
|
|
370
|
+
out.push(`${TOOL_RESULT_INDENT}${lnum(Number(lineNo), nw)} ${FG_RULE}│${RST} ${display}${RST}`);
|
|
368
371
|
count++;
|
|
369
372
|
} else if (line.trim() === "--") {
|
|
370
|
-
out.push(
|
|
373
|
+
out.push(`${TOOL_RESULT_INDENT}${FG_DIM} ···${RST}`);
|
|
371
374
|
} else if (line.trim()) {
|
|
372
375
|
out.push(line);
|
|
373
376
|
count++;
|
|
@@ -407,9 +410,15 @@ export function makeRenderResult() {
|
|
|
407
410
|
const preview = lines.slice(0, maxShow).join("\n");
|
|
408
411
|
const more = lines.length > maxShow ? `\n${FG_DIM}... ${lines.length - maxShow} more lines${RST}` : "";
|
|
409
412
|
const metrics = renderToolMetrics(result);
|
|
410
|
-
text.setText(
|
|
413
|
+
text.setText(
|
|
414
|
+
fillToolBackground(
|
|
415
|
+
`${TOOL_RESULT_INDENT}${preview}${more}${metrics ? `\n${TOOL_RESULT_INDENT}${metrics}` : ""}`,
|
|
416
|
+
undefined,
|
|
417
|
+
renderWidth,
|
|
418
|
+
),
|
|
419
|
+
);
|
|
411
420
|
} else {
|
|
412
|
-
text.setText(fillToolBackground(
|
|
421
|
+
text.setText(fillToolBackground(`${TOOL_RESULT_INDENT}${theme.fg("dim", "(no text output)")}`));
|
|
413
422
|
}
|
|
414
423
|
return text;
|
|
415
424
|
};
|
package/src/tools/bash.ts
CHANGED
|
@@ -1,26 +1,12 @@
|
|
|
1
1
|
/* pi-pretty: bash tool -- command execution with styled output. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
type AgentToolResult,
|
|
8
|
-
} from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import type { SdkToolDef, BashDetails, TextContent, ComponentLike, ThemeLike, RenderCtxLike } from "../types.js";
|
|
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";
|
|
20
|
-
import { wrapExecuteWithMetrics } from "./metrics.js";
|
|
21
|
-
import { renderBashOutput, renderToolError, renderToolMetrics, fillToolBackground } from "../render.js";
|
|
3
|
+
import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { BG_BASE, FG_DIM, MAX_PREVIEW_LINES, resolveBaseBackground, TOOL_RESULT_INDENT, termWidth } from "../config.js";
|
|
5
|
+
import { compactErrorLines, inferBashExitCode, stripBashExitStatusLine } from "../helpers.js";
|
|
6
|
+
import { fillToolBackground, renderToolDuration, renderToolError } from "../render.js";
|
|
22
7
|
import { resolveTextCtor } from "../tui-text.js";
|
|
23
|
-
import {
|
|
8
|
+
import type { BashDetails, ComponentLike, RenderCtxLike, SdkToolDef, TextContent, ThemeLike } from "../types.js";
|
|
9
|
+
import { wrapExecuteWithMetrics } from "./metrics.js";
|
|
24
10
|
|
|
25
11
|
type Result = AgentToolResult<Record<string, unknown>>;
|
|
26
12
|
|
|
@@ -38,8 +24,8 @@ export function registerBashTool(
|
|
|
38
24
|
label: "Bash",
|
|
39
25
|
description: sdkTool.description
|
|
40
26
|
? `${sdkTool.description} For text search: \`rg -n\`.`
|
|
41
|
-
: "Execute shell commands. For text search:
|
|
42
|
-
promptSnippet: "Execute commands via bash. For text search:
|
|
27
|
+
: "Execute shell commands. For text search: `rg -n`.",
|
|
28
|
+
promptSnippet: "Execute commands via bash. For text search: `rg -n`.",
|
|
43
29
|
promptGuidelines: [
|
|
44
30
|
"For text search: `rg -n`. If no results, try `rg -u` (respects .gitignore by default).",
|
|
45
31
|
"In rg: | means alternation, \\| means literal pipe. Opposite of GNU grep. Never use \\| for alternation.",
|
|
@@ -71,19 +57,16 @@ export function registerBashTool(
|
|
|
71
57
|
const t = typeof args.timeout === "number" ? ` ${theme.fg("muted", `(timeout ${args.timeout}s)`)}` : "";
|
|
72
58
|
const tw = termWidth() || 80;
|
|
73
59
|
const rawCmd = String(args.command ?? "");
|
|
60
|
+
const headerBudget = ctx.expanded ? tw : Math.max(8, tw - 20);
|
|
74
61
|
const cmd =
|
|
75
62
|
rawCmd.length === 0
|
|
76
63
|
? theme.fg("toolOutput", "...")
|
|
77
|
-
: rawCmd.length >
|
|
78
|
-
? rawCmd.slice(0, Math.max(1,
|
|
64
|
+
: !ctx.expanded && rawCmd.length > headerBudget
|
|
65
|
+
? `${rawCmd.slice(0, Math.max(1, headerBudget))}…`
|
|
79
66
|
: rawCmd;
|
|
80
|
-
const
|
|
67
|
+
const commandLabel = theme.fg(ctx.isError ? "error" : "toolTitle", theme.bold(`$ ${cmd}`));
|
|
81
68
|
text.setText(
|
|
82
|
-
fillToolBackground(
|
|
83
|
-
`\n ${theme.fg("toolTitle", theme.bold(`$ ${cmd}`))}${t}`,
|
|
84
|
-
ctx.isError ? BG_ERROR : undefined,
|
|
85
|
-
toolWidth,
|
|
86
|
-
),
|
|
69
|
+
fillToolBackground(`\n${TOOL_RESULT_INDENT}${commandLabel}${t}`, undefined, ctx.expanded ? undefined : tw),
|
|
87
70
|
);
|
|
88
71
|
return text;
|
|
89
72
|
},
|
|
@@ -109,25 +92,22 @@ export function registerBashTool(
|
|
|
109
92
|
|
|
110
93
|
if (d?._type === "bashResult") {
|
|
111
94
|
const isErr = ctx.isError || (d.exitCode !== null && d.exitCode !== 0);
|
|
112
|
-
const bg = isErr ? BG_ERROR : undefined;
|
|
113
95
|
const cleaned = stripBashExitStatusLine(d.text);
|
|
114
96
|
const output = isErr ? compactErrorLines(cleaned).join("\n") : cleaned;
|
|
115
|
-
const { summary } = renderBashOutput(output, d.exitCode);
|
|
116
97
|
const lineCount = output.split("\n").length;
|
|
117
|
-
const info =
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const header =
|
|
98
|
+
const info = [`${lineCount} lines`, renderToolDuration(result), !ctx.expanded ? "ctrl+o to expand" : ""]
|
|
99
|
+
.filter(Boolean)
|
|
100
|
+
.map((part) => theme.fg("dim", part))
|
|
101
|
+
.join(theme.fg("dim", " · "));
|
|
102
|
+
const header = `${TOOL_RESULT_INDENT}${info}`;
|
|
122
103
|
const rw = termWidth();
|
|
123
104
|
|
|
124
105
|
const renderFn = (w: number) => {
|
|
125
|
-
if (!
|
|
126
|
-
|
|
127
|
-
const show = output.split("\n")
|
|
128
|
-
const out = [header,
|
|
129
|
-
|
|
130
|
-
return fillToolBackground(out.join("\n"), bg, w);
|
|
106
|
+
if (!ctx.expanded) return fillToolBackground(`${header}\n`, undefined, w);
|
|
107
|
+
if (!output.trim()) return fillToolBackground(`${header}\n`, undefined, w);
|
|
108
|
+
const show = output.split("\n");
|
|
109
|
+
const out = [header, "", ...show.map((line: string) => `${TOOL_RESULT_INDENT}${line}`)];
|
|
110
|
+
return fillToolBackground(`${out.join("\n")}\n`, undefined, w);
|
|
131
111
|
};
|
|
132
112
|
|
|
133
113
|
text.setText(renderFn(rw));
|
|
@@ -137,7 +117,7 @@ export function registerBashTool(
|
|
|
137
117
|
let key: string | undefined;
|
|
138
118
|
(text as unknown as Record<string, unknown>).render = (w: number) => {
|
|
139
119
|
const width = Math.max(1, Math.floor(w || termWidth()));
|
|
140
|
-
const k = `bash:${ctx.expanded ? "1" : "0"}:${width}:${d.exitCode ?? "killed"}:${output.length}:${
|
|
120
|
+
const k = `bash:${ctx.expanded ? "1" : "0"}:${width}:${d.exitCode ?? "killed"}:${output.length}:${renderToolDuration(result)}`;
|
|
141
121
|
if (key !== k) {
|
|
142
122
|
text.setText(renderFn(width));
|
|
143
123
|
key = k;
|
|
@@ -154,16 +134,15 @@ export function registerBashTool(
|
|
|
154
134
|
}
|
|
155
135
|
const fc = result.content?.[0];
|
|
156
136
|
text.setText(
|
|
157
|
-
fillToolBackground(
|
|
137
|
+
fillToolBackground(
|
|
138
|
+
`${TOOL_RESULT_INDENT}${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "done")}`,
|
|
139
|
+
),
|
|
158
140
|
);
|
|
159
141
|
return text;
|
|
160
142
|
},
|
|
161
143
|
} as unknown as ToolDefinition<any, any, any>);
|
|
162
144
|
}
|
|
163
145
|
|
|
164
|
-
function rule(w: number): string {
|
|
165
|
-
return `${FG_RULE}${"\u2500".repeat(w)}${RST}`;
|
|
166
|
-
}
|
|
167
146
|
function getText(result: Result): string {
|
|
168
147
|
return (
|
|
169
148
|
((result.content ?? []) as TextContent[])
|
package/src/tools/find.ts
CHANGED
|
@@ -1,35 +1,80 @@
|
|
|
1
|
-
/* pi-pretty: find tool -- FFF-backed file search with SDK fallback. */
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
type AgentToolResult,
|
|
8
|
-
} from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import { isAbsolute, relative as toRelative } from "node:path";
|
|
10
|
-
import type {
|
|
11
|
-
SdkToolDef,
|
|
12
|
-
FindDetails,
|
|
13
|
-
FffServiceLike,
|
|
14
|
-
FileItem,
|
|
15
|
-
TextContent,
|
|
16
|
-
ThemeLike,
|
|
17
|
-
RenderCtxLike,
|
|
18
|
-
ComponentLike,
|
|
19
|
-
} from "../types.js";
|
|
20
|
-
import { resolveBaseBackground, BG_ERROR, FG_DIM, RST } from "../config.js";
|
|
1
|
+
/* pi-pretty: find tool -- FFF-backed file search with SDK (fd) fallback. */
|
|
2
|
+
|
|
3
|
+
import { isAbsolute, relative } from "node:path";
|
|
4
|
+
import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { BG_ERROR, FG_DIM, RST, resolveBaseBackground, TOOL_RESULT_INDENT } from "../config.js";
|
|
6
|
+
import { isLikelyGlobPattern, normalizeFindGlobPattern } from "../find-glob.js";
|
|
21
7
|
import { shortPath } from "../helpers.js";
|
|
22
|
-
import { wrapExecuteWithMetrics } from "./metrics.js";
|
|
23
|
-
import { renderFindResults, renderToolError, renderToolMetrics, fillToolBackground } from "../render.js";
|
|
24
|
-
import { resolveTextCtor } from "../tui-text.js";
|
|
25
8
|
import { NOTICE_PARTIAL_FILE_INDEX } from "../notices.js";
|
|
9
|
+
import { fillToolBackground, renderFindResults, renderToolDuration, renderToolError } from "../render.js";
|
|
10
|
+
import { resolveTextCtor } from "../tui-text.js";
|
|
11
|
+
import type { FffServiceWithCursor, FindDetails, RenderCtxLike, SdkToolDef, TextContent, ThemeLike } from "../types.js";
|
|
12
|
+
import { wrapExecuteWithMetrics } from "./metrics.js";
|
|
26
13
|
|
|
27
14
|
type Result = AgentToolResult<Record<string, unknown>>;
|
|
28
15
|
|
|
16
|
+
function getText(result: Result): string {
|
|
17
|
+
return (result.content ?? [])
|
|
18
|
+
.filter((c) => c.type === "text")
|
|
19
|
+
.map((c) => (c as TextContent).text ?? "")
|
|
20
|
+
.join("\n");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildGlobPattern(pattern: string, path: string | undefined, basePath: string | null): string {
|
|
24
|
+
const raw = pattern.startsWith("/") ? pattern.slice(1) : pattern;
|
|
25
|
+
const normalized = normalizeFindGlobPattern(raw);
|
|
26
|
+
let cleanPath = path ?? "";
|
|
27
|
+
if (cleanPath && isAbsolute(cleanPath) && basePath) {
|
|
28
|
+
cleanPath = relative(basePath, cleanPath) || "";
|
|
29
|
+
}
|
|
30
|
+
cleanPath = cleanPath.replace(/\/$/, "");
|
|
31
|
+
if (cleanPath) {
|
|
32
|
+
if (normalized.startsWith("**/")) {
|
|
33
|
+
return `${cleanPath}/${normalized}`;
|
|
34
|
+
}
|
|
35
|
+
if (normalized.includes("/")) {
|
|
36
|
+
return `${cleanPath}/${normalized}`;
|
|
37
|
+
}
|
|
38
|
+
return `${cleanPath}/**/${normalized}`;
|
|
39
|
+
}
|
|
40
|
+
return normalized.startsWith("**/") || normalized.includes("/") ? normalized : `**/${normalized}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function appendFindNotices(result: Result, extra: string[]): Result {
|
|
44
|
+
if (extra.length === 0) return result;
|
|
45
|
+
const d = result.details;
|
|
46
|
+
if (!d || d._type !== "findResult") return result;
|
|
47
|
+
const prev = Array.isArray(d.notices) ? (d.notices as string[]) : [];
|
|
48
|
+
return { ...result, details: { ...d, notices: [...prev, ...extra] } };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function sdkFindAsFindResult(
|
|
52
|
+
sdkTool: SdkToolDef,
|
|
53
|
+
tid: string,
|
|
54
|
+
params: Record<string, unknown>,
|
|
55
|
+
sig: AbortSignal | undefined,
|
|
56
|
+
ctx: ExtensionContext,
|
|
57
|
+
pattern: string,
|
|
58
|
+
extraNotices: string[],
|
|
59
|
+
): Promise<Result> {
|
|
60
|
+
const result = (await sdkTool.execute(tid, params, sig, undefined, ctx)) as Result;
|
|
61
|
+
const tc = getText(result);
|
|
62
|
+
const prev = (result.details as FindDetails | undefined)?.notices ?? [];
|
|
63
|
+
const notices = [...(Array.isArray(prev) ? prev : []), ...extraNotices];
|
|
64
|
+
result.details = {
|
|
65
|
+
_type: "findResult",
|
|
66
|
+
text: tc,
|
|
67
|
+
pattern,
|
|
68
|
+
matchCount: tc ? tc.trim().split("\n").filter(Boolean).length : 0,
|
|
69
|
+
notices,
|
|
70
|
+
};
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
29
74
|
export function registerFindTool(
|
|
30
75
|
pi: ExtensionAPI,
|
|
31
76
|
cwd: string,
|
|
32
|
-
fffService:
|
|
77
|
+
fffService: FffServiceWithCursor | null | undefined,
|
|
33
78
|
sdkTool: SdkToolDef,
|
|
34
79
|
TextComp?: new (t?: string, x?: number, y?: number) => { setText(v: string): void },
|
|
35
80
|
): void {
|
|
@@ -44,32 +89,40 @@ export function registerFindTool(
|
|
|
44
89
|
renderShell: "self",
|
|
45
90
|
|
|
46
91
|
execute: wrapExecuteWithMetrics(async (tid, params, sig, _upd, ctx: ExtensionContext) => {
|
|
47
|
-
const pattern = String(
|
|
48
|
-
const path =
|
|
49
|
-
const limit =
|
|
50
|
-
const glob = (params as any).glob;
|
|
51
|
-
|
|
92
|
+
const pattern = String(params.pattern ?? "");
|
|
93
|
+
const path = params.path ? String(params.path) : undefined;
|
|
94
|
+
const limit = params.limit;
|
|
52
95
|
const fff = fffService?.isAvailable ? fffService.getFinder() : null;
|
|
96
|
+
|
|
53
97
|
if (fff) {
|
|
54
98
|
try {
|
|
55
99
|
const effectiveLimit = Math.max(1, typeof limit === "number" ? limit : 100);
|
|
56
100
|
const basePathResult = fff.getBasePath();
|
|
57
101
|
const basePath = basePathResult.ok ? basePathResult.value : null;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const cleanPattern = pattern.startsWith("/") ? pattern.slice(1) : pattern;
|
|
64
|
-
const globPattern = cleanPath ? `${cleanPath}/**/${cleanPattern}` : `**/${cleanPattern}`;
|
|
65
|
-
const searchResult = fff.glob(globPattern, { pageSize: effectiveLimit });
|
|
102
|
+
const globPattern = buildGlobPattern(pattern, path, basePath);
|
|
103
|
+
const searchResult = fff.glob(globPattern, {
|
|
104
|
+
pageSize: effectiveLimit,
|
|
105
|
+
});
|
|
106
|
+
|
|
66
107
|
if (searchResult.ok) {
|
|
67
|
-
const items
|
|
108
|
+
const items = searchResult.value.items.slice(0, effectiveLimit);
|
|
68
109
|
const notices: string[] = [];
|
|
69
110
|
if (fffService?.partialIndex) notices.push(NOTICE_PARTIAL_FILE_INDEX);
|
|
70
111
|
if (items.length >= effectiveLimit) notices.push(`${effectiveLimit} limit reached`);
|
|
71
|
-
if (searchResult.value.totalMatched > items.length)
|
|
112
|
+
if (searchResult.value.totalMatched > items.length) {
|
|
72
113
|
notices.push(`${searchResult.value.totalMatched} total matches`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (items.length === 0 && isLikelyGlobPattern(pattern)) {
|
|
117
|
+
return sdkFindAsFindResult(sdkTool, tid, params, sig, ctx, pattern, [
|
|
118
|
+
"FFF glob returned no matches; results from SDK find (fd).",
|
|
119
|
+
...notices,
|
|
120
|
+
]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (items.length > 0) notices.push("Search engine: FFF glob.");
|
|
124
|
+
else if (notices.length === 0) notices.push("Search engine: FFF glob (no matches).");
|
|
125
|
+
|
|
73
126
|
const paths = items.map((i) => i.relativePath).join("\n");
|
|
74
127
|
return {
|
|
75
128
|
content: [{ type: "text" as const, text: paths }],
|
|
@@ -79,7 +132,7 @@ export function registerFindTool(
|
|
|
79
132
|
pattern,
|
|
80
133
|
matchCount: items.length,
|
|
81
134
|
notices,
|
|
82
|
-
}
|
|
135
|
+
},
|
|
83
136
|
};
|
|
84
137
|
}
|
|
85
138
|
} catch {
|
|
@@ -87,82 +140,74 @@ export function registerFindTool(
|
|
|
87
140
|
}
|
|
88
141
|
}
|
|
89
142
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
_type: "findResult",
|
|
94
|
-
text: tc,
|
|
95
|
-
pattern,
|
|
96
|
-
matchCount: tc ? tc.trim().split("\n").filter(Boolean).length : 0,
|
|
97
|
-
} as FindDetails;
|
|
98
|
-
return result;
|
|
143
|
+
return sdkFindAsFindResult(sdkTool, tid, params, sig, ctx, pattern, [
|
|
144
|
+
fff ? "FFF find unavailable; results from SDK find (fd)." : "Search engine: SDK find (fd).",
|
|
145
|
+
]);
|
|
99
146
|
}),
|
|
100
147
|
|
|
101
148
|
renderCall(args: any, theme: ThemeLike, ctx: RenderCtxLike) {
|
|
102
149
|
resolveBaseBackground(theme);
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
const limit =
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
const patternPart = pattern ? theme.fg("accent", pattern) : "";
|
|
150
|
+
const a = args as { pattern?: unknown; path?: unknown; limit?: unknown };
|
|
151
|
+
const text = (ctx as RenderCtxLike).lastComponent ?? new TC("", 0, 0);
|
|
152
|
+
const pattern = a.pattern == null ? "" : String(a.pattern);
|
|
153
|
+
const pathArg = a.path == null ? "<missing>" : shortPath(cwd, home, String(a.path));
|
|
154
|
+
const limit = a.limit;
|
|
155
|
+
const findLabel = theme.fg(ctx.isError ? "error" : "toolTitle", theme.bold("✱ find"));
|
|
156
|
+
const patternPart = pattern ? theme.fg("toolTitle", pattern) : "";
|
|
111
157
|
const inPart = theme.fg("dim", " in ");
|
|
112
|
-
const pathPart = theme.fg("toolOutput",
|
|
158
|
+
const pathPart = theme.fg("toolOutput", pathArg);
|
|
113
159
|
const limitPart = limit !== undefined && limit !== null ? theme.fg("dim", ` limit ${limit}`) : "";
|
|
114
160
|
const out = `${findLabel} ${patternPart}${inPart}${pathPart}${limitPart}`;
|
|
115
|
-
text.setText(fillToolBackground(
|
|
161
|
+
text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${out}`, ctx.isError ? BG_ERROR : undefined));
|
|
116
162
|
return text;
|
|
117
163
|
},
|
|
118
164
|
|
|
119
165
|
renderResult(result: Result, _opt: unknown, theme: ThemeLike, ctx: RenderCtxLike) {
|
|
120
166
|
resolveBaseBackground(theme);
|
|
121
|
-
const
|
|
167
|
+
const r = result;
|
|
168
|
+
const text = (ctx as RenderCtxLike).lastComponent ?? new TC("", 0, 0);
|
|
122
169
|
if (ctx.isError) {
|
|
123
|
-
text.setText(renderToolError(getText(
|
|
170
|
+
text.setText(renderToolError(getText(r) || "Error", theme));
|
|
124
171
|
return text;
|
|
125
172
|
}
|
|
126
|
-
const d =
|
|
173
|
+
const d = r.details as FindDetails | undefined;
|
|
127
174
|
if (d?._type === "findResult") {
|
|
128
175
|
if (!d.text.trim()) {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
176
|
+
const noticeStr = d.notices?.length
|
|
177
|
+
? `\n${TOOL_RESULT_INDENT}${theme.fg("warning", `[${d.notices.join(". ")}]`)}`
|
|
178
|
+
: "";
|
|
179
|
+
text.setText(fillToolBackground(`\n${TOOL_RESULT_INDENT}${theme.fg("dim", "0 files")}${noticeStr}\n`));
|
|
180
|
+
return text;
|
|
181
|
+
}
|
|
182
|
+
if (!ctx.expanded) {
|
|
183
|
+
const duration = renderToolDuration(r);
|
|
184
|
+
text.setText(
|
|
185
|
+
fillToolBackground(
|
|
186
|
+
`${TOOL_RESULT_INDENT}${FG_DIM}${d.matchCount} files — ctrl+o to expand${RST}${duration ? `${FG_DIM}· ${duration}${RST}` : ""}\n`,
|
|
187
|
+
),
|
|
188
|
+
);
|
|
132
189
|
return text;
|
|
133
190
|
}
|
|
134
191
|
const rendered = renderFindResults(d.text, theme)
|
|
135
192
|
.split("\n")
|
|
136
|
-
.map((l) =>
|
|
193
|
+
.map((l) => `${TOOL_RESULT_INDENT}${l}`)
|
|
137
194
|
.join("\n");
|
|
138
|
-
const
|
|
139
|
-
|
|
195
|
+
const noticeStr = d.notices?.length
|
|
196
|
+
? `\n${TOOL_RESULT_INDENT}${theme.fg("warning", `[${d.notices.join(". ")}]`)}`
|
|
197
|
+
: "";
|
|
198
|
+
const duration = renderToolDuration(r);
|
|
140
199
|
text.setText(
|
|
141
200
|
fillToolBackground(
|
|
142
|
-
|
|
201
|
+
`\n${TOOL_RESULT_INDENT}${theme.fg("dim", `${d.matchCount} files`)}${duration ? `${FG_DIM}· ${duration}${RST}` : ""}\n${rendered}${noticeStr}\n`,
|
|
143
202
|
),
|
|
144
203
|
);
|
|
145
204
|
return text;
|
|
146
205
|
}
|
|
147
|
-
const fc =
|
|
206
|
+
const fc = r.content?.[0] as TextContent | undefined;
|
|
148
207
|
text.setText(
|
|
149
|
-
fillToolBackground(
|
|
150
|
-
` \n ${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "0 files")}\n `,
|
|
151
|
-
),
|
|
208
|
+
fillToolBackground(`\n${TOOL_RESULT_INDENT}${theme.fg("dim", fc?.text?.slice(0, 120) ?? "0 files")}\n`),
|
|
152
209
|
);
|
|
153
210
|
return text;
|
|
154
211
|
},
|
|
155
|
-
} as unknown as ToolDefinition
|
|
156
|
-
}
|
|
157
|
-
|
|
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
|
-
);
|
|
212
|
+
} as unknown as ToolDefinition);
|
|
168
213
|
}
|