77//! - pinned message indices that compaction should preserve
88
99use crate :: models:: { ContentBlock , Message } ;
10+ use crate :: workspace_discovery:: {
11+ DISCOVERY_ALWAYS_DIRS , path_is_excluded_from_discovery, should_skip_unignored_discovery_entry,
12+ } ;
1013use ignore:: WalkBuilder ;
1114use regex:: Regex ;
1215use serde:: { Deserialize , Serialize } ;
@@ -269,32 +272,6 @@ const COMPLETIONS_WALK_DEPTH: usize = 6;
269272/// above the actual entry count and the cap is a no-op.
270273const FILE_INDEX_MAX_ENTRIES : usize = 50_000 ;
271274
272- /// Directories that must remain discoverable for `@`-mention completion and
273- /// fuzzy file resolution even when excluded by `.gitignore`. AI-tool
274- /// convention directories (`.deepseek/`, `.cursor/`, `.claude/`, `.agents/`)
275- /// are routinely gitignored, but users need to `@`-mention files inside them.
276- const DISCOVERY_ALWAYS_DIRS : & [ & str ] = & [ ".deepseek" , ".cursor" , ".claude" , ".agents" ] ;
277-
278- /// Subdirectories under `DISCOVERY_ALWAYS_DIRS` that must NOT be indexed
279- /// even when the parent dir is walked with gitignore disabled. These are
280- /// large, machine-generated, or sensitive paths that would blow up the
281- /// walker (e.g. `.deepseek/snapshots/` — the snapshot side repo that
282- /// #1112 caps at 500 MB; indexing it would trigger the same OOM/hang
283- /// the cap was built to prevent).
284- const DISCOVERY_EXCLUDED_SUBDIRS : & [ & str ] = & [ ".deepseek/snapshots" ] ;
285-
286- /// Check whether a path resolved against `walk_root` falls inside any
287- /// `DISCOVERY_EXCLUDED_SUBDIRS` entry. Used to keep the snapshot side
288- /// repo (`.deepseek/snapshots/`) out of the completion/index walk.
289- fn path_is_excluded_from_discovery ( walk_root : & Path , path : & Path ) -> bool {
290- for excluded in DISCOVERY_EXCLUDED_SUBDIRS {
291- if path. starts_with ( walk_root. join ( excluded) ) {
292- return true ;
293- }
294- }
295- false
296- }
297-
298275/// Configure a `WalkBuilder` for workspace discovery: hidden files, no
299276/// symlink following, depth-limited, custom `.deepseekignore` honored,
300277/// and gitignore overrides for AI-tool dot-directories so `@`-completion
@@ -494,7 +471,10 @@ fn local_reference_paths(root: &Path, limit: usize) -> Vec<PathBuf> {
494471 . git_global ( false )
495472 . git_exclude ( false ) ;
496473 let _ = builder. add_custom_ignore_filename ( ".deepseekignore" ) ;
497- builder. filter_entry ( |entry| !should_skip_local_reference_dir ( entry. path ( ) ) ) ;
474+ let root_for_filter = root. to_path_buf ( ) ;
475+ builder. filter_entry ( move |entry| {
476+ !should_skip_unignored_discovery_entry ( & root_for_filter, entry. path ( ) )
477+ } ) ;
498478
499479 for entry in builder. build ( ) . flatten ( ) {
500480 if out. len ( ) >= limit {
@@ -514,25 +494,6 @@ fn local_reference_paths(root: &Path, limit: usize) -> Vec<PathBuf> {
514494 out
515495}
516496
517- fn should_skip_local_reference_dir ( path : & Path ) -> bool {
518- let Some ( name) = path. file_name ( ) . and_then ( |name| name. to_str ( ) ) else {
519- return false ;
520- } ;
521- matches ! (
522- name,
523- ".git"
524- | "target"
525- | "node_modules"
526- | ".venv"
527- | "venv"
528- | "env"
529- | "dist"
530- | "build"
531- | "__pycache__"
532- | ".ruff_cache"
533- )
534- }
535-
536497impl Clone for Workspace {
537498 fn clone ( & self ) -> Self {
538499 // Don't carry the cached file_index — clones get a fresh OnceLock so
@@ -1523,6 +1484,82 @@ mod tests {
15231484 ) ;
15241485 }
15251486
1487+ #[ test]
1488+ fn workspace_completions_skip_hidden_worktrees_and_build_bulk ( ) {
1489+ let tmp = TempDir :: new ( ) . unwrap ( ) ;
1490+ let root = tmp. path ( ) ;
1491+ std:: fs:: write ( root. join ( ".gitignore" ) , ".worktrees/\n .generated/\n " ) . unwrap ( ) ;
1492+
1493+ std:: fs:: create_dir_all ( root. join ( ".worktrees/release/src" ) ) . unwrap ( ) ;
1494+ std:: fs:: write (
1495+ root. join ( ".worktrees/release/src/worktree-only.rs" ) ,
1496+ "fn main() {}" ,
1497+ )
1498+ . unwrap ( ) ;
1499+ std:: fs:: create_dir_all ( root. join ( ".worktrees/release/target/debug" ) ) . unwrap ( ) ;
1500+ std:: fs:: write (
1501+ root. join ( ".worktrees/release/target/debug/generated.o" ) ,
1502+ "object" ,
1503+ )
1504+ . unwrap ( ) ;
1505+
1506+ std:: fs:: create_dir_all ( root. join ( ".claude/worktrees/agent/src" ) ) . unwrap ( ) ;
1507+ std:: fs:: write (
1508+ root. join ( ".claude/worktrees/agent/src/agent-only.md" ) ,
1509+ "agent note" ,
1510+ )
1511+ . unwrap ( ) ;
1512+ std:: fs:: create_dir_all ( root. join ( ".claude/commands" ) ) . unwrap ( ) ;
1513+ std:: fs:: write ( root. join ( ".claude/commands/keep.md" ) , "command" ) . unwrap ( ) ;
1514+
1515+ std:: fs:: create_dir_all ( root. join ( ".generated/specs" ) ) . unwrap ( ) ;
1516+ std:: fs:: write ( root. join ( ".generated/specs/device-layout.md" ) , "layout" ) . unwrap ( ) ;
1517+
1518+ let ws = Workspace :: with_cwd ( root. to_path_buf ( ) , Some ( root. to_path_buf ( ) ) ) ;
1519+
1520+ let worktree_entries = ws. completions ( ".worktrees" , 32 ) ;
1521+ assert ! (
1522+ worktree_entries
1523+ . iter( )
1524+ . all( |entry| !entry. starts_with( ".worktrees/" ) ) ,
1525+ "hidden release worktrees must stay out of completions: {worktree_entries:?}" ,
1526+ ) ;
1527+
1528+ let claude_worktree_entries = ws. completions ( ".claude/worktrees" , 32 ) ;
1529+ assert ! (
1530+ claude_worktree_entries
1531+ . iter( )
1532+ . all( |entry| !entry. starts_with( ".claude/worktrees/" ) ) ,
1533+ ".claude/worktrees must stay out of completions: {claude_worktree_entries:?}" ,
1534+ ) ;
1535+
1536+ let generated_entries = ws. completions ( ".generated/specs" , 32 ) ;
1537+ assert ! (
1538+ generated_entries
1539+ . iter( )
1540+ . any( |entry| entry == ".generated/specs/device-layout.md" ) ,
1541+ "explicit user-generated hidden folders should still complete: {generated_entries:?}" ,
1542+ ) ;
1543+
1544+ let command_entries = ws. completions ( ".claude/commands" , 32 ) ;
1545+ assert ! (
1546+ command_entries
1547+ . iter( )
1548+ . any( |entry| entry == ".claude/commands/keep.md" ) ,
1549+ "normal .claude command files should still complete: {command_entries:?}" ,
1550+ ) ;
1551+
1552+ assert ! (
1553+ ws. resolve( "worktree-only.rs" ) . is_err( ) ,
1554+ "fuzzy resolution must not index files from hidden release worktrees"
1555+ ) ;
1556+ assert ! (
1557+ ws. resolve( "agent-only.md" ) . is_err( ) ,
1558+ "fuzzy resolution must not index files from .claude/worktrees"
1559+ ) ;
1560+ assert ! ( ws. resolve( "keep.md" ) . is_ok( ) ) ;
1561+ }
1562+
15261563 #[ test]
15271564 fn fuzzy_index_resolves_hidden_and_ignored_files_except_deepseekignored ( ) {
15281565 let tmp = TempDir :: new ( ) . unwrap ( ) ;
0 commit comments