From 0f97f938f7827a810fe1d45aef48e6daa2f96ee3 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 18 Mar 2026 13:23:40 -0400 Subject: [PATCH] feat: add searchExcludeDirs setting for @ file autocomplete blacklist (#1202) Implements the directory blacklist feature from #660 (incomplete items 3-4). Users can now configure directories to exclude from the @ file picker and fuzzy search via settings.json: { "searchExcludeDirs": ["node_modules", ".git", "dist", "build"] } Changes: - settings-manager.ts: added searchExcludeDirs setting with get/set - autocomplete.ts (pi-tui): CombinedAutocompleteProvider accepts excludeDirs option, filters excluded directory names in both readdir-based and native fuzzy search paths - interactive-mode.ts: passes searchExcludeDirs to the provider The native fd fuzzy search already respects .gitignore. This setting covers directories that aren't gitignored but shouldn't appear in autocomplete (e.g., large vendor dirs, build outputs in projects without comprehensive .gitignore). Fixes #1190 --- .../src/core/settings-manager.ts | 11 ++++++++++ .../src/modes/interactive/interactive-mode.ts | 5 ++++- packages/pi-tui/src/autocomplete.ts | 20 ++++++++++++++++++- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/pi-coding-agent/src/core/settings-manager.ts b/packages/pi-coding-agent/src/core/settings-manager.ts index d9ae1c2ca..603cd2e93 100644 --- a/packages/pi-coding-agent/src/core/settings-manager.ts +++ b/packages/pi-coding-agent/src/core/settings-manager.ts @@ -141,6 +141,7 @@ export interface Settings { editorPaddingX?: number; // Horizontal padding for input editor (default: 0) autocompleteMaxVisible?: number; // Max visible items in autocomplete dropdown (default: 5) respectGitignoreInPicker?: boolean; // When false, @ file picker shows gitignored files (default: true) + searchExcludeDirs?: string[]; // Directories to exclude from @ file search (e.g., ["node_modules", ".git", "dist"]) showHardwareCursor?: boolean; // Show terminal cursor while still positioning it for IME markdown?: MarkdownSettings; memory?: MemorySettings; @@ -1041,6 +1042,16 @@ export class SettingsManager { this.save(); } + getSearchExcludeDirs(): string[] { + return this.settings.searchExcludeDirs ?? []; + } + + setSearchExcludeDirs(dirs: string[]): void { + this.globalSettings.searchExcludeDirs = dirs.filter(Boolean); + this.markModified("searchExcludeDirs"); + this.save(); + } + getCodeBlockIndent(): string { return this.settings.markdown?.codeBlockIndent ?? " "; } diff --git a/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts b/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts index c3db38b86..0247cfd38 100644 --- a/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts @@ -367,7 +367,10 @@ export class InteractiveMode { this.autocompleteProvider = new CombinedAutocompleteProvider( [...slashCommands, ...templateCommands, ...extensionCommands, ...skillCommandList], process.cwd(), - { respectGitignore: this.settingsManager.getRespectGitignoreInPicker() }, + { + respectGitignore: this.settingsManager.getRespectGitignoreInPicker(), + excludeDirs: this.settingsManager.getSearchExcludeDirs(), + }, ); this.defaultEditor.setAutocompleteProvider(this.autocompleteProvider); if (this.editor !== this.defaultEditor) { diff --git a/packages/pi-tui/src/autocomplete.ts b/packages/pi-tui/src/autocomplete.ts index 0fcbdc57e..52ea67c25 100644 --- a/packages/pi-tui/src/autocomplete.ts +++ b/packages/pi-tui/src/autocomplete.ts @@ -131,21 +131,27 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { private commands: (SlashCommand | AutocompleteItem)[]; private basePath: string; private respectGitignore: boolean; + private excludeDirs: Set; constructor( commands: (SlashCommand | AutocompleteItem)[] = [], basePath: string = process.cwd(), - options?: { respectGitignore?: boolean }, + options?: { respectGitignore?: boolean; excludeDirs?: string[] }, ) { this.commands = commands; this.basePath = basePath; this.respectGitignore = options?.respectGitignore ?? true; + this.excludeDirs = new Set(options?.excludeDirs ?? []); } setRespectGitignore(value: boolean): void { this.respectGitignore = value; } + setExcludeDirs(dirs: string[]): void { + this.excludeDirs = new Set(dirs.filter(Boolean)); + } + getSuggestions( lines: string[], cursorLine: number, @@ -485,6 +491,11 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { continue; } + // Skip excluded directories + if (this.excludeDirs.has(entry.name)) { + continue; + } + // Check if entry is a directory (or a symlink pointing to a directory) let isDirectory = entry.isDirectory(); if (!isDirectory && entry.isSymbolicLink()) { @@ -578,6 +589,13 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider { for (const { path: entryPath, isDirectory } of result.matches) { // Native module includes trailing / for directories const pathWithoutSlash = isDirectory ? entryPath.slice(0, -1) : entryPath; + + // Skip paths that start with or contain an excluded directory + if (this.excludeDirs.size > 0) { + const segments = pathWithoutSlash.split("/"); + if (segments.some(seg => this.excludeDirs.has(seg))) continue; + } + const displayPath = scopedQuery ? this.scopedPathForDisplay(scopedQuery.displayBase, pathWithoutSlash) : pathWithoutSlash;