diff --git a/packages/coding-agent/src/prompts/tools/find.md b/packages/coding-agent/src/prompts/tools/find.md index 310850fc6..440db34bb 100644 --- a/packages/coding-agent/src/prompts/tools/find.md +++ b/packages/coding-agent/src/prompts/tools/find.md @@ -3,7 +3,8 @@ Fast file pattern matching that works with any codebase size. -- Supports glob patterns like `**/*.js` or `src/**/*.ts` +- Pattern includes the search path: `src/**/*.ts`, `lib/*.json`, `**/*.md` +- Simple patterns like `*.ts` automatically search recursively from cwd - Includes hidden files by default (use `hidden: false` to exclude) - Speculatively perform multiple searches in parallel when potentially useful diff --git a/packages/coding-agent/src/tools/find.ts b/packages/coding-agent/src/tools/find.ts index 956bc6928..c708947d1 100644 --- a/packages/coding-agent/src/tools/find.ts +++ b/packages/coding-agent/src/tools/find.ts @@ -22,8 +22,7 @@ import { toolResult } from "./tool-result"; import { type TruncationResult, truncateHead } from "./truncate"; const findSchema = Type.Object({ - pattern: Type.String({ description: "Glob pattern, e.g. '*.ts', '**/*.json'" }), - path: Type.Optional(Type.String({ description: "Directory to search (default: cwd)" })), + pattern: Type.String({ description: "Glob pattern, e.g. '*.ts', 'src/**/*.json', 'lib/*.tsx'" }), hidden: Type.Optional(Type.Boolean({ description: "Include hidden files and directories (default: true)" })), limit: Type.Optional(Type.Number({ description: "Max results (default: 1000)" })), }); @@ -31,6 +30,51 @@ const findSchema = Type.Object({ const DEFAULT_LIMIT = 1000; const GLOB_TIMEOUT_MS = 5000; +/** + * Parse a pattern to extract the base directory path and glob pattern. + * Examples: + * "src/app/**\/*.tsx" → { basePath: "src/app", globPattern: "**\/*.tsx" } + * "src/app/*.tsx" → { basePath: "src/app", globPattern: "*.tsx" } + * "*.ts" → { basePath: ".", globPattern: "**\/*.ts" } + * "**\/*.json" → { basePath: ".", globPattern: "**\/*.json" } + * "/abs/path/**\/*.ts" → { basePath: "/abs/path", globPattern: "**\/*.ts" } + */ +function parsePatternPath(pattern: string): { basePath: string; globPattern: string } { + // Find the first segment containing glob characters + const segments = pattern.split("/"); + const globChars = ["*", "?", "[", "{"]; + + let firstGlobIndex = -1; + for (let i = 0; i < segments.length; i++) { + if (globChars.some(c => segments[i].includes(c))) { + firstGlobIndex = i; + break; + } + } + + // No glob characters found - treat as literal path with implicit **/* + if (firstGlobIndex === -1) { + // Pattern is a directory path like "src/app" - search recursively in it + return { basePath: pattern, globPattern: "**/*" }; + } + + // Glob starts at first segment - no base path + if (firstGlobIndex === 0) { + // Simple pattern like "*.ts" needs **/ prefix for recursive search + const needsRecursive = !pattern.startsWith("**/"); + return { + basePath: ".", + globPattern: needsRecursive ? `**/${pattern}` : pattern, + }; + } + + // Split at the glob boundary + const basePath = segments.slice(0, firstGlobIndex).join("/"); + const globPattern = segments.slice(firstGlobIndex).join("/"); + + return { basePath, globPattern }; +} + export interface FindToolDetails { truncation?: TruncationResult; resultLimitReached?: number; @@ -81,10 +125,19 @@ export class FindTool implements AgentTool { _onUpdate?: AgentToolUpdateCallback, _context?: AgentToolContext, ): Promise> { - const { pattern, path: searchDir, limit, hidden } = params; + const { pattern, limit, hidden } = params; return untilAborted(signal, async () => { - const searchPath = resolveToCwd(searchDir || ".", this.session.cwd); + // Parse pattern to extract base directory and glob pattern + // e.g., "src/app/**/*.tsx" → basePath: "src/app", globPattern: "**/*.tsx" + // e.g., "*.ts" → basePath: ".", globPattern: "**/*.ts" + const normalizedPattern = pattern.trim().replace(/\\/g, "/"); + if (!normalizedPattern) { + throw new ToolError("Pattern must not be empty"); + } + + const { basePath, globPattern } = parsePatternPath(normalizedPattern); + const searchPath = resolveToCwd(basePath, this.session.cwd); if (searchPath === "/") { throw new ToolError("Searching from root directory '/' is not allowed"); @@ -94,10 +147,6 @@ export class FindTool implements AgentTool { const relative = path.relative(this.session.cwd, searchPath).replace(/\\/g, "/"); return relative.length === 0 ? "." : relative; })(); - const normalizedPattern = pattern.trim(); - if (!normalizedPattern) { - throw new ToolError("Pattern must not be empty"); - } const rawLimit = limit ?? DEFAULT_LIMIT; const effectiveLimit = Number.isFinite(rawLimit) ? Math.floor(rawLimit) : Number.NaN; @@ -105,7 +154,6 @@ export class FindTool implements AgentTool { throw new ToolError("Limit must be a positive number"); } const includeHidden = hidden ?? true; - const globPattern = normalizedPattern.replace(/\\/g, "/"); // If custom operations provided with glob, use that instead of fd if (this.customOps?.glob) { @@ -113,7 +161,7 @@ export class FindTool implements AgentTool { throw new ToolError(`Path not found: ${searchPath}`); } - const results = await this.customOps.glob(normalizedPattern, searchPath, { + const results = await this.customOps.glob(globPattern, searchPath, { ignore: ["**/node_modules/**", "**/.git/**"], limit: effectiveLimit, }); @@ -279,8 +327,6 @@ export class FindTool implements AgentTool { interface FindRenderArgs { pattern: string; - path?: string; - sortByMtime?: boolean; limit?: number; } @@ -290,8 +336,6 @@ export const findToolRenderer = { inline: true, renderCall(args: FindRenderArgs, uiTheme: Theme): Component { const meta: string[] = []; - if (args.path) meta.push(`in ${args.path}`); - if (args.sortByMtime) meta.push("sort:mtime"); if (args.limit !== undefined) meta.push(`limit:${args.limit}`); const text = renderStatusLine( diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index 2bb49db8e..ec6e72e96 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -484,8 +484,7 @@ function b() { fs.writeFileSync(path.join(testDir, "visible.txt"), "visible"); const result = await findTool.execute("test-call-13", { - pattern: "**/*.txt", - path: testDir, + pattern: `${testDir}/**/*.txt`, hidden: true, }); @@ -504,8 +503,7 @@ function b() { fs.writeFileSync(path.join(testDir, "kept.txt"), "kept"); const result = await findTool.execute("test-call-14", { - pattern: "**/*.txt", - path: testDir, + pattern: `${testDir}/**/*.txt`, }); const output = getTextOutput(result);