Skip to content

fix(tui): skip hidden worktrees in workspace discovery to prevent TUI saturation#2329

Merged
Hmbown merged 3 commits into
Hmbown:mainfrom
donglovejava:fix/hidden-worktree-discovery-saturation
May 31, 2026
Merged

fix(tui): skip hidden worktrees in workspace discovery to prevent TUI saturation#2329
Hmbown merged 3 commits into
Hmbown:mainfrom
donglovejava:fix/hidden-worktree-discovery-saturation

Conversation

@donglovejava
Copy link
Copy Markdown
Contributor

@donglovejava donglovejava commented May 28, 2026

Summary

When sub-agents fan out during release work, each agent independently triggers workspace discovery walks. If hidden git worktrees (e.g. .claude/worktrees/, .worktrees/) exist inside the repository, these walks traverse thousands of generated files, consuming excessive disk I/O and saturating the TUI event loop — causing visible freeze/hang.

Two fixes:

  1. Cherry-pick 4ec2df9a — skip .worktrees/, .claude/worktrees/, .deepseek/snapshots/ in the discovery walk builder (workspace_discovery.rs), and skip them in the file-index builder (working_set.rs) and file picker (file_picker.rs).

  2. Add .worktrees to PACK_IGNORED_DIRS in project_context.rs — prevents the directory structure packer from enumerating hidden worktree contents when generating the project context block.

Changes

File Change
workspace_discovery.rs NEW: shared discovery filters (DISCOVERY_ALWAYS_DIRS, DISCOVERY_EXCLUDED_SUBDIRS, path_is_excluded_from_discovery, should_skip_unignored_discovery_entry)
working_set.rs Use shared discovery filters; skip excluded worktree bulk in both the file index builder and the completions() walk
tui/file_picker.rs Use shared discovery filters; skip excluded worktree bulk in collect_candidates()
project_context.rs Add .worktrees to PACK_IGNORED_DIRS
main.rs Register workspace_discovery module

Verification

  • cargo check -p codewhale-tui — passes
  • 33 related tests pass (working_set, file_picker, workspace_discovery)

Related

Closes the release-blocker worktree saturation issue.

Greptile Summary

This PR introduces a shared workspace_discovery module with centralized exclusion filters to prevent hidden git worktrees (.claude/worktrees/, .worktrees/) from saturating the TUI event loop during workspace discovery walks. It also adds .worktrees to PACK_IGNORED_DIRS in the project context packer.

  • workspace_discovery.rs centralises DISCOVERY_EXCLUDED_SUBDIRS, DISCOVERY_EXCLUDED_DIR_NAMES, and two predicate functions; local_reference_paths is correctly updated to use filter_entry for subtree pruning.
  • build_file_index (working_set.rs) and collect_candidates (file_picker.rs) both still use a per-entry continue guard instead of filter_entry, meaning those walks descend fully into excluded worktree subtrees before discarding entries — the same saturation pattern the PR intends to fix.

Confidence Score: 4/5

The fix is partially applied: local_reference_paths gains proper subtree pruning, but build_file_index and the file-picker's dot-directory walk still traverse excluded worktree directories in full before discarding entries.

local_reference_paths is correctly fixed with filter_entry, but two other hot paths — build_file_index (called on first @-mention) and collect_candidates in the file picker — retain the old per-entry continue pattern. On a repo with several agent worktrees these walks will still fully descend into .claude/worktrees/ and discard thousands of entries one-by-one, reproducing the I/O saturation the PR set out to eliminate.

crates/tui/src/working_set.rs (build_file_index and walk_always_discoverable_dirs) and crates/tui/src/tui/file_picker.rs (collect_candidates) both need filter_entry on their dot-directory WalkBuilder instances.

Important Files Changed

Filename Overview
crates/tui/src/workspace_discovery.rs New module cleanly centralises discovery filter constants and predicates; should_skip_unignored_discovery_entry is correct and correctly used via filter_entry in local_reference_paths.
crates/tui/src/working_set.rs local_reference_paths is fixed with filter_entry, but build_file_index and walk_always_discoverable_dirs still use per-entry continue, which traverses excluded worktree subtrees without pruning them.
crates/tui/src/tui/file_picker.rs collect_candidates imports and calls path_is_excluded_from_discovery with a plain continue, so the walk still descends fully into .claude/worktrees/ and .deepseek/snapshots/ before discarding entries.
crates/tui/src/project_context.rs Adds .worktrees to PACK_IGNORED_DIRS; straightforward and correct addition.
crates/tui/src/main.rs Single-line module registration for workspace_discovery; no issues.

Comments Outside Diff (1)

  1. crates/tui/src/working_set.rs, line 317-326 (link)

    P1 path_is_excluded_from_discovery doesn't prune the walk, only skips individual entries

    walk_always_discoverable_dirs uses a plain continue to discard excluded entries, but the WalkBuilder iterator has already descended into the excluded directory and queued all its children. For a .claude/worktrees directory containing several agent checkouts — each a full project tree — the iterator will generate and discard thousands of entries before the exclusion check has any effect. The limit guard (>= limit) only fires once enough non-excluded hits accumulate, so a huge worktree subtree is walked completely on every @-completion query.

    local_reference_paths on the same PR correctly solves this with filter_entry, which prunes entire subtrees at directory boundaries. Applying the same filter_entry(move |entry| !should_skip_unignored_discovery_entry(...)) pattern to walk_always_discoverable_dirs (and the analogous loop in file_picker::collect_candidates) would actually stop the walk from descending into .claude/worktrees/ rather than just discarding its contents.

    Fix in Codex Fix in Claude Code Fix in Cursor

Fix All in Codex Fix All in Claude Code Fix All in Cursor

Reviews (2): Last reviewed commit: "fix: trailing newline in project_context..." | Re-trigger Greptile

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request centralizes workspace discovery logic and constants into a new workspace_discovery module, while also adding .worktrees and .claude/worktrees to the excluded directories to prevent unnecessary indexing. Feedback on the changes highlights an optimization opportunity in path_is_excluded_from_discovery to avoid heap allocations inside a hot loop by stripping the root prefix before comparison.

Comment on lines +34 to +38
pub(crate) fn path_is_excluded_from_discovery(walk_root: &Path, path: &Path) -> bool {
DISCOVERY_EXCLUDED_SUBDIRS
.iter()
.any(|excluded| path.starts_with(walk_root.join(excluded)))
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Efficiency & Correctness: Unnecessary Heap Allocations in Hot Loop

Using walk_root.join(excluded) inside any(...) allocates a new PathBuf on the heap for every excluded subdirectory on every single file/directory entry visited during workspace discovery. In large repositories with tens of thousands of files, this results in hundreds of thousands of unnecessary heap allocations, which can significantly degrade TUI responsiveness and tab-completion performance.

Additionally, comparing absolute and relative paths directly using starts_with can be fragile if there are differences in prefix representation (e.g., ./.worktrees vs .worktrees).

Solution

We can resolve both issues by stripping the walk_root prefix from path first using strip_prefix. This returns a borrowed &Path slice (zero heap allocations) and allows us to perform a clean, robust relative path comparison.

pub(crate) fn path_is_excluded_from_discovery(walk_root: &Path, path: &Path) -> bool {
    if let Ok(rel_path) = path.strip_prefix(walk_root) {
        DISCOVERY_EXCLUDED_SUBDIRS
            .iter()
            .any(|excluded| rel_path.starts_with(excluded))
    } else {
        false
    }
}

Comment thread crates/tui/src/project_context.rs Outdated
@Hmbown
Copy link
Copy Markdown
Owner

Hmbown commented May 31, 2026

hmm good point.... let me think about how to do this as sub agents working in a single repo is becoming more of the shape for future releases and using side git trees to harvest best use. so thank you very much for raising this so I can solve this before pushing it further

@Hmbown Hmbown merged commit 03c4b6b into Hmbown:main May 31, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants