@xynogen/pix-pretty 1.3.3 → 1.4.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xynogen/pix-pretty",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "description": "Enhanced tool output rendering with syntax highlighting, file icons, tree views, FFF search, and paste chip formatting",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/fff.ts CHANGED
@@ -1,15 +1,8 @@
1
- import { spawn } from "node:child_process";
2
1
  import { join } from "node:path";
3
2
 
4
3
  import type { GrepCursor, GrepMatch } from "@ff-labs/fff-node";
5
4
 
6
- import type {
7
- ConstraintParseResult,
8
- FffBackedFinder,
9
- MultiGrepRipgrepFallbackParams,
10
- MultiGrepRipgrepFallbackResult,
11
- OptionalFffModule,
12
- } from "./types.js";
5
+ import type { FffBackedFinder, OptionalFffModule } from "./types.js";
13
6
 
14
7
  export interface FffState {
15
8
  module: OptionalFffModule | null;
@@ -159,257 +152,3 @@ export function fffFormatGrepText(items: GrepMatch[], limit: number): string {
159
152
 
160
153
  return lines.join("\n");
161
154
  }
162
-
163
- // ---------------------------------------------------------------------------
164
- // Multi-grep ripgrep fallback
165
- // ---------------------------------------------------------------------------
166
-
167
- const GLOB_META_RE = /[*?[{]/;
168
-
169
- function trimSlashes(value: string): string {
170
- return value.replace(/^\/+/, "").replace(/\/+$/, "");
171
- }
172
-
173
- function normalizeConstraintPath(value: string): string {
174
- let normalized = value.replace(/\\/g, "/").trim();
175
- while (normalized.startsWith("./")) normalized = normalized.slice(2);
176
- return normalized;
177
- }
178
-
179
- function tokenizeConstraints(constraints: string): ConstraintParseResult {
180
- const tokens: string[] = [];
181
- let current = "";
182
- let quote: '"' | "'" | null = null;
183
-
184
- for (let i = 0; i < constraints.length; i++) {
185
- const char = constraints[i];
186
- if (quote) {
187
- if (char === quote) quote = null;
188
- else current += char;
189
- continue;
190
- }
191
-
192
- if (char === '"' || char === "'") {
193
- quote = char;
194
- continue;
195
- }
196
-
197
- if (/\s/.test(char)) {
198
- if (current) {
199
- tokens.push(current);
200
- current = "";
201
- }
202
- continue;
203
- }
204
-
205
- current += char;
206
- }
207
-
208
- if (quote) return { ok: false, error: "unterminated quoted constraint" };
209
- if (current) tokens.push(current);
210
-
211
- return { ok: true, globs: [], tokens };
212
- }
213
-
214
- function tokenToRipgrepGlob(
215
- token: string,
216
- ): { ok: true; glob: string } | { ok: false; error: string } {
217
- let negated = false;
218
- let body = token;
219
-
220
- if (body.startsWith("!")) {
221
- negated = true;
222
- body = body.slice(1);
223
- }
224
-
225
- body = normalizeConstraintPath(body);
226
- if (!body) return { ok: false, error: `empty constraint token: ${token}` };
227
- if (body.includes("\0"))
228
- return {
229
- ok: false,
230
- error: `invalid NUL byte in constraint token: ${token}`,
231
- };
232
-
233
- let glob: string;
234
- if (body.endsWith("/")) {
235
- const dir = trimSlashes(body);
236
- if (!dir)
237
- return { ok: false, error: `empty directory constraint: ${token}` };
238
- glob = `**/${dir}/**`;
239
- } else if (GLOB_META_RE.test(body) || body.includes("/")) {
240
- glob = body.replace(/^\/+/, "");
241
- } else if (body.includes(".")) {
242
- glob = `**/${body}`;
243
- } else {
244
- glob = `**/${body}/**`;
245
- }
246
-
247
- return { ok: true, glob: negated ? `!${glob}` : glob };
248
- }
249
-
250
- export function parseMultiGrepConstraints(
251
- constraints: string | undefined,
252
- ): ConstraintParseResult {
253
- const trimmed = constraints?.trim();
254
- if (!trimmed) return { ok: true, globs: [], tokens: [] };
255
-
256
- const tokenized = tokenizeConstraints(trimmed);
257
- if (!tokenized.ok) return tokenized;
258
-
259
- const globs: string[] = [];
260
- for (const token of tokenized.tokens) {
261
- const parsed = tokenToRipgrepGlob(token);
262
- if (!parsed.ok) return parsed;
263
- globs.push(parsed.glob);
264
- }
265
-
266
- return { ok: true, globs, tokens: tokenized.tokens };
267
- }
268
-
269
- function isRipgrepMatchLine(line: string): boolean {
270
- return /^.+?:\d+:/.test(line);
271
- }
272
-
273
- function buildRipgrepArgs(
274
- params: MultiGrepRipgrepFallbackParams,
275
- globs: string[],
276
- ): string[] {
277
- const args = [
278
- "--line-number",
279
- "--with-filename",
280
- "--color=never",
281
- "--hidden",
282
- "--fixed-strings",
283
- ];
284
-
285
- if (params.ignoreCase) args.push("--ignore-case");
286
- if (params.context && params.context > 0)
287
- args.push("--context", String(params.context));
288
-
289
- for (const glob of globs) args.push("--glob", glob);
290
- for (const pattern of params.patterns) args.push("-e", pattern);
291
-
292
- const searchPath = params.path?.trim();
293
- if (searchPath) args.push("--", searchPath);
294
-
295
- return args;
296
- }
297
-
298
- export function getMultiGrepRipgrepArgs(
299
- params: MultiGrepRipgrepFallbackParams,
300
- ): ConstraintParseResult & { args?: string[] } {
301
- const parsed = parseMultiGrepConstraints(params.constraints);
302
- if (!parsed.ok) return parsed;
303
- return { ...parsed, args: buildRipgrepArgs(params, parsed.globs) };
304
- }
305
-
306
- export async function runMultiGrepRipgrepFallback(
307
- params: MultiGrepRipgrepFallbackParams,
308
- ): Promise<MultiGrepRipgrepFallbackResult> {
309
- const parsed = parseMultiGrepConstraints(params.constraints);
310
- if (!parsed.ok) throw new Error(`unsupported constraints: ${parsed.error}`);
311
-
312
- const args = buildRipgrepArgs(params, parsed.globs);
313
-
314
- return new Promise((resolve, reject) => {
315
- if (params.signal?.aborted) {
316
- reject(new Error("Operation aborted"));
317
- return;
318
- }
319
-
320
- const child = spawn("rg", args, {
321
- cwd: params.cwd,
322
- stdio: ["ignore", "pipe", "pipe"],
323
- });
324
- const outputLines: string[] = [];
325
- let stderr = "";
326
- let buffer = "";
327
- let matchCount = 0;
328
- let limitReached = false;
329
- let killedForLimit = false;
330
- let settled = false;
331
-
332
- const settle = (fn: () => void): void => {
333
- if (settled) return;
334
- settled = true;
335
- fn();
336
- };
337
-
338
- const stopChild = (dueToLimit = false): void => {
339
- if (!child.killed) {
340
- killedForLimit = dueToLimit;
341
- child.kill();
342
- }
343
- };
344
-
345
- const onAbort = (): void => stopChild(false);
346
- params.signal?.addEventListener("abort", onAbort, { once: true });
347
-
348
- const cleanup = (): void => {
349
- params.signal?.removeEventListener("abort", onAbort);
350
- };
351
-
352
- const handleLine = (line: string): void => {
353
- if (limitReached) return;
354
- outputLines.push(line);
355
- if (isRipgrepMatchLine(line)) {
356
- matchCount++;
357
- if (matchCount >= params.limit) {
358
- limitReached = true;
359
- stopChild(true);
360
- }
361
- }
362
- };
363
-
364
- child.stdout?.on("data", (chunk: Buffer) => {
365
- buffer += chunk.toString("utf8");
366
- let newlineIndex = buffer.indexOf("\n");
367
- while (newlineIndex >= 0) {
368
- const line = buffer.slice(0, newlineIndex).replace(/\r$/, "");
369
- buffer = buffer.slice(newlineIndex + 1);
370
- handleLine(line);
371
- newlineIndex = buffer.indexOf("\n");
372
- }
373
- });
374
-
375
- child.stderr?.on("data", (chunk: Buffer) => {
376
- stderr += chunk.toString("utf8");
377
- });
378
-
379
- child.on("error", (error: NodeJS.ErrnoException) => {
380
- cleanup();
381
- const message =
382
- error.code === "ENOENT"
383
- ? "ripgrep (rg) is not available"
384
- : `Failed to run ripgrep: ${error.message}`;
385
- settle(() => reject(new Error(message)));
386
- });
387
-
388
- child.on("close", (code) => {
389
- cleanup();
390
-
391
- if (params.signal?.aborted) {
392
- settle(() => reject(new Error("Operation aborted")));
393
- return;
394
- }
395
-
396
- if (buffer && !limitReached) handleLine(buffer.replace(/\r$/, ""));
397
-
398
- if (!killedForLimit && code !== 0 && code !== 1) {
399
- const message = stderr.trim() || `ripgrep exited with code ${code}`;
400
- settle(() => reject(new Error(message)));
401
- return;
402
- }
403
-
404
- settle(() =>
405
- resolve({
406
- text: outputLines.length
407
- ? outputLines.join("\n")
408
- : "No matches found",
409
- matchCount,
410
- limitReached,
411
- }),
412
- );
413
- });
414
- });
415
- }
package/src/index.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  * fff.ts Fast File Finder + cursor store + multi-grep fallback
17
17
  * diff.ts unified diff parser
18
18
  * diff-render.ts split/word-level diff renderer
19
- * tools/ per-tool registrars (read/bash/ls/find/grep/multi-grep/edit/write)
19
+ * tools/ per-tool registrars (read/bash/ls/find/grep/edit/write)
20
20
  * commands/ slash command registrars (fff)
21
21
  */
22
22
 
@@ -40,7 +40,6 @@ import {
40
40
  fffEnsureFinder,
41
41
  fffState,
42
42
  getPiPrettyFffDir,
43
- runMultiGrepRipgrepFallback,
44
43
  } from "./fff.js";
45
44
  import { clearHighlightCache } from "./highlight.js";
46
45
  import { registerBashTool } from "./tools/bash.js";
@@ -49,7 +48,6 @@ import { registerEditTool } from "./tools/edit.js";
49
48
  import { registerFindTool } from "./tools/find.js";
50
49
  import { registerGrepTool } from "./tools/grep.js";
51
50
  import { registerLsTool } from "./tools/ls.js";
52
- import { registerMultiGrepTool } from "./tools/multi-grep.js";
53
51
  import { registerReadTool } from "./tools/read.js";
54
52
  import { registerWriteTool } from "./tools/write.js";
55
53
  import type {
@@ -134,9 +132,6 @@ export default function piPrettyExtension(
134
132
  const cwd = process.cwd();
135
133
  const home = process.env.HOME ?? "";
136
134
  const sp = (p: string) => shortPath(cwd, home, p);
137
- const multiGrepRipgrepFallback =
138
- deps?.multiGrepRipgrepFallback ?? runMultiGrepRipgrepFallback;
139
-
140
135
  // Respect PRETTY_DISABLE_TOOLS env var
141
136
  const disabledTools = new Set(
142
137
  (process.env.PRETTY_DISABLE_TOOLS ?? "")
@@ -224,7 +219,6 @@ export default function piPrettyExtension(
224
219
  TextComponent: TextComponent!,
225
220
  fffState,
226
221
  cursorStore,
227
- multiGrepRipgrepFallback,
228
222
  };
229
223
 
230
224
  // ── Register tools ──────────────────────────────────────────────────
@@ -244,9 +238,6 @@ export default function piPrettyExtension(
244
238
  if (isToolEnabled("grep") && createGrepTool) {
245
239
  registerGrepTool(pi, createGrepTool, toolCtx);
246
240
  }
247
- if (isToolEnabled("multi_grep") && (fffState.module || createGrepTool)) {
248
- registerMultiGrepTool(pi, createGrepTool ?? null, toolCtx);
249
- }
250
241
  if (isToolEnabled("edit") && createEditTool) {
251
242
  registerEditTool(pi, createEditTool, toolCtx, trackInvalidator);
252
243
  }
@@ -46,11 +46,6 @@ describe("registerBashTool", () => {
46
46
  TextComponent: MockTextComponent as any,
47
47
  fffState: {} as any,
48
48
  cursorStore: {} as any,
49
- multiGrepRipgrepFallback: async () => ({
50
- text: "",
51
- matchCount: 0,
52
- limitReached: false,
53
- }),
54
49
  },
55
50
  );
56
51
 
@@ -1,5 +1,5 @@
1
1
  import type { CursorStore, FffState } from "../fff.js";
2
- import type { MultiGrepRipgrepFallback, TextComponentCtor } from "../types.js";
2
+ import type { TextComponentCtor } from "../types.js";
3
3
 
4
4
  // ── Shared context passed to each tool registrar ───────────────────────
5
5
 
@@ -14,6 +14,4 @@ export interface ToolContext {
14
14
  fffState: FffState;
15
15
  /** FFF cursor store */
16
16
  cursorStore: CursorStore;
17
- /** Ripgrep fallback for multi_grep */
18
- multiGrepRipgrepFallback: MultiGrepRipgrepFallback;
19
17
  }
package/src/types.ts CHANGED
@@ -20,35 +20,6 @@ export type BundledLanguage = string;
20
20
 
21
21
  export type BundledTheme = string;
22
22
 
23
- // ---------------------------------------------------------------------------
24
- // Multi-grep ripgrep fallback contracts
25
- // ---------------------------------------------------------------------------
26
-
27
- export type ConstraintParseResult =
28
- | { ok: true; globs: string[]; tokens: string[] }
29
- | { ok: false; error: string };
30
-
31
- export type MultiGrepRipgrepFallbackParams = {
32
- cwd: string;
33
- patterns: string[];
34
- path?: string;
35
- constraints?: string;
36
- context?: number;
37
- limit: number;
38
- ignoreCase: boolean;
39
- signal?: AbortSignal;
40
- };
41
-
42
- export type MultiGrepRipgrepFallbackResult = {
43
- text: string;
44
- matchCount: number;
45
- limitReached: boolean;
46
- };
47
-
48
- export type MultiGrepRipgrepFallback = (
49
- params: MultiGrepRipgrepFallbackParams,
50
- ) => Promise<MultiGrepRipgrepFallbackResult>;
51
-
52
23
  // ---------------------------------------------------------------------------
53
24
  // Config
54
25
  // ---------------------------------------------------------------------------
@@ -203,14 +174,6 @@ export type FindParams = FindToolInput;
203
174
 
204
175
  export type GrepParams = GrepToolInput;
205
176
 
206
- export type MultiGrepParams = {
207
- patterns: string[];
208
- path?: string;
209
- constraints?: string;
210
- context?: number;
211
- limit?: number;
212
- };
213
-
214
177
  export type EditRenderState = {
215
178
  _pk?: string;
216
179
  _pt?: string;
@@ -322,5 +285,4 @@ export interface PiPrettyDeps {
322
285
  sdk: PiPrettySdk;
323
286
  TextComponent: TextComponentCtor;
324
287
  fffModule?: OptionalFffModule;
325
- multiGrepRipgrepFallback?: MultiGrepRipgrepFallback;
326
288
  }
package/src/utils.ts CHANGED
@@ -258,34 +258,3 @@ export function trimToUndefined(value: string | undefined): string | undefined {
258
258
  const trimmed = value?.trim();
259
259
  return trimmed ? trimmed : undefined;
260
260
  }
261
-
262
- function escapeRegexLiteral(text: string): string {
263
- return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
264
- }
265
-
266
- export function buildLiteralAlternationPattern(patterns: string[]): string {
267
- return patterns
268
- .map(escapeRegexLiteral)
269
- .sort((a, b) => b.length - a.length)
270
- .join("|");
271
- }
272
-
273
- export function shouldIgnoreCaseForPatterns(patterns: string[]): boolean {
274
- return patterns.every((pattern) => pattern.toLowerCase() === pattern);
275
- }
276
-
277
- export function getConstraintBackedPath(
278
- constraints: string | undefined,
279
- ): string | undefined {
280
- const trimmed = trimToUndefined(constraints);
281
- if (
282
- !trimmed ||
283
- /\s/.test(trimmed) ||
284
- trimmed.includes("!") ||
285
- trimmed.endsWith("/") ||
286
- /[*?[{]/.test(trimmed)
287
- ) {
288
- return undefined;
289
- }
290
- return trimmed;
291
- }
@@ -1,311 +0,0 @@
1
- import type {
2
- ExtensionContext,
3
- GrepToolInput,
4
- ToolRenderResultOptions,
5
- } from "@earendil-works/pi-coding-agent";
6
-
7
- import { fffFormatGrepText } from "../fff.js";
8
- import type {
9
- GrepResultDetails,
10
- MultiGrepParams,
11
- PiPrettyApi,
12
- RenderContextLike,
13
- ThemeLike,
14
- ToolFactory,
15
- ToolResultLike,
16
- } from "../types.js";
17
- import {
18
- appendNotices,
19
- buildLiteralAlternationPattern,
20
- countRipgrepMatches,
21
- fillToolBackground,
22
- getConstraintBackedPath,
23
- getErrorMessage,
24
- getTextContent,
25
- makeTextResult,
26
- normalizeLineEndings,
27
- pluralize,
28
- renderDimPreview,
29
- renderToolError,
30
- shouldIgnoreCaseForPatterns,
31
- trimToUndefined,
32
- } from "../utils.js";
33
- import type { ToolContext } from "./context.js";
34
-
35
- export function registerMultiGrepTool(
36
- pi: PiPrettyApi,
37
- createGrepTool: ToolFactory<GrepToolInput> | null,
38
- ctx: ToolContext,
39
- ): void {
40
- const {
41
- cwd,
42
- sp,
43
- TextComponent,
44
- fffState,
45
- cursorStore,
46
- multiGrepRipgrepFallback,
47
- } = ctx;
48
- const multiGrepFallback = createGrepTool ? createGrepTool(cwd) : null;
49
-
50
- pi.registerTool({
51
- name: "multi_grep",
52
- label: "multi_grep",
53
- renderShell: "self",
54
- description: [
55
- "Search file contents for lines matching ANY of multiple patterns (OR logic).",
56
- "Uses SIMD-accelerated Aho-Corasick multi-pattern matching when FFF is available.",
57
- "Falls back to ripgrep while preserving literal OR semantics and file constraints when needed.",
58
- "Patterns are literal text — never escape special characters.",
59
- "Use path to scope a directory/file and constraints for file filtering ('*.rs', 'src/', '!test/').",
60
- ].join(" "),
61
- promptSnippet:
62
- "Multi-pattern OR search across file contents (FFF-accelerated with grep fallback)",
63
- promptGuidelines: [
64
- "Use multi_grep when you need to find multiple identifiers at once (OR logic).",
65
- "Include all naming conventions: snake_case, PascalCase, camelCase variants.",
66
- "Patterns are literal text. Never escape special characters.",
67
- "Use path to scope a directory or file when you need fresh on-disk results.",
68
- "Use the constraints parameter for additional file filtering, not inside patterns.",
69
- ],
70
-
71
- parameters: {
72
- type: "object",
73
- properties: {
74
- patterns: {
75
- type: "array",
76
- items: { type: "string" },
77
- description:
78
- "Patterns to search for (OR logic — matches lines containing ANY pattern).",
79
- },
80
- path: {
81
- type: "string",
82
- description:
83
- "Directory or file path to search (default: current directory)",
84
- },
85
- constraints: {
86
- type: "string",
87
- description:
88
- "File constraints, e.g. '*.{ts,tsx} !test/' to filter files.",
89
- },
90
- context: {
91
- type: "number",
92
- description:
93
- "Number of context lines before and after each match (default: 0)",
94
- },
95
- limit: {
96
- type: "number",
97
- description: "Maximum number of matches to return (default: 100)",
98
- },
99
- },
100
- required: ["patterns"],
101
- },
102
-
103
- async execute(
104
- tid: string,
105
- params: MultiGrepParams,
106
- sig: AbortSignal | undefined,
107
- upd: unknown,
108
- toolCtx: ExtensionContext,
109
- ) {
110
- if (sig?.aborted) return makeTextResult("Aborted", {});
111
-
112
- if (!params.patterns || params.patterns.length === 0) {
113
- return makeTextResult(
114
- "Error: patterns array must have at least 1 element",
115
- { error: "empty patterns" },
116
- );
117
- }
118
-
119
- const effectiveLimit = Math.max(1, params.limit ?? 100);
120
- const pattern = buildLiteralAlternationPattern(params.patterns);
121
- const requestedPath = trimToUndefined(params.path);
122
- const requestedConstraints = trimToUndefined(params.constraints);
123
- const effectivePath =
124
- requestedPath ?? getConstraintBackedPath(requestedConstraints);
125
- const hasNativeConstraints = Boolean(
126
- requestedPath || requestedConstraints,
127
- );
128
-
129
- // FFF path (no constraints — constrained searches use ripgrep)
130
- if (
131
- fffState.finder &&
132
- !fffState.finder.isDestroyed &&
133
- !hasNativeConstraints
134
- ) {
135
- try {
136
- const grepResult = fffState.finder.multiGrep({
137
- patterns: params.patterns,
138
- maxMatchesPerFile: Math.min(effectiveLimit, 50),
139
- smartCase: true,
140
- cursor: null,
141
- beforeContext: params.context ?? 0,
142
- afterContext: params.context ?? 0,
143
- });
144
-
145
- if (!grepResult.ok) {
146
- return makeTextResult(`multi_grep error: ${grepResult.error}`, {
147
- error: grepResult.error,
148
- });
149
- }
150
-
151
- const grep = grepResult.value;
152
- const notices: string[] = [];
153
- if (fffState.partialIndex)
154
- notices.push("Warning: partial file index");
155
- if (grep.items.length >= effectiveLimit)
156
- notices.push(`${effectiveLimit} limit reached`);
157
- if (grep.nextCursor) {
158
- const cursorId = cursorStore.store(grep.nextCursor);
159
- notices.push(`More results: cursor="${cursorId}"`);
160
- }
161
-
162
- const textContent = appendNotices(
163
- fffFormatGrepText(grep.items, effectiveLimit),
164
- notices,
165
- );
166
- return makeTextResult<GrepResultDetails>(textContent, {
167
- _type: "grepResult",
168
- text: textContent,
169
- pattern,
170
- matchCount: Math.min(grep.items.length, effectiveLimit),
171
- });
172
- } catch {
173
- /* fall through to SDK */
174
- }
175
- }
176
-
177
- // Ripgrep path (constrained, or FFF unavailable)
178
- if (requestedConstraints || !multiGrepFallback) {
179
- try {
180
- const pathBackedConstraint = Boolean(
181
- requestedConstraints &&
182
- !requestedPath &&
183
- requestedConstraints === effectivePath,
184
- );
185
- const constraintsForRipgrep = pathBackedConstraint
186
- ? undefined
187
- : requestedConstraints;
188
- const notices: string[] = [];
189
-
190
- if (!fffState.finder || fffState.finder.isDestroyed)
191
- notices.push("FFF unavailable, used ripgrep fallback");
192
- else if (hasNativeConstraints)
193
- notices.push("Used ripgrep fallback for constrained search");
194
- else notices.push("Used ripgrep fallback");
195
-
196
- const rgResult = await multiGrepRipgrepFallback({
197
- cwd,
198
- patterns: params.patterns,
199
- path: effectivePath,
200
- constraints: constraintsForRipgrep,
201
- ignoreCase: shouldIgnoreCaseForPatterns(params.patterns),
202
- context: params.context,
203
- limit: effectiveLimit,
204
- signal: sig,
205
- });
206
- const textContent =
207
- normalizeLineEndings(rgResult.text) || "No matches found";
208
- if (rgResult.limitReached)
209
- notices.push(`${effectiveLimit} limit reached`);
210
- const finalText = appendNotices(textContent, notices);
211
-
212
- return makeTextResult<GrepResultDetails>(finalText, {
213
- _type: "grepResult",
214
- text: finalText,
215
- pattern,
216
- matchCount: rgResult.matchCount,
217
- });
218
- } catch (error: unknown) {
219
- const message = getErrorMessage(error);
220
- return makeTextResult(`multi_grep error: ${message}`, {
221
- error: message,
222
- });
223
- }
224
- }
225
-
226
- // SDK grep fallback
227
- try {
228
- const notices: string[] = [];
229
- if (!fffState.finder || fffState.finder.isDestroyed)
230
- notices.push("FFF unavailable, used SDK grep fallback");
231
-
232
- const result = await multiGrepFallback.execute(
233
- tid,
234
- {
235
- pattern,
236
- path: effectivePath,
237
- ignoreCase: shouldIgnoreCaseForPatterns(params.patterns),
238
- context: params.context,
239
- limit: params.limit,
240
- },
241
- sig,
242
- upd as never,
243
- toolCtx,
244
- );
245
- const textContent =
246
- normalizeLineEndings(getTextContent(result)) || "No matches found";
247
- const finalText = appendNotices(textContent, notices);
248
-
249
- return makeTextResult<GrepResultDetails>(finalText, {
250
- _type: "grepResult",
251
- text: finalText,
252
- pattern,
253
- matchCount: textContent ? countRipgrepMatches(textContent) : 0,
254
- });
255
- } catch (error: unknown) {
256
- const message = getErrorMessage(error);
257
- return makeTextResult(`multi_grep error: ${message}`, {
258
- error: message,
259
- });
260
- }
261
- },
262
-
263
- renderCall(
264
- args: MultiGrepParams,
265
- theme: ThemeLike,
266
- renderCtx: RenderContextLike,
267
- ) {
268
- const patterns = args.patterns ?? [];
269
- const path = args.path
270
- ? ` ${theme.fg("muted", `in ${sp(args.path)}`)}`
271
- : "";
272
- const constraints = args.constraints;
273
- const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
274
- let content =
275
- theme.fg("toolTitle", theme.bold("multi_grep")) +
276
- " " +
277
- theme.fg("accent", patterns.map((p) => `"${p}"`).join(", "));
278
- content += path;
279
- if (constraints) content += theme.fg("muted", ` (${constraints})`);
280
- text.setText(fillToolBackground(content));
281
- return text;
282
- },
283
-
284
- renderResult(
285
- result: ToolResultLike<GrepResultDetails | { error?: string }>,
286
- _opt: ToolRenderResultOptions,
287
- theme: ThemeLike,
288
- renderCtx: RenderContextLike,
289
- ) {
290
- const text = renderCtx.lastComponent ?? new TextComponent("", 0, 0);
291
-
292
- if (renderCtx.isError) {
293
- text.setText(renderToolError(getTextContent(result) || "Error", theme));
294
- return text;
295
- }
296
-
297
- const d = result.details;
298
- const isGrep = d && "_type" in d && d._type === "grepResult";
299
- const output = getTextContent(result) || "searched";
300
- text.setText(
301
- renderDimPreview(output, theme, {
302
- header: isGrep
303
- ? pluralize(d.matchCount, "match", "matches")
304
- : undefined,
305
- highlight: isGrep ? d.pattern : undefined,
306
- }),
307
- );
308
- return text;
309
- },
310
- });
311
- }