@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
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
|
|
3
|
-
export type ConstraintParseResult = { ok: true; globs: string[]; tokens: string[] } | { ok: false; error: string };
|
|
4
|
-
|
|
5
|
-
export type MultiGrepRipgrepFallbackParams = {
|
|
6
|
-
cwd: string;
|
|
7
|
-
patterns: string[];
|
|
8
|
-
path?: string;
|
|
9
|
-
constraints?: string;
|
|
10
|
-
context?: number;
|
|
11
|
-
limit: number;
|
|
12
|
-
ignoreCase: boolean;
|
|
13
|
-
signal?: AbortSignal;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export type MultiGrepRipgrepFallbackResult = {
|
|
17
|
-
text: string;
|
|
18
|
-
matchCount: number;
|
|
19
|
-
limitReached: boolean;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export type MultiGrepRipgrepFallback = (
|
|
23
|
-
params: MultiGrepRipgrepFallbackParams,
|
|
24
|
-
) => Promise<MultiGrepRipgrepFallbackResult>;
|
|
25
|
-
|
|
26
|
-
const GLOB_META_RE = /[*?[{]/;
|
|
27
|
-
|
|
28
|
-
function trimSlashes(value: string): string {
|
|
29
|
-
return value.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function normalizeConstraintPath(value: string): string {
|
|
33
|
-
let normalized = value.replace(/\\/g, "/").trim();
|
|
34
|
-
while (normalized.startsWith("./")) normalized = normalized.slice(2);
|
|
35
|
-
return normalized;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function tokenizeConstraints(constraints: string): ConstraintParseResult {
|
|
39
|
-
const tokens: string[] = [];
|
|
40
|
-
let current = "";
|
|
41
|
-
let quote: '"' | "'" | null = null;
|
|
42
|
-
|
|
43
|
-
for (let i = 0; i < constraints.length; i++) {
|
|
44
|
-
const char = constraints[i];
|
|
45
|
-
if (quote) {
|
|
46
|
-
if (char === quote) quote = null;
|
|
47
|
-
else current += char;
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (char === '"' || char === "'") {
|
|
52
|
-
quote = char;
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (/\s/.test(char)) {
|
|
57
|
-
if (current) {
|
|
58
|
-
tokens.push(current);
|
|
59
|
-
current = "";
|
|
60
|
-
}
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
current += char;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (quote) return { ok: false, error: "unterminated quoted constraint" };
|
|
68
|
-
if (current) tokens.push(current);
|
|
69
|
-
|
|
70
|
-
return { ok: true, globs: [], tokens };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function tokenToRipgrepGlob(token: string): { ok: true; glob: string } | { ok: false; error: string } {
|
|
74
|
-
let negated = false;
|
|
75
|
-
let body = token;
|
|
76
|
-
|
|
77
|
-
if (body.startsWith("!")) {
|
|
78
|
-
negated = true;
|
|
79
|
-
body = body.slice(1);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
body = normalizeConstraintPath(body);
|
|
83
|
-
if (!body) return { ok: false, error: `empty constraint token: ${token}` };
|
|
84
|
-
if (body.includes("\0")) return { ok: false, error: `invalid NUL byte in constraint token: ${token}` };
|
|
85
|
-
|
|
86
|
-
let glob: string;
|
|
87
|
-
if (body.endsWith("/")) {
|
|
88
|
-
const dir = trimSlashes(body);
|
|
89
|
-
if (!dir) return { ok: false, error: `empty directory constraint: ${token}` };
|
|
90
|
-
glob = `**/${dir}/**`;
|
|
91
|
-
} else if (GLOB_META_RE.test(body) || body.includes("/")) {
|
|
92
|
-
glob = body.replace(/^\/+/, "");
|
|
93
|
-
} else if (body.includes(".")) {
|
|
94
|
-
glob = `**/${body}`;
|
|
95
|
-
} else {
|
|
96
|
-
glob = `**/${body}/**`;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return { ok: true, glob: negated ? `!${glob}` : glob };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function parseMultiGrepConstraints(constraints: string | undefined): ConstraintParseResult {
|
|
103
|
-
const trimmed = constraints?.trim();
|
|
104
|
-
if (!trimmed) return { ok: true, globs: [], tokens: [] };
|
|
105
|
-
|
|
106
|
-
const tokenized = tokenizeConstraints(trimmed);
|
|
107
|
-
if (!tokenized.ok) return tokenized;
|
|
108
|
-
|
|
109
|
-
const globs: string[] = [];
|
|
110
|
-
for (const token of tokenized.tokens) {
|
|
111
|
-
const parsed = tokenToRipgrepGlob(token);
|
|
112
|
-
if (!parsed.ok) return parsed;
|
|
113
|
-
globs.push(parsed.glob);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return { ok: true, globs, tokens: tokenized.tokens };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function isRipgrepMatchLine(line: string): boolean {
|
|
120
|
-
return /^.+?:\d+:/.test(line);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function buildRipgrepArgs(params: MultiGrepRipgrepFallbackParams, globs: string[]): string[] {
|
|
124
|
-
const args = ["--line-number", "--with-filename", "--color=never", "--hidden", "--fixed-strings"];
|
|
125
|
-
|
|
126
|
-
if (params.ignoreCase) args.push("--ignore-case");
|
|
127
|
-
if (params.context && params.context > 0) args.push("--context", String(params.context));
|
|
128
|
-
|
|
129
|
-
for (const glob of globs) args.push("--glob", glob);
|
|
130
|
-
for (const pattern of params.patterns) args.push("-e", pattern);
|
|
131
|
-
|
|
132
|
-
const searchPath = params.path?.trim();
|
|
133
|
-
if (searchPath) args.push("--", searchPath);
|
|
134
|
-
|
|
135
|
-
return args;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function getMultiGrepRipgrepArgs(
|
|
139
|
-
params: MultiGrepRipgrepFallbackParams,
|
|
140
|
-
): ConstraintParseResult & { args?: string[] } {
|
|
141
|
-
const parsed = parseMultiGrepConstraints(params.constraints);
|
|
142
|
-
if (!parsed.ok) return parsed;
|
|
143
|
-
return { ...parsed, args: buildRipgrepArgs(params, parsed.globs) };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export async function runMultiGrepRipgrepFallback(
|
|
147
|
-
params: MultiGrepRipgrepFallbackParams,
|
|
148
|
-
): Promise<MultiGrepRipgrepFallbackResult> {
|
|
149
|
-
const parsed = parseMultiGrepConstraints(params.constraints);
|
|
150
|
-
if (!parsed.ok) throw new Error(`unsupported constraints: ${parsed.error}`);
|
|
151
|
-
|
|
152
|
-
const args = buildRipgrepArgs(params, parsed.globs);
|
|
153
|
-
|
|
154
|
-
return new Promise((resolve, reject) => {
|
|
155
|
-
if (params.signal?.aborted) {
|
|
156
|
-
reject(new Error("Operation aborted"));
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const child = spawn("rg", args, { cwd: params.cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
161
|
-
const outputLines: string[] = [];
|
|
162
|
-
let stderr = "";
|
|
163
|
-
let buffer = "";
|
|
164
|
-
let matchCount = 0;
|
|
165
|
-
let limitReached = false;
|
|
166
|
-
let killedForLimit = false;
|
|
167
|
-
let settled = false;
|
|
168
|
-
|
|
169
|
-
const settle = (fn: () => void): void => {
|
|
170
|
-
if (settled) return;
|
|
171
|
-
settled = true;
|
|
172
|
-
fn();
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
const stopChild = (dueToLimit = false): void => {
|
|
176
|
-
if (!child.killed) {
|
|
177
|
-
killedForLimit = dueToLimit;
|
|
178
|
-
child.kill();
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const onAbort = (): void => stopChild(false);
|
|
183
|
-
params.signal?.addEventListener("abort", onAbort, { once: true });
|
|
184
|
-
|
|
185
|
-
const cleanup = (): void => {
|
|
186
|
-
params.signal?.removeEventListener("abort", onAbort);
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
const handleLine = (line: string): void => {
|
|
190
|
-
if (limitReached) return;
|
|
191
|
-
outputLines.push(line);
|
|
192
|
-
if (isRipgrepMatchLine(line)) {
|
|
193
|
-
matchCount++;
|
|
194
|
-
if (matchCount >= params.limit) {
|
|
195
|
-
limitReached = true;
|
|
196
|
-
stopChild(true);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
child.stdout?.on("data", (chunk: Buffer) => {
|
|
202
|
-
buffer += chunk.toString("utf8");
|
|
203
|
-
let newlineIndex = buffer.indexOf("\n");
|
|
204
|
-
while (newlineIndex >= 0) {
|
|
205
|
-
const line = buffer.slice(0, newlineIndex).replace(/\r$/, "");
|
|
206
|
-
buffer = buffer.slice(newlineIndex + 1);
|
|
207
|
-
handleLine(line);
|
|
208
|
-
newlineIndex = buffer.indexOf("\n");
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
child.stderr?.on("data", (chunk: Buffer) => {
|
|
213
|
-
stderr += chunk.toString("utf8");
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
child.on("error", (error: NodeJS.ErrnoException) => {
|
|
217
|
-
cleanup();
|
|
218
|
-
const message =
|
|
219
|
-
error.code === "ENOENT" ? "ripgrep (rg) is not available" : `Failed to run ripgrep: ${error.message}`;
|
|
220
|
-
settle(() => reject(new Error(message)));
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
child.on("close", (code) => {
|
|
224
|
-
cleanup();
|
|
225
|
-
|
|
226
|
-
if (params.signal?.aborted) {
|
|
227
|
-
settle(() => reject(new Error("Operation aborted")));
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (buffer && !limitReached) handleLine(buffer.replace(/\r$/, ""));
|
|
232
|
-
|
|
233
|
-
if (!killedForLimit && code !== 0 && code !== 1) {
|
|
234
|
-
const message = stderr.trim() || `ripgrep exited with code ${code}`;
|
|
235
|
-
settle(() => reject(new Error(message)));
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
settle(() =>
|
|
240
|
-
resolve({
|
|
241
|
-
text: outputLines.length ? outputLines.join("\n") : "No matches found",
|
|
242
|
-
matchCount,
|
|
243
|
-
limitReached,
|
|
244
|
-
}),
|
|
245
|
-
);
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
}
|
package/src/tools/multi-grep.ts
DELETED
|
@@ -1,331 +0,0 @@
|
|
|
1
|
-
/* pi-pretty: multi_grep tool -- FFF-backed multi-pattern search with ripgrep/SDK fallback. */
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
type ToolDefinition,
|
|
5
|
-
type ExtensionAPI,
|
|
6
|
-
type ExtensionContext,
|
|
7
|
-
type AgentToolResult,
|
|
8
|
-
} from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import { Type } from "typebox";
|
|
10
|
-
import type {
|
|
11
|
-
SdkToolDef,
|
|
12
|
-
GrepDetails,
|
|
13
|
-
FffServiceWithCursor,
|
|
14
|
-
TextContent,
|
|
15
|
-
ComponentLike,
|
|
16
|
-
ThemeLike,
|
|
17
|
-
RenderCtxLike,
|
|
18
|
-
} from "../types.js";
|
|
19
|
-
import { resolveBaseBackground, MAX_PREVIEW_LINES, BG_ERROR, FG_DIM, FG_LNUM, FG_RULE, RST } from "../config.js";
|
|
20
|
-
import { shortPath, normalizeLineEndings } from "../helpers.js";
|
|
21
|
-
import { wrapExecuteWithMetrics } from "./metrics.js";
|
|
22
|
-
import { renderToolError, renderToolMetrics, fillToolBackground } from "../render.js";
|
|
23
|
-
import { resolveTextCtor } from "../tui-text.js";
|
|
24
|
-
import { fffFormatGrepText } from "../fff-helpers.js";
|
|
25
|
-
import { parseMultiGrepConstraints } from "../multi-grep-fallback.js";
|
|
26
|
-
import { NOTICE_PARTIAL_FILE_INDEX } from "../notices.js";
|
|
27
|
-
import type { MultiGrepFallback } from "../types.js";
|
|
28
|
-
|
|
29
|
-
type Result = AgentToolResult<Record<string, unknown>>;
|
|
30
|
-
|
|
31
|
-
const noopFallback: MultiGrepFallback = async () => ({
|
|
32
|
-
text: "",
|
|
33
|
-
matchCount: 0,
|
|
34
|
-
limitReached: false,
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
export function registerMultiGrepTool(
|
|
38
|
-
pi: ExtensionAPI,
|
|
39
|
-
cwd: string,
|
|
40
|
-
fffService: FffServiceWithCursor | null | undefined,
|
|
41
|
-
sdkGrepTool?: SdkToolDef,
|
|
42
|
-
ripgrepFallback: MultiGrepFallback = noopFallback,
|
|
43
|
-
TextComp?: new (t?: string, x?: number, y?: number) => { setText(v: string): void },
|
|
44
|
-
): void {
|
|
45
|
-
const TC = resolveTextCtor(TextComp);
|
|
46
|
-
const home = process.env.HOME ?? "";
|
|
47
|
-
|
|
48
|
-
pi.registerTool({
|
|
49
|
-
name: "multi_grep",
|
|
50
|
-
label: "Multi Grep",
|
|
51
|
-
description: "Search file contents using multiple patterns (OR logic)",
|
|
52
|
-
parameters: Type.Object({
|
|
53
|
-
patterns: Type.Array(Type.String()),
|
|
54
|
-
path: Type.Optional(Type.String()),
|
|
55
|
-
constraints: Type.Optional(Type.String()),
|
|
56
|
-
context: Type.Optional(Type.Number()),
|
|
57
|
-
limit: Type.Optional(Type.Number()),
|
|
58
|
-
}),
|
|
59
|
-
renderShell: "self",
|
|
60
|
-
|
|
61
|
-
execute: wrapExecuteWithMetrics(async (tid, params, sig, _upd, ctx: ExtensionContext) => {
|
|
62
|
-
const p = params as any;
|
|
63
|
-
const patterns = Array.isArray(p.patterns) ? p.patterns.map(String) : [String(p.patterns ?? "")];
|
|
64
|
-
|
|
65
|
-
// Guard: empty patterns
|
|
66
|
-
if (!patterns.length || (patterns.length === 1 && !patterns[0])) {
|
|
67
|
-
return {
|
|
68
|
-
content: [{ text: "patterns array must have at least 1 element", type: "text" as const }],
|
|
69
|
-
details: { _type: "grepResult" } as GrepDetails,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Guard: aborted signal
|
|
74
|
-
if (sig?.aborted) {
|
|
75
|
-
return {
|
|
76
|
-
content: [{ text: "Aborted", type: "text" as const }],
|
|
77
|
-
details: { _type: "grepResult" } as GrepDetails,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
const constraintsStr = p.constraints ? String(p.constraints) : undefined;
|
|
81
|
-
const context = typeof p.context === "number" ? p.context : undefined;
|
|
82
|
-
const effectiveLimit = typeof p.limit === "number" ? p.limit : 200;
|
|
83
|
-
const alternationPattern = patterns.length === 1 ? patterns[0] : patterns.join("|");
|
|
84
|
-
|
|
85
|
-
const hasNativeConstraints = Boolean(constraintsStr);
|
|
86
|
-
const parsedConstraints = constraintsStr ? parseMultiGrepConstraints(constraintsStr) : null;
|
|
87
|
-
const requestedConstraints = parsedConstraints?.ok ? constraintsStr : undefined;
|
|
88
|
-
let effectivePath = p.path ? String(p.path) : undefined;
|
|
89
|
-
const requestedPath = parsedConstraints?.ok ? parsedConstraints.tokens[0] : undefined;
|
|
90
|
-
if (requestedPath && !effectivePath) effectivePath = requestedPath;
|
|
91
|
-
|
|
92
|
-
// 1. FFF multiGrep (no constraints AND no path)
|
|
93
|
-
if (fffService?.isAvailable && !hasNativeConstraints && !effectivePath) {
|
|
94
|
-
try {
|
|
95
|
-
const fff = fffService.getFinder();
|
|
96
|
-
if (!fff) throw new Error("FFF finder not available");
|
|
97
|
-
const grepResult = fff.multiGrep({
|
|
98
|
-
patterns,
|
|
99
|
-
pageSize: effectiveLimit,
|
|
100
|
-
smartCase: !shouldIgnoreCase(patterns),
|
|
101
|
-
beforeContext: context ?? 0,
|
|
102
|
-
afterContext: context ?? 0,
|
|
103
|
-
});
|
|
104
|
-
if (grepResult.ok) {
|
|
105
|
-
const grep = grepResult.value;
|
|
106
|
-
const items = grep.items.slice(0, effectiveLimit);
|
|
107
|
-
const cursorStore = fffService.getCursorStore();
|
|
108
|
-
const notices: string[] = [];
|
|
109
|
-
if (fffService.partialIndex) notices.push(NOTICE_PARTIAL_FILE_INDEX);
|
|
110
|
-
if (items.length >= effectiveLimit) notices.push(`${effectiveLimit} limit reached`);
|
|
111
|
-
if (grep.nextCursor) {
|
|
112
|
-
const cursorId = cursorStore.store(grep.nextCursor);
|
|
113
|
-
notices.push(`More results available: cursor="${cursorId}"`);
|
|
114
|
-
}
|
|
115
|
-
const text = appendNotices(fffFormatGrepText(items, effectiveLimit), notices);
|
|
116
|
-
return {
|
|
117
|
-
content: [{ type: "text" as const, text }],
|
|
118
|
-
details: {
|
|
119
|
-
_type: "grepResult",
|
|
120
|
-
text,
|
|
121
|
-
pattern: alternationPattern,
|
|
122
|
-
matchCount: items.length,
|
|
123
|
-
} as GrepDetails,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
// FFF failure -> return error directly
|
|
127
|
-
return {
|
|
128
|
-
content: [{ type: "text" as const, text: grepResult.error || "multi_grep failed" }],
|
|
129
|
-
details: {
|
|
130
|
-
_type: "grepResult",
|
|
131
|
-
text: "",
|
|
132
|
-
pattern: alternationPattern,
|
|
133
|
-
matchCount: 0,
|
|
134
|
-
} as GrepDetails,
|
|
135
|
-
};
|
|
136
|
-
} catch {
|
|
137
|
-
/* fall through */
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// 2. Ripgrep fallback
|
|
142
|
-
if (requestedConstraints || !sdkGrepTool) {
|
|
143
|
-
try {
|
|
144
|
-
const pathBacked = Boolean(
|
|
145
|
-
requestedConstraints &&
|
|
146
|
-
requestedPath &&
|
|
147
|
-
!Boolean(p.path) &&
|
|
148
|
-
!requestedConstraints.includes("*") &&
|
|
149
|
-
!requestedConstraints.includes("?"),
|
|
150
|
-
);
|
|
151
|
-
const constraintsForRg = pathBacked ? undefined : requestedConstraints;
|
|
152
|
-
const notices: string[] = [];
|
|
153
|
-
if (!fffService?.isAvailable) notices.push("FFF unavailable, used ripgrep fallback");
|
|
154
|
-
else if (hasNativeConstraints) notices.push("Used ripgrep fallback for constrained search");
|
|
155
|
-
else notices.push("Used ripgrep fallback");
|
|
156
|
-
|
|
157
|
-
const rgResult = await ripgrepFallback({
|
|
158
|
-
cwd,
|
|
159
|
-
patterns,
|
|
160
|
-
path: effectivePath,
|
|
161
|
-
constraints: constraintsForRg,
|
|
162
|
-
ignoreCase: shouldIgnoreCase(patterns),
|
|
163
|
-
context,
|
|
164
|
-
limit: effectiveLimit,
|
|
165
|
-
signal: sig,
|
|
166
|
-
});
|
|
167
|
-
const text = normalizeLineEndings(rgResult.text) || "No matches found";
|
|
168
|
-
if (rgResult.limitReached) notices.push(`${effectiveLimit} limit reached`);
|
|
169
|
-
return {
|
|
170
|
-
content: [{ type: "text" as const, text: appendNotices(text, notices) }],
|
|
171
|
-
details: {
|
|
172
|
-
_type: "grepResult",
|
|
173
|
-
text,
|
|
174
|
-
pattern: alternationPattern,
|
|
175
|
-
matchCount: rgResult.matchCount,
|
|
176
|
-
} as GrepDetails,
|
|
177
|
-
};
|
|
178
|
-
} catch (error: unknown) {
|
|
179
|
-
return {
|
|
180
|
-
content: [
|
|
181
|
-
{
|
|
182
|
-
type: "text" as const,
|
|
183
|
-
text: `multi_grep error: ${error instanceof Error ? error.message : String(error)}`,
|
|
184
|
-
},
|
|
185
|
-
],
|
|
186
|
-
details: {
|
|
187
|
-
_type: "grepResult",
|
|
188
|
-
text: "",
|
|
189
|
-
pattern: alternationPattern,
|
|
190
|
-
matchCount: 0,
|
|
191
|
-
} as GrepDetails,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// 3. SDK grep fallback
|
|
197
|
-
try {
|
|
198
|
-
const notices: string[] = [];
|
|
199
|
-
if (!fffService?.isAvailable) notices.push("FFF unavailable, used SDK grep fallback");
|
|
200
|
-
const result = (await sdkGrepTool.execute(
|
|
201
|
-
tid,
|
|
202
|
-
{
|
|
203
|
-
pattern: alternationPattern,
|
|
204
|
-
path: effectivePath,
|
|
205
|
-
ignoreCase: shouldIgnoreCase(patterns),
|
|
206
|
-
context,
|
|
207
|
-
limit: effectiveLimit,
|
|
208
|
-
},
|
|
209
|
-
sig,
|
|
210
|
-
null,
|
|
211
|
-
ctx,
|
|
212
|
-
)) as Result;
|
|
213
|
-
const tc = getText(result);
|
|
214
|
-
result.content = [{ type: "text" as const, text: appendNotices(tc, notices) }];
|
|
215
|
-
result.details = {
|
|
216
|
-
_type: "grepResult",
|
|
217
|
-
text: tc,
|
|
218
|
-
pattern: alternationPattern,
|
|
219
|
-
matchCount: tc ? tc.trim().split("\n").filter(Boolean).length : 0,
|
|
220
|
-
} as GrepDetails;
|
|
221
|
-
return result;
|
|
222
|
-
} catch (error: unknown) {
|
|
223
|
-
return {
|
|
224
|
-
content: [
|
|
225
|
-
{
|
|
226
|
-
type: "text" as const,
|
|
227
|
-
text: `multi_grep error: ${error instanceof Error ? error.message : String(error)}`,
|
|
228
|
-
},
|
|
229
|
-
],
|
|
230
|
-
details: {
|
|
231
|
-
_type: "grepResult",
|
|
232
|
-
text: "",
|
|
233
|
-
pattern: alternationPattern,
|
|
234
|
-
matchCount: 0,
|
|
235
|
-
} as GrepDetails,
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
}),
|
|
239
|
-
|
|
240
|
-
renderCall(args: any, theme: ThemeLike, ctx: RenderCtxLike) {
|
|
241
|
-
resolveBaseBackground(theme);
|
|
242
|
-
const text = ctx.lastComponent ?? new TC("", 0, 0);
|
|
243
|
-
const patterns: string[] = Array.isArray(args.patterns) ? args.patterns.map((p: unknown) => String(p)) : [];
|
|
244
|
-
const limit = typeof args.limit === "number" ? args.limit : undefined;
|
|
245
|
-
const path =
|
|
246
|
-
args.path === null || args.path === undefined ? "<missing>" : shortPath(cwd, home, String(args.path));
|
|
247
|
-
const literal = args.literal === true;
|
|
248
|
-
const patternStr =
|
|
249
|
-
patterns.length === 0
|
|
250
|
-
? ""
|
|
251
|
-
: patterns.length === 1
|
|
252
|
-
? patterns[0]!
|
|
253
|
-
: patterns.length === 2
|
|
254
|
-
? `${patterns[0]}|${patterns[1]}`
|
|
255
|
-
: `${patterns[0]}|${patterns[1]}|+${patterns.length - 2}`;
|
|
256
|
-
let out = `${theme.fg("toolTitle", theme.bold("mgrep"))} ${theme.fg("accent", `/${patternStr || ""}/`)}${theme.fg("toolOutput", ` in ${path}`)}`;
|
|
257
|
-
if (literal) out += theme.fg("dim", ` (literal)`);
|
|
258
|
-
if (limit !== undefined) out += theme.fg("dim", ` limit ${limit}`);
|
|
259
|
-
text.setText(fillToolBackground(`\n ${out}`, ctx.isError ? BG_ERROR : undefined));
|
|
260
|
-
return text;
|
|
261
|
-
},
|
|
262
|
-
|
|
263
|
-
renderResult(result: Result, _opt: unknown, theme: ThemeLike, ctx: RenderCtxLike) {
|
|
264
|
-
resolveBaseBackground(theme);
|
|
265
|
-
|
|
266
|
-
const text = ctx.lastComponent ?? new TC("", 0, 0);
|
|
267
|
-
if (ctx.isError) {
|
|
268
|
-
text.setText(renderToolError(getText(result) || "Error", theme));
|
|
269
|
-
return text;
|
|
270
|
-
}
|
|
271
|
-
const d = result.details as GrepDetails | undefined;
|
|
272
|
-
if (d?._type === "grepResult" && d.text) {
|
|
273
|
-
const lines = d.text.split("\n");
|
|
274
|
-
const maxShow = ctx.expanded ? lines.length : Math.min(lines.length, MAX_PREVIEW_LINES);
|
|
275
|
-
const show = lines.slice(0, maxShow);
|
|
276
|
-
const nw = Math.max(3, 5);
|
|
277
|
-
|
|
278
|
-
let hlRe: RegExp | null = null;
|
|
279
|
-
try {
|
|
280
|
-
hlRe = new RegExp(`(${d.pattern})`, "gi");
|
|
281
|
-
} catch {}
|
|
282
|
-
|
|
283
|
-
const out: string[] = [];
|
|
284
|
-
let currentFile = "";
|
|
285
|
-
for (const line of show) {
|
|
286
|
-
const fileMatch = line.match(/^(.+?)[:-](\d+)[:-](.*)$/);
|
|
287
|
-
if (fileMatch) {
|
|
288
|
-
const [, file, lineNo, content] = fileMatch;
|
|
289
|
-
if (file !== currentFile) {
|
|
290
|
-
if (currentFile) out.push("");
|
|
291
|
-
out.push(` ${theme.fg("accent", theme.bold(file))}`);
|
|
292
|
-
currentFile = file;
|
|
293
|
-
}
|
|
294
|
-
let display = content;
|
|
295
|
-
if (hlRe) display = content.replace(hlRe, (m) => `${RST}${theme.fg("warning", theme.bold(m))}${RST}`);
|
|
296
|
-
const padded = `${FG_LNUM}${String(lineNo).padStart(nw)}${RST} ${FG_RULE}│${RST} ${display}${RST}`;
|
|
297
|
-
out.push(` ${padded}`);
|
|
298
|
-
} else if (line.trim()) {
|
|
299
|
-
out.push(` ${FG_DIM} ${line.trim()}${RST}`);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
const preview = out.join("\n");
|
|
303
|
-
const more = lines.length > maxShow ? `\n${FG_DIM} ... ${lines.length - maxShow} more lines${RST}` : "";
|
|
304
|
-
text.setText(
|
|
305
|
-
fillToolBackground(` ${FG_DIM}${d.matchCount} matches${RST}${renderToolMetrics(result)}\n${preview}${more}`),
|
|
306
|
-
);
|
|
307
|
-
return text;
|
|
308
|
-
}
|
|
309
|
-
const fc = result.content?.[0];
|
|
310
|
-
text.setText(
|
|
311
|
-
fillToolBackground(` ${theme.fg("dim", fc && "text" in fc ? String(fc.text).slice(0, 120) : "no matches")}`),
|
|
312
|
-
);
|
|
313
|
-
return text;
|
|
314
|
-
},
|
|
315
|
-
} as unknown as ToolDefinition<any, any, any>);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function shouldIgnoreCase(patterns: string[]): boolean {
|
|
319
|
-
return !patterns.some((p) => /[A-Z]/.test(p));
|
|
320
|
-
}
|
|
321
|
-
function appendNotices(text: string, notices: string[]): string {
|
|
322
|
-
return notices.length ? `${text}\n\n[${notices.join(". ")}]` : text;
|
|
323
|
-
}
|
|
324
|
-
function getText(result: Result): string {
|
|
325
|
-
return (
|
|
326
|
-
((result.content ?? []) as TextContent[])
|
|
327
|
-
.filter((c) => c.type === "text")
|
|
328
|
-
.map((c) => c.text)
|
|
329
|
-
.join("\n") ?? ""
|
|
330
|
-
);
|
|
331
|
-
}
|