From 8090f983dbb916d5b0f2c7793f9c4a5c452e7da6 Mon Sep 17 00:00:00 2001 From: Dhruvil Date: Wed, 24 Dec 2025 13:46:23 +0530 Subject: [PATCH] feat: Add global exclude patterns for file filtering --- docs/cli/configuration.md | 4 +- .../cli/src/config/config.integration.test.ts | 18 +++++ packages/cli/src/config/config.ts | 1 + packages/cli/src/config/settingsSchema.ts | 9 +++ .../cli/src/ui/components/SettingsDialog.tsx | 78 ++++++++++++++++++- packages/core/src/config/config.ts | 27 ++++--- .../core/src/services/fileDiscoveryService.ts | 24 +++++- packages/core/src/utils/gitIgnoreParser.ts | 2 +- packages/core/src/utils/ignorePatterns.ts | 2 - 9 files changed, 147 insertions(+), 18 deletions(-) diff --git a/docs/cli/configuration.md b/docs/cli/configuration.md index b3c40d3..fe332b8 100644 --- a/docs/cli/configuration.md +++ b/docs/cli/configuration.md @@ -66,12 +66,14 @@ In addition to a project settings file, a project's `.blackboxcli` directory can - **`respectGitIgnore`** (boolean): Whether to respect .gitignore patterns when discovering files. When set to `true`, git-ignored files (like `node_modules/`, `dist/`, `.env`) are automatically excluded from @ commands and file listing operations. - **`enableRecursiveFileSearch`** (boolean): Whether to enable searching recursively for filenames under the current tree when completing @ prefixes in the prompt. - **`disableFuzzySearch`** (boolean): When `true`, disables the fuzzy search capabilities when searching for files, which can improve performance on projects with a large number of files. + - **`globalExcludes`** (array of strings): Global file exclusion patterns that apply to all projects. These patterns are combined with default exclusions and project-specific .blackboxignore files. Supports glob patterns. - **Example:** ```json "fileFiltering": { "respectGitIgnore": true, "enableRecursiveFileSearch": false, - "disableFuzzySearch": true + "disableFuzzySearch": true, + "globalExcludes": ["dist/", ".DS_Store", "**/*.pyc", "**/__pycache__/**"] } ``` diff --git a/packages/cli/src/config/config.integration.test.ts b/packages/cli/src/config/config.integration.test.ts index 00cec55..43b4095 100644 --- a/packages/cli/src/config/config.integration.test.ts +++ b/packages/cli/src/config/config.integration.test.ts @@ -119,6 +119,24 @@ describe('Configuration Integration Tests', () => { expect(config.getFileFilteringRespectGitIgnore()).toBe(true); }); + + it('should load global exclude patterns from configuration', async () => { + const configParams: ConfigParameters = { + cwd: '/tmp', + contentGeneratorConfig: TEST_CONTENT_GENERATOR_CONFIG, + embeddingModel: 'test-embedding-model', + sandbox: false, + targetDir: tempDir, + debugMode: false, + fileFiltering: { + globalExcludes: ['dist/', '.DS_Store', '**/*.pyc'], + }, + }; + + const config = new Config(configParams); + + expect(config.getCustomExcludes()).toEqual(['dist/', '.DS_Store', '**/*.pyc']); + }); }); describe('Configuration Integration', () => { diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 196d88c..ff2c8ba 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -673,6 +673,7 @@ export async function loadCliConfig( enableRecursiveFileSearch: settings.context?.fileFiltering?.enableRecursiveFileSearch, disableFuzzySearch: settings.context?.fileFiltering?.disableFuzzySearch, + globalExcludes: settings.context?.fileFiltering?.globalExcludes, }, checkpointing: argv.checkpointing || settings.general?.checkpointing?.enabled, diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 63d3558..c7d7daf 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -473,6 +473,15 @@ export const SETTINGS_SCHEMA = { description: 'Disable fuzzy search when searching for files.', showInDialog: true, }, + globalExcludes: { + type: 'array', + label: 'Global Exclude Patterns', + category: 'Context', + requiresRestart: true, + default: [] as string[], + description: 'Global file exclusion patterns that apply to all projects (e.g., ["dist/", ".DS_Store", "**/*.pyc"])', + showInDialog: true, + }, }, }, }, diff --git a/packages/cli/src/ui/components/SettingsDialog.tsx b/packages/cli/src/ui/components/SettingsDialog.tsx index 1b60324..d239b7e 100644 --- a/packages/cli/src/ui/components/SettingsDialog.tsx +++ b/packages/cli/src/ui/components/SettingsDialog.tsx @@ -76,7 +76,7 @@ export function SettingsDialog({ ); // Preserve pending changes across scope switches - type PendingValue = boolean | number | string; + type PendingValue = boolean | number | string | unknown[]; const [globalPendingChanges, setGlobalPendingChanges] = useState< Map >(new Map()); @@ -237,7 +237,34 @@ export function SettingsDialog({ const startEditing = (key: string, initial?: string) => { setEditingKey(key); - const initialValue = initial ?? ''; + let initialValue = initial ?? ''; + + if (!initial) { + const definition = getSettingDefinition(key); + const type = definition?.type; + if (type === 'array') { + // For arrays, initialize with current value as JSON + const path = key.split('.'); + const currentValue = getNestedValue(pendingSettings, path); + const defaultValue = getDefaultValue(key); + const effectiveValue = currentValue !== undefined && currentValue !== null + ? currentValue + : defaultValue; + initialValue = JSON.stringify(effectiveValue || []); + } else if (type === 'number' || type === 'string') { + // For numbers/strings, initialize with current value + const path = key.split('.'); + const currentValue = getNestedValue(pendingSettings, path); + const defaultValue = getDefaultValue(key); + const effectiveValue = currentValue !== undefined && currentValue !== null + ? currentValue + : defaultValue; + initialValue = effectiveValue !== undefined && effectiveValue !== null + ? String(effectiveValue) + : ''; + } + } + setEditBuffer(initialValue); setEditCursorPos(cpLen(initialValue)); // Position cursor at end of initial value }; @@ -254,7 +281,7 @@ export function SettingsDialog({ return; } - let parsed: string | number; + let parsed: string | number | unknown[]; if (type === 'number') { const numParsed = Number(editBuffer.trim()); if (Number.isNaN(numParsed)) { @@ -265,6 +292,19 @@ export function SettingsDialog({ return; } parsed = numParsed; + } else if (type === 'array') { + try { + parsed = JSON.parse(editBuffer.trim()); + if (!Array.isArray(parsed)) { + throw new Error('Not an array'); + } + } catch { + // Invalid JSON array; cancel edit + setEditingKey(null); + setEditBuffer(''); + setEditCursorPos(0); + return; + } } else { // For strings, use the buffer as is. parsed = editBuffer; @@ -368,6 +408,7 @@ export function SettingsDialog({ if (type === 'number') { pasted = key.sequence.replace(/[^0-9\-+.]/g, ''); } + // For arrays, strings, allow all characters if (pasted) { setEditBuffer((b) => { const before = cpSlice(b, 0, editCursorPos); @@ -479,7 +520,8 @@ export function SettingsDialog({ const currentItem = items[activeSettingIndex]; if ( currentItem?.type === 'number' || - currentItem?.type === 'string' + currentItem?.type === 'string' || + currentItem?.type === 'array' ) { startEditing(currentItem.value); } else { @@ -699,6 +741,34 @@ export function SettingsDialog({ const isDifferentFromDefault = effectiveCurrentValue !== defaultValue; + if (isDifferentFromDefault || isModified) { + displayValue += '*'; + } + } else if (item.type === 'array') { + // For arrays, get the actual current value from pending settings + const path = item.value.split('.'); + const currentValue = getNestedValue(pendingSettings, path); + + const defaultValue = getDefaultValue(item.value); + + if (currentValue !== undefined && currentValue !== null) { + displayValue = JSON.stringify(currentValue); + } else { + displayValue = + defaultValue !== undefined && defaultValue !== null + ? JSON.stringify(defaultValue) + : '[]'; + } + + // Add * if value differs from default OR if currently being modified + const isModified = modifiedSettings.has(item.value); + const effectiveCurrentValue = + currentValue !== undefined && currentValue !== null + ? currentValue + : defaultValue; + const isDifferentFromDefault = + JSON.stringify(effectiveCurrentValue) !== JSON.stringify(defaultValue); + if (isDifferentFromDefault || isModified) { displayValue += '*'; } diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index bfb4f61..3d115ec 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -121,16 +121,25 @@ export interface GeminiCLIExtension { export interface FileFilteringOptions { respectGitIgnore: boolean; respectGeminiIgnore: boolean; + enableRecursiveFileSearch?: boolean; + disableFuzzySearch?: boolean; + globalExcludes?: string[]; } // For memory files export const DEFAULT_MEMORY_FILE_FILTERING_OPTIONS: FileFilteringOptions = { respectGitIgnore: false, respectGeminiIgnore: true, + enableRecursiveFileSearch: true, + disableFuzzySearch: false, + globalExcludes: [], }; // For all other files export const DEFAULT_FILE_FILTERING_OPTIONS: FileFilteringOptions = { respectGitIgnore: true, respectGeminiIgnore: true, + enableRecursiveFileSearch: true, + disableFuzzySearch: false, + globalExcludes: [], }; export class MCPServerConfig { constructor( @@ -205,6 +214,7 @@ export interface ConfigParameters { respectGeminiIgnore?: boolean; enableRecursiveFileSearch?: boolean; disableFuzzySearch?: boolean; + globalExcludes?: string[]; }; checkpointing?: boolean; proxy?: string; @@ -296,6 +306,7 @@ export class Config { respectGeminiIgnore: boolean; enableRecursiveFileSearch: boolean; disableFuzzySearch: boolean; + globalExcludes: string[]; }; private fileDiscoveryService: FileDiscoveryService | null = null; private gitService: GitService | undefined = undefined; @@ -408,11 +419,15 @@ export class Config { enableRecursiveFileSearch: params.fileFiltering?.enableRecursiveFileSearch ?? true, disableFuzzySearch: params.fileFiltering?.disableFuzzySearch ?? false, + globalExcludes: params.fileFiltering?.globalExcludes ?? [], }; this.checkpointing = params.checkpointing ?? false; this.proxy = params.proxy; this.cwd = params.cwd ?? process.cwd(); this.fileDiscoveryService = params.fileDiscoveryService ?? null; + if (this.fileDiscoveryService) { + this.fileDiscoveryService.setGlobalExcludes(this.fileFiltering.globalExcludes); + } this.bugCommand = params.bugCommand; this.model = params.model; this.extensionContextFilePaths = params.extensionContextFilePaths ?? []; @@ -818,17 +833,10 @@ export class Config { /** * Gets custom file exclusion patterns from configuration. - * TODO: This is a placeholder implementation. In the future, this could - * read from settings files, CLI arguments, or environment variables. + * Returns patterns defined in the global settings. */ getCustomExcludes(): string[] { - // Placeholder implementation - returns empty array for now - // Future implementation could read from: - // - User settings file - // - Project-specific configuration - // - Environment variables - // - CLI arguments - return []; + return this.fileFiltering.globalExcludes; } getCheckpointingEnabled(): boolean { @@ -850,6 +858,7 @@ export class Config { getFileService(): FileDiscoveryService { if (!this.fileDiscoveryService) { this.fileDiscoveryService = new FileDiscoveryService(this.targetDir); + this.fileDiscoveryService.setGlobalExcludes(this.fileFiltering.globalExcludes); } return this.fileDiscoveryService; } diff --git a/packages/core/src/services/fileDiscoveryService.ts b/packages/core/src/services/fileDiscoveryService.ts index 264dd9d..596f843 100644 --- a/packages/core/src/services/fileDiscoveryService.ts +++ b/packages/core/src/services/fileDiscoveryService.ts @@ -14,11 +14,13 @@ const GEMINI_IGNORE_FILE_NAME = '.blackboxignore'; export interface FilterFilesOptions { respectGitIgnore?: boolean; respectGeminiIgnore?: boolean; + respectGlobalExcludes?: boolean; } export class FileDiscoveryService { private gitIgnoreFilter: GitIgnoreFilter | null = null; private geminiIgnoreFilter: GitIgnoreFilter | null = null; + private globalExcludesFilter: GitIgnoreFilter | null = null; private projectRoot: string; constructor(projectRoot: string) { @@ -41,6 +43,19 @@ export class FileDiscoveryService { this.geminiIgnoreFilter = gParser; } + /** + * Sets global exclude patterns that apply across all projects + */ + setGlobalExcludes(patterns: string[]): void { + if (patterns.length > 0) { + const parser = new GitIgnoreParser(this.projectRoot); + parser.addPatterns(patterns); + this.globalExcludesFilter = parser; + } else { + this.globalExcludesFilter = null; + } + } + /** * Filters a list of file paths based on git ignore rules */ @@ -100,6 +115,9 @@ export class FileDiscoveryService { if (respectGeminiIgnore && this.shouldGeminiIgnoreFile(filePath)) { return true; } + if (this.globalExcludesFilter && this.globalExcludesFilter.isIgnored(filePath)) { + return true; + } return false; } @@ -107,6 +125,10 @@ export class FileDiscoveryService { * Returns loaded patterns from .blackboxignore */ getGeminiIgnorePatterns(): string[] { - return this.geminiIgnoreFilter?.getPatterns() ?? []; + const patterns = this.geminiIgnoreFilter?.getPatterns() ?? []; + if (this.globalExcludesFilter) { + patterns.push(...this.globalExcludesFilter.getPatterns()); + } + return patterns; } } diff --git a/packages/core/src/utils/gitIgnoreParser.ts b/packages/core/src/utils/gitIgnoreParser.ts index 177a0d2..9473531 100644 --- a/packages/core/src/utils/gitIgnoreParser.ts +++ b/packages/core/src/utils/gitIgnoreParser.ts @@ -51,7 +51,7 @@ export class GitIgnoreParser implements GitIgnoreFilter { this.addPatterns(patterns); } - private addPatterns(patterns: string[]) { + addPatterns(patterns: string[]): void { this.ig.add(patterns); this.patterns.push(...patterns); } diff --git a/packages/core/src/utils/ignorePatterns.ts b/packages/core/src/utils/ignorePatterns.ts index 9f9776d..2a6d052 100644 --- a/packages/core/src/utils/ignorePatterns.ts +++ b/packages/core/src/utils/ignorePatterns.ts @@ -164,7 +164,6 @@ export class FileExclusions { } // Add custom patterns from configuration - // TODO: getCustomExcludes method needs to be implemented in Config interface if (this.config) { const configCustomExcludes = this.config.getCustomExcludes?.() ?? []; patterns.push(...configCustomExcludes); @@ -199,7 +198,6 @@ export class FileExclusions { const corePatterns = this.getCoreIgnorePatterns(); // Add any custom patterns from config if available - // TODO: getCustomExcludes method needs to be implemented in Config interface const configPatterns = this.config?.getCustomExcludes?.() ?? []; return [...corePatterns, ...configPatterns, ...additionalExcludes];