Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/cli/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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__/**"]
}
```

Expand Down
18 changes: 18 additions & 0 deletions packages/cli/src/config/config.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
},
},
Expand Down
78 changes: 74 additions & 4 deletions packages/cli/src/ui/components/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, PendingValue>
>(new Map());
Expand Down Expand Up @@ -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
};
Expand All @@ -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)) {
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 += '*';
}
Expand Down
27 changes: 18 additions & 9 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -205,6 +214,7 @@ export interface ConfigParameters {
respectGeminiIgnore?: boolean;
enableRecursiveFileSearch?: boolean;
disableFuzzySearch?: boolean;
globalExcludes?: string[];
};
checkpointing?: boolean;
proxy?: string;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 ?? [];
Expand Down Expand Up @@ -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 {
Expand All @@ -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;
}
Expand Down
24 changes: 23 additions & 1 deletion packages/core/src/services/fileDiscoveryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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
*/
Expand Down Expand Up @@ -100,13 +115,20 @@ export class FileDiscoveryService {
if (respectGeminiIgnore && this.shouldGeminiIgnoreFile(filePath)) {
return true;
}
if (this.globalExcludesFilter && this.globalExcludesFilter.isIgnored(filePath)) {
return true;
}
return false;
}

/**
* 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;
}
}
2 changes: 1 addition & 1 deletion packages/core/src/utils/gitIgnoreParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
2 changes: 0 additions & 2 deletions packages/core/src/utils/ignorePatterns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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];
Expand Down