From 6b55991099074d9ea68e97921a04aa211abb11a8 Mon Sep 17 00:00:00 2001 From: JSap0914 Date: Fri, 3 Jul 2026 16:32:34 +0900 Subject: [PATCH] fix(core): match ls ignore globs by relative path --- packages/core/src/tools/ls.test.ts | 45 ++++++++++++++++++++++++++++++ packages/core/src/tools/ls.ts | 27 ++++++++++-------- 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/packages/core/src/tools/ls.test.ts b/packages/core/src/tools/ls.test.ts index bc9a548bc22..97b144e4932 100644 --- a/packages/core/src/tools/ls.test.ts +++ b/packages/core/src/tools/ls.test.ts @@ -183,6 +183,51 @@ describe('LSTool', () => { }); }); + it('should respect workspace-relative ignore patterns', async () => { + const distDir = path.join(tempRootDir, 'dist'); + await fs.mkdir(distDir); + await fs.writeFile(path.join(distDir, 'app.js'), 'content1'); + await fs.writeFile(path.join(distDir, 'keep.txt'), 'content2'); + + const invocation = lsTool.build({ + dir_path: distDir, + ignore: ['dist/*.js'], + }); + const result = await invocation.execute({ abortSignal }); + + expect(result.llmContent).not.toContain('app.js'); + expect(result.llmContent).toContain('keep.txt'); + expect(result.returnDisplay).toEqual({ + summary: 'Found 1 item(s).', + files: expect.any(Array), + }); + }); + + it('should respect globstar ignore patterns while keeping basename patterns', async () => { + const logsDir = path.join(tempRootDir, 'logs', 'nested'); + await fs.mkdir(logsDir, { recursive: true }); + await fs.writeFile(path.join(logsDir, 'error.log'), 'content1'); + await fs.writeFile(path.join(logsDir, 'keep.txt'), 'content2'); + + const globstarInvocation = lsTool.build({ + dir_path: logsDir, + ignore: ['**/*.log'], + }); + const globstarResult = await globstarInvocation.execute({ abortSignal }); + + expect(globstarResult.llmContent).not.toContain('error.log'); + expect(globstarResult.llmContent).toContain('keep.txt'); + + const basenameInvocation = lsTool.build({ + dir_path: logsDir, + ignore: ['*.log'], + }); + const basenameResult = await basenameInvocation.execute({ abortSignal }); + + expect(basenameResult.llmContent).not.toContain('error.log'); + expect(basenameResult.llmContent).toContain('keep.txt'); + }); + it('should respect gitignore patterns', async () => { await fs.writeFile(path.join(tempRootDir, 'file1.txt'), 'content1'); await fs.writeFile(path.join(tempRootDir, 'file2.log'), 'content1'); diff --git a/packages/core/src/tools/ls.ts b/packages/core/src/tools/ls.ts index c2e1a593bc8..6b290ba4b08 100644 --- a/packages/core/src/tools/ls.ts +++ b/packages/core/src/tools/ls.ts @@ -7,6 +7,7 @@ import type { MessageBus } from '../confirmation-bus/message-bus.js'; import fs from 'node:fs/promises'; import path from 'node:path'; +import picomatch from 'picomatch'; import { BaseDeclarativeTool, BaseToolInvocation, @@ -93,23 +94,25 @@ class LSToolInvocation extends BaseToolInvocation { } /** - * Checks if a filename matches any of the ignore patterns - * @param filename Filename to check + * Checks if a path matches any of the ignore patterns. Patterns with a path + * separator are matched against the workspace-relative path, while basename + * patterns keep the historical filename-only behavior. + * @param relativePath Workspace-relative path to check * @param patterns Array of glob patterns to check against - * @returns True if the filename should be ignored + * @returns True if the path should be ignored */ - private shouldIgnore(filename: string, patterns?: string[]): boolean { + private shouldIgnore(relativePath: string, patterns?: string[]): boolean { if (!patterns || patterns.length === 0) { return false; } + const normalizedPath = relativePath.replace(/\\/g, '/'); + const filename = path.basename(relativePath); for (const pattern of patterns) { - // Convert glob pattern to RegExp - const regexPattern = pattern - .replace(/[.+^${}()|[\]\\]/g, '\\$&') - .replace(/\*/g, '.*') - .replace(/\?/g, '.'); - const regex = new RegExp(`^${regexPattern}$`); - if (regex.test(filename)) { + const normalizedPattern = pattern.replace(/\\/g, '/'); + const hasPathSeparator = normalizedPattern.includes('/'); + const matcher = picomatch(normalizedPattern, { dot: true }); + + if (matcher(hasPathSeparator ? normalizedPath : filename)) { return true; } } @@ -229,7 +232,7 @@ class LSToolInvocation extends BaseToolInvocation { for (const relativePath of filteredPaths) { const fullPath = path.resolve(this.config.getTargetDir(), relativePath); - if (this.shouldIgnore(path.basename(fullPath), this.params.ignore)) { + if (this.shouldIgnore(relativePath, this.params.ignore)) { continue; }