merged
  result 100644 7260a098a40009ac8c3c3cff170dd01689d89f37 .cursor/rules/bash-commands.mdc
  our    100644 c4395effb7f002bc54b6a8a9efa2c6335dff7f78 .cursor/rules/bash-commands.mdc
@@ -1,6 +1,6 @@
 ---
-description: bash shell commands
-globs: 
+description:
+globs:
 alwaysApply: false
 ---
 When responding with bash shell commands, make them 'bash 1liners' aka long lines of text, do not split lines using the `\`
\ No newline at end of file
changed in both
  base   100644 30c357a1c6c5e9562c3cd5e41c6e82186348a5ca .cursor/rules/derived-cursor-rules.mdc
  our    100644 d79fdcf0182f35cf14ccf6e98e7678568d5bd167 .cursor/rules/derived-cursor-rules.mdc
  their  100644 e41dc4e8ef92b97cda93af714e7c999a96ad40dc .cursor/rules/derived-cursor-rules.mdc
added in remote
  their  100644 3968e9719f42e1dd181e3db0bfed3c2360c8bbe6 .github/workflows/claude-code-review.yml
@@ -0,0 +1,78 @@
+name: Claude Code Review
+
+on:
+  pull_request:
+    types: [opened, synchronize]
+    # Optional: Only run on specific file changes
+    # paths:
+    #   - "src/**/*.ts"
+    #   - "src/**/*.tsx"
+    #   - "src/**/*.js"
+    #   - "src/**/*.jsx"
+
+jobs:
+  claude-review:
+    # Only run if PR description contains 'claude code-review' or 'claude code review' (case variations)
+    if: |
+      contains(github.event.pull_request.body, 'claude code-review') ||
+      contains(github.event.pull_request.body, 'Claude code-review') ||
+      contains(github.event.pull_request.body, 'claude code review') ||
+      contains(github.event.pull_request.body, 'Claude code review') ||
+      contains(github.event.pull_request.body, 'CLAUDE CODE-REVIEW') ||
+      contains(github.event.pull_request.body, 'CLAUDE CODE REVIEW')
+
+    runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      pull-requests: read
+      issues: read
+      id-token: write
+
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 1
+
+      - name: Run Claude Code Review
+        id: claude-review
+        uses: anthropics/claude-code-action@beta
+        with:
+          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
+
+          # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
+          # model: "claude-opus-4-20250514"
+
+          # Direct prompt for automated review (no @claude mention needed)
+          direct_prompt: |
+            Please review this pull request and provide feedback on:
+            - Code quality and best practices
+            - Potential bugs or issues
+            - Performance considerations
+            - Security concerns
+            - Test coverage
+
+            Be constructive and helpful in your feedback.
+
+          # Optional: Customize review based on file types
+          # direct_prompt: |
+          #   Review this PR focusing on:
+          #   - For TypeScript files: Type safety and proper interface usage
+          #   - For API endpoints: Security, input validation, and error handling
+          #   - For React components: Performance, accessibility, and best practices
+          #   - For tests: Coverage, edge cases, and test quality
+
+          # Optional: Different prompts for different authors
+          # direct_prompt: |
+          #   ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' &&
+          #   'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' ||
+          #   'Please provide a thorough code review focusing on our coding standards and best practices.' }}
+
+          # Optional: Add specific tools for running tests or linting
+          # allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)"
+
+          # Optional: Skip review for certain conditions
+          # if: |
+          #   !contains(github.event.pull_request.title, '[skip-review]') &&
+          #   !contains(github.event.pull_request.title, '[WIP]')
+
added in remote
  their  100644 58d0fa2ec1e29ed237f01f259ed7f65520b20bbc .github/workflows/claude.yml
@@ -0,0 +1,59 @@
+name: Claude Code
+
+on:
+  issue_comment:
+    types: [created]
+  pull_request_review_comment:
+    types: [created]
+  issues:
+    types: [opened, assigned]
+  pull_request_review:
+    types: [submitted]
+
+jobs:
+  claude:
+    if: |
+      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
+      (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
+      (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
+      (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
+    runs-on: ubuntu-latest
+    permissions:
+      contents: read
+      pull-requests: read
+      issues: read
+      id-token: write
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+        with:
+          fetch-depth: 1
+
+      - name: Run Claude Code
+        id: claude
+        uses: anthropics/claude-code-action@beta
+        with:
+          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
+          
+          # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
+          # model: "claude-opus-4-20250514"
+          
+          # Optional: Customize the trigger phrase (default: @claude)
+          # trigger_phrase: "/claude"
+          
+          # Optional: Trigger when specific user is assigned to an issue
+          # assignee_trigger: "claude-bot"
+          
+          # Optional: Allow Claude to run specific commands
+          # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
+          
+          # Optional: Add custom instructions for Claude to customize its behavior for your project
+          # custom_instructions: |
+          #   Follow our coding standards
+          #   Ensure all new code has tests
+          #   Use TypeScript for new files
+          
+          # Optional: Custom environment variables for Claude
+          # claude_env: |
+          #   NODE_ENV: test
+
merged
  result 100644 4dcab38037d0eb0ede7eb0ff73e395efc9aadd21 .gitignore
  our    100644 33cbcef1a4e4b259ed57951417f8a681a0f027a2 .gitignore
@@ -9,4 +9,5 @@
 references/mono-file-version/
 monolithich/
 *.log
-.specstory/ai_rules_backups/*
\ No newline at end of file
+.specstory/ai_rules_backups/*
+test_results/*
\ No newline at end of file
changed in both
  base   100644 42f8ed3a8e13fb0793f682e123868e439731b263 CHANGELOG.md
  our    100644 874bdc68c1997458813a3c037aa852a9f4808b05 CHANGELOG.md
  their  100644 e6da767af34bb8e6ad7ebfe0703e430616bb6035 CHANGELOG.md
@@ -7,6 +7,7 @@
 
 ## [Unreleased]
 
+<<<<<<< .our
 ### Removed
 - Removed unused function `perform_search` from `src/tui/state/search.rs`.
 - Removed unused function `format_selected_items` from `src/tui/handlers/file_ops.rs`.
@@ -15,6 +16,39 @@
 ### Fixed
 - Derived `Default` trait for `Trie` struct in `src/tui/state/app_state.rs` and `SearchState` struct in `src/tui/state/search.rs` to resolve compiler errors when calling `Self::default()` in their `new()` methods.
 - Corrected test script `test_C2_select_all.sh` to accurately verify deselection state by excluding the status bar from checkbox checks.
+=======
+### Testing
+- **Added Comprehensive GitIgnore Testing Suite**: Implemented unit tests to validate and demonstrate the critical gitignore context handling bug described in `CRITICALBUG.md`. Tests include:
+  - `test_gitignore_context_issue_demonstration`: Documents current incorrect behavior
+  - `test_correct_gitignore_behavior_expectations`: Defines expected correct behavior (currently failing)
+  - `test_gitignore_cache_key_problem`: Demonstrates the cache key design flaw
+  - `test_file_contextual_gitignore_requirements`: Documents requirements for file-contextual gitignore processing
+  - Test infrastructure with nested `.gitignore` files to validate hierarchical ignore pattern handling (`src/fs/mod.rs`)
+
+### Security
+- **Fixed Symlink Loop Vulnerability**: Added symlink loop detection in `collect_folder_descendants` function to prevent infinite recursion and potential stack overflow attacks (`src/fs/mod.rs`).
+
+### Fixed
+- **CRITICAL: Fixed Incorrect GitIgnore Context Handling**: Resolved fundamental design flaw where gitignore rules were applied incorrectly due to improper cache key strategy. The previous implementation used only `base_dir` as cache key, causing all files to share the same gitignore matcher regardless of their location in the directory hierarchy. Files are now properly matched against their file-contextual gitignore rules:
+  - Implemented `get_cached_gitignore_matcher_for_context()` to build matchers specific to each file's directory context
+  - Updated `is_path_ignored_iterative()` and `is_path_ignored_iterative_cached()` to use file's parent directory as context
+  - Updated TUI ignore checking in `src/tui/state/app_state.rs` to use file-contextual matching
+  - Files now properly respect nested `.gitignore` files (e.g., `src/components/Button.tsx` is correctly ignored by `src/components/.gitignore`)
+  - Maintained backward compatibility through deprecated legacy function (`src/fs/mod.rs`)
+- **Fixed Race Condition in Selection Operations**: Implemented proper synchronization to prevent race conditions between async selection counting and user input that could cause inconsistent application state (`src/tui/state/selection.rs`, `src/tui/app.rs`).
+- **Fixed Incorrect Regex Pattern**: Corrected malformed regex pattern `"$.^"` to `"^$"` in ignore pattern caching to ensure proper fallback behavior (`src/fs/mod.rs:291`).
+- Derived `Default` trait for `Trie` struct in `src/tui/state/app_state.rs` and `SearchState` struct in `src/tui/state/search.rs` to resolve compiler errors when calling `Self::default()` in their `new()` methods.
+- Corrected test script `test_C2_select_all.sh` to accurately verify deselection state by excluding the status bar from checkbox checks.
+- **Fixed Duplicate Doc Alias Warning**: Removed duplicate `#[doc(alias = "search-state")]` attribute from `SearchState` struct in `src/tui/state/search.rs`.
+
+### Performance
+- **GitIgnore Caching Implementation**: Implemented caching for compiled gitignore matchers, reducing complexity from O(n×m) to O(1) for repeated path checks in the same directory. This provides significant performance improvements for large codebases with multiple .gitignore files (`src/fs/mod.rs`, `src/tui/state/app_state.rs`).
+
+### Removed
+- Removed unused function `perform_search` from `src/tui/state/search.rs`.
+- Removed unused function `format_selected_items` from `src/tui/handlers/file_ops.rs`.
+- Removed unused method `set_message_duration` from `src/tui/views/message_view.rs`.
+>>>>>>> .their
 
 ### Documentation
 - Comprehensive rewrite and standardization of all in-file Rust documentation for the following modules:
@@ -89,6 +123,27 @@
 - **Background Operations**: Clipboard operations now run asynchronously to prevent UI blocking
 - **Caching Strategy**: Intelligent caching with TTL support reduces redundant file system operations
 
+<<<<<<< .our
+=======
+### Testing
+- **Added Enhanced Gitignore Context Handling Tests**: Added comprehensive unit tests for file-contextual gitignore functionality:
+  - `test_get_cached_gitignore_matcher_for_context`: Direct testing of the new file-contextual gitignore function
+  - `test_gitignore_cache_per_directory`: Testing cache behavior for different directory contexts
+  - `test_gitignore_inheritance_validation`: Comprehensive validation of gitignore rule inheritance hierarchy
+- **Added Symlink Loop Detection Tests**: Implemented comprehensive symlink handling tests:
+  - `test_symlink_loop_detection_comprehensive`: Direct testing of symlink loop prevention
+  - `test_symlink_performance_stress`: Performance testing with many symlinks
+  - `test_broken_symlink_handling`: Testing graceful handling of broken symlinks
+- **Added Race Condition Prevention Tests**: Implemented unit tests for TUI race condition fixes (`src/tui/app.rs`):
+  - `test_app_event_variants`: Testing completeness of AppEvent enum
+  - `test_app_creation_with_event_channel`: Testing App creation with event channels
+  - `test_event_channel_non_blocking`: Testing non-blocking event handling
+  - `test_selection_operation_cancellation_pattern`: Testing race condition prevention patterns
+- **Added Additional Edge Case Tests**: Added tests for regex pattern fixes and gitignore cache management:
+  - `test_regex_pattern_fix`: Testing corrected regex patterns
+  - `test_gitignore_cache_management`: Testing gitignore cache clearing behavior
+
+>>>>>>> .their
 ## [0.8.0] - 2025-06-03
 
 ### Performance
@@ -161,4 +216,10 @@
 - Basic dependency analysis
 - Cross-platform clipboard support
 - File filtering and ignore patterns
-- Removed redundant `#[doc(alias = ...)]` attributes from `src/cli/mod.rs`, `src/utils/mod.rs`, and `src/tui/views/mod.rs` to resolve clippy errors about alias matching the item's name.
\ No newline at end of file
+<<<<<<< .our
+- Removed redundant `#[doc(alias = ...)]` attributes from `src/cli/mod.rs`, `src/utils/mod.rs`, and `src/tui/views/mod.rs` to resolve clippy errors about alias matching the item's name.
+=======
+- Removed redundant `#[doc(alias = ...)]` attributes from `src/cli/mod.rs`, `src/utils/mod.rs`, and `src/tui/views/mod.rs` to resolve clippy errors about alias matching the item's name.
+### [Fixed]
+- Removed references to non-existent `perform_search` function from documentation in [`src/tui/state/search.rs`](src/tui/state/search.rs)
+>>>>>>> .their
added in remote
  their  100755 04733ec4f3c2f4711af1f129474c1c3b568c6773 scripts/run_tests.sh
@@ -0,0 +1,59 @@
+#!/bin/bash
+base_dir="/home/tech/projects/aibundle-modular"
+current_dir=$(pwd)
+
+if [ "$current_dir" != "$base_dir" ]; then
+    cd $base_dir || exit 1
+fi
+
+if [ ! -d "test_results" ]; then
+    mkdir -p test_results
+else
+    rm -rf test_results/*
+fi
+
+echo "Running tests..."
+
+CARGO_TERM_COLOR=never cargo test --doc src/cli/mod.rs > test_results/src_cli_mod_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/cli/options.rs > test_results/src_cli_options_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/clipboard/mod.rs > test_results/src_clipboard_mod_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/config/mod.rs > test_results/src_config_mod_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/fs/mod.rs > test_results/src_fs_mod_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/lib.rs > test_results/src_lib_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/models/app_config.rs > test_results/src_models_app_config_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/models/constants.rs > test_results/src_models_constants_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/models/enums.rs > test_results/src_models_enums_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/models/mod.rs > test_results/src_models_mod_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/output/format.rs > test_results/src_output_format_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/output/json.rs > test_results/src_output_json_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/output/llm.rs > test_results/src_output_llm_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/output/markdown.rs > test_results/src_output_markdown_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/output/mod.rs > test_results/src_output_mod_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/output/xml.rs > test_results/src_output_xml_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/tui/app.rs > test_results/src_tui_app_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/tui/components/file_list.rs > test_results/src_tui_components_file_list_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/tui/components/header.rs > test_results/src_tui_components_header_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/tui/components/mod.rs > test_results/src_tui_components_mod_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/tui/components/modal.rs > test_results/src_tui_components_modal_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/tui/components/status_bar.rs > test_results/src_tui_components_status_bar_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/tui/handlers/clipboard.rs > test_results/src_tui_handlers_clipboard_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/tui/handlers/file_ops.rs > test_results/src_tui_handlers_file_ops_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/tui/handlers/keyboard.rs > test_results/src_tui_handlers_keyboard_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/tui/handlers/mod.rs > test_results/src_tui_handlers_mod_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/tui/handlers/search.rs > test_results/src_tui_handlers_search_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/tui/mod.rs > test_results/src_tui_mod_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/tui/state/app_state.rs > test_results/src_tui_state_app_state_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/tui/state/mod.rs > test_results/src_tui_state_mod_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/tui/state/search.rs > test_results/src_tui_state_search_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/tui/state/selection.rs > test_results/src_tui_state_selection_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/tui/views/help_view.rs > test_results/src_tui_views_help_view_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/tui/views/main_view.rs > test_results/src_tui_views_main_view_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/tui/views/message_view.rs > test_results/src_tui_views_message_view_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/tui/views/mod.rs > test_results/src_tui_views_mod_rs.txt
+CARGO_TERM_COLOR=never cargo test --doc src/utils/mod.rs > test_results/src_utils_mod_rs.txt
+
+if [ "$current_dir" != "$base_dir" ]; then
+    cd $current_dir || exit 1
+fi
+
+echo "Tests completed. Results saved in test_results directory."
\ No newline at end of file
changed in both
  base   100644 4a032f9b510b7a10dd804927263b67dafda8426c src/fs/mod.rs
  our    100644 a43308ff0caf390a1c59c615f65f0451b98451f3 src/fs/mod.rs
  their  100644 f6069796807b90b595802f8d961d1de7236aa165 src/fs/mod.rs
@@ -48,6 +48,12 @@
         Arc::new(Mutex::new(HashMap::new()));
 }
 
+// Cache for compiled gitignore matchers to reduce redundant gitignore processing
+lazy_static! {
+    static ref GITIGNORE_CACHE: Arc<Mutex<HashMap<PathBuf, Option<ignore::gitignore::Gitignore>>>> =
+        Arc::new(Mutex::new(HashMap::new()));
+}
+
 /// Binary file signatures for magic number detection
 const BINARY_SIGNATURES: &[(&[u8], &str)] = &[
     // ELF executables
@@ -169,6 +175,117 @@
     std::fs::canonicalize(path)
 }
 
+/// Clears the gitignore cache. Should be called when .gitignore files change
+/// or when switching to a different project directory.
+pub fn clear_gitignore_cache() {
+    if let Ok(mut cache) = GITIGNORE_CACHE.lock() {
+        cache.clear();
+    }
+}
+
+/// Gets or creates a cached gitignore matcher for the given directory context.
+/// This builds a matcher that considers all .gitignore files from the given directory
+/// upwards to the project root. This is file-contextual gitignore matching.
+///
+/// # Arguments
+/// * `context_dir` - The directory context for which to build the gitignore matcher
+/// * `project_root` - The project root directory (used as upper bound for traversal)
+///
+/// Returns None if no gitignore files are found or if compilation fails.
+pub fn get_cached_gitignore_matcher_for_context(
+    context_dir: &PathBuf,
+    project_root: &PathBuf
+) -> Option<ignore::gitignore::Gitignore> {
+    if let Ok(mut cache) = GITIGNORE_CACHE.lock() {
+        if let Some(cached_matcher) = cache.get(context_dir) {
+            return cached_matcher.clone();
+        }
+
+        // Build gitignore matcher if not in cache
+        let mut builder = GitignoreBuilder::new(project_root);
+        let mut dir = context_dir.clone();
+        let mut found_gitignore = false;
+
+        // Traverse upwards from context_dir to project_root to find .gitignore files
+        loop {
+            let gitignore_file = dir.join(".gitignore");
+            if gitignore_file.exists() {
+                // builder.add() returns Some(error) on error, None on success
+                if builder.add(gitignore_file).is_none() {
+                    found_gitignore = true;
+                }
+            }
+
+            // Stop when we reach the project root or can't go further up
+            if dir == *project_root {
+                break;
+            }
+
+            if let Some(parent) = dir.parent() {
+                dir = parent.to_path_buf();
+                // Also stop if we've gone above the project root
+                if !dir.starts_with(project_root) {
+                    break;
+                }
+            } else {
+                break;
+            }
+        }
+
+        let matcher = if found_gitignore {
+            builder.build().ok()
+        } else {
+            None
+        };
+
+        // Cache the result (even if None)
+        cache.insert(context_dir.clone(), matcher.clone());
+        matcher
+    } else {
+        // Fallback if cache lock fails - build without caching
+        let mut builder = GitignoreBuilder::new(project_root);
+        let mut dir = context_dir.clone();
+        let mut found_gitignore = false;
+
+        loop {
+            let gitignore_file = dir.join(".gitignore");
+            if gitignore_file.exists() && builder.add(gitignore_file).is_none() {
+                found_gitignore = true;
+            }
+
+            if dir == *project_root {
+                break;
+            }
+
+            if let Some(parent) = dir.parent() {
+                dir = parent.to_path_buf();
+                if !dir.starts_with(project_root) {
+                    break;
+                }
+            } else {
+                break;
+            }
+        }
+
+        if found_gitignore {
+            builder.build().ok()
+        } else {
+            None
+        }
+    }
+}
+
+
+/// Legacy function maintained for backward compatibility.
+/// Gets or creates a cached gitignore matcher for the given base directory.
+/// Returns None if no gitignore files are found or if compilation fails.
+#[deprecated(note = "Use get_cached_gitignore_matcher_for_context instead for proper file-contextual gitignore matching")]
+pub fn get_cached_gitignore_matcher(base_dir: &PathBuf) -> Option<ignore::gitignore::Gitignore> {
+    get_cached_gitignore_matcher_for_context(base_dir, base_dir)
+}
+
+
+
 /// Determines if a path should be ignored based on ignore configuration and .gitignore rules.
 ///
 /// # Arguments
@@ -211,43 +328,16 @@
         }
     }
 
-    // Check .gitignore files
+    // Check .gitignore files using file-contextual matcher
     if ignore_config.use_gitignore {
-        let mut builder = GitignoreBuilder::new(base_dir);
-        let mut current_dir_for_gitignore = path.parent().unwrap_or(path).to_path_buf();
-        let mut gitignore_path_found = false;
-
-        // Traverse upwards from the path itself (or its parent) to find .gitignore files up to base_dir
-        // or the root of the filesystem if base_dir is part of a deeper git repo structure.
-        loop {
-            let gitignore_file = current_dir_for_gitignore.join(".gitignore");
-            if gitignore_file.exists() && builder.add(gitignore_file).is_some() {
-                gitignore_path_found = true;
-            }
-            if current_dir_for_gitignore == *base_dir || current_dir_for_gitignore.parent().is_none() {
-                break;
-            }
-            if !current_dir_for_gitignore.pop() {
-                break;
-            }
-        }
-
-        // If no .gitignore files were explicitly added from the path's hierarchy up to base_dir,
-        // try adding one from the base_dir itself, as AppState does.
-        if !gitignore_path_found {
-            let base_gitignore = base_dir.join(".gitignore");
-            if base_gitignore.exists() {
-                builder.add(base_gitignore);
-            }
-        }
-
-        // Add .gitignore from initial_dir (usually project root) as a fallback or primary source.
-        // This logic mirrors AppState more closely.
-        // Consider if `base_dir` for `collect_folder_descendants` should always be `app_state.current_dir`
-        // or `app_state.initial_dir` if .gitignores are typically at project root.
-        // For now, using `base_dir` as passed.
+        // Use the file's parent directory as context for gitignore matching
+        let context_dir = if path.is_file() {
+            path.parent().unwrap_or(base_dir).to_path_buf()
+        } else {
+            path.clone()
+        };
 
-        if let Ok(gitignore_matcher) = builder.build() {
+        if let Some(gitignore_matcher) = get_cached_gitignore_matcher_for_context(&context_dir, base_dir) {
             let is_dir = path.is_dir();
             if let Match::Ignore(_) = gitignore_matcher.matched_path_or_any_parents(path, is_dir) {
                 return true;
@@ -306,23 +396,18 @@
         }
     }
 
-    // GitIgnore checking (unchanged as it's already optimized by the ignore crate)
+    // GitIgnore checking using file-contextual cached matcher for improved performance
     if ignore_config.use_gitignore {
-        let mut builder = GitignoreBuilder::new(base_dir);
-        let mut dir = base_dir.clone();
-        while let Some(parent) = dir.parent() {
-            let gitignore = dir.join(".gitignore");
-            if gitignore.exists() {
-                match builder.add(gitignore) {
-                    None => (),
-                    Some(_) => break,
-                }
-            }
-            dir = parent.to_path_buf();
-        }
-        if let Ok(gitignore) = builder.build() {
+        // Use the file's parent directory as context for gitignore matching
+        let context_dir = if path.is_file() {
+            path.parent().unwrap_or(base_dir).to_path_buf()
+        } else {
+            path.clone()
+        };
+
+        if let Some(gitignore_matcher) = get_cached_gitignore_matcher_for_context(&context_dir, base_dir) {
             let is_dir = path.is_dir();
-            if let Match::Ignore(_) = gitignore.matched_path_or_any_parents(path, is_dir) {
+            if let Match::Ignore(_) = gitignore_matcher.matched_path_or_any_parents(path, is_dir) {
                 return true;
             }
         }
@@ -724,6 +809,8 @@
 /// 
 /// This function includes symlink loop detection to prevent infinite recursion.
 ///
+/// This function includes symlink loop detection to prevent infinite recursion.
+///
 /// # Arguments
 /// * `folder_path` - The path to the folder to scan.
 /// * `gitignore_base_dir` - The base directory for resolving .gitignore files (usually app_state.current_dir).
@@ -812,3 +899,728 @@
 // ✅ 1. Robust binary file detection with magic numbers and content sniffing
 // ✅ 3. Lazy sorting only when UI needs update with state tracking
 // ✅ 4. Optimized ignore checks with cached pattern compilation
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::fs::{self, File};
+    use std::io::Write;
+    use tempfile::TempDir;
+
+    /// Creates a test directory structure with nested .gitignore files
+    /// to demonstrate the contextual gitignore issue
+    fn create_test_gitignore_structure() -> TempDir {
+        let temp_dir = TempDir::new().expect("Failed to create temp directory");
+        let base_path = temp_dir.path();
+
+        // Create directory structure
+        fs::create_dir_all(base_path.join("src/components")).unwrap();
+        fs::create_dir_all(base_path.join("src/utils")).unwrap();
+        fs::create_dir_all(base_path.join("tests")).unwrap();
+
+        // Create root .gitignore
+        let mut root_gitignore = File::create(base_path.join(".gitignore")).unwrap();
+        writeln!(root_gitignore, "# Root gitignore").unwrap();
+        writeln!(root_gitignore, "*.log").unwrap();
+        writeln!(root_gitignore, "node_modules/").unwrap();
+
+        // Create src/.gitignore
+        let mut src_gitignore = File::create(base_path.join("src/.gitignore")).unwrap();
+        writeln!(src_gitignore, "# Src gitignore").unwrap();
+        writeln!(src_gitignore, "*.tmp").unwrap();
+        writeln!(src_gitignore, "debug.js").unwrap();
+
+        // Create src/components/.gitignore
+        let mut components_gitignore = File::create(base_path.join("src/components/.gitignore")).unwrap();
+        writeln!(components_gitignore, "# Components gitignore").unwrap();
+        writeln!(components_gitignore, "*.test.js").unwrap();
+        writeln!(components_gitignore, "Button.tsx").unwrap();
+
+        // Create test files
+        File::create(base_path.join("app.log")).unwrap(); // Should be ignored by root
+        File::create(base_path.join("src/debug.js")).unwrap(); // Should be ignored by src
+        File::create(base_path.join("src/utils.tmp")).unwrap(); // Should be ignored by src
+        File::create(base_path.join("src/components/Button.tsx")).unwrap(); // Should be ignored by components
+        File::create(base_path.join("src/components/Modal.test.js")).unwrap(); // Should be ignored by components
+        File::create(base_path.join("src/components/Header.tsx")).unwrap(); // Should NOT be ignored
+        File::create(base_path.join("src/main.js")).unwrap(); // Should NOT be ignored
+        File::create(base_path.join("README.md")).unwrap(); // Should NOT be ignored
+
+        temp_dir
+    }
+
+    #[test]
+    fn test_gitignore_context_issue_demonstration() {
+        // This test demonstrates the current incorrect behavior
+        let temp_dir = create_test_gitignore_structure();
+        let base_path = temp_dir.path().to_path_buf();
+
+        // Clear cache to ensure fresh test
+        clear_gitignore_cache();
+
+        let ignore_config = IgnoreConfig {
+            use_gitignore: true,
+            use_default_ignores: false,
+            include_binary_files: false,
+            extra_ignore_patterns: vec![],
+        };
+
+        // Test the problematic file: src/components/Button.tsx
+        let button_file = base_path.join("src/components/Button.tsx");
+
+        // With the current implementation, this may not be correctly ignored
+        // because get_cached_gitignore_matcher only uses base_dir as cache key
+        let is_ignored = is_path_ignored_iterative(&button_file, &base_path, &ignore_config);
+
+        // The current implementation might fail this assertion
+        // because it doesn't properly handle nested gitignore files
+        println!("Button.tsx ignored: {}", is_ignored);
+        println!("Expected: true (should be ignored by src/components/.gitignore)");
+
+        // This test will likely fail with the current implementation
+        // demonstrating the bug described in CRITICALBUG.md
+    }
+
+    #[test]
+    fn test_correct_gitignore_behavior_expectations() {
+        // This test defines what the correct behavior should be
+        let temp_dir = create_test_gitignore_structure();
+        let base_path = temp_dir.path().to_path_buf();
+
+        clear_gitignore_cache();
+
+        let ignore_config = IgnoreConfig {
+            use_gitignore: true,
+            use_default_ignores: false,
+            include_binary_files: false,
+            extra_ignore_patterns: vec![],
+        };
+
+
+        // Test files that should be ignored
+        let test_cases = vec![
+            (base_path.join("app.log"), true, "root .gitignore"),
+            (base_path.join("src/debug.js"), true, "src .gitignore"),
+            (base_path.join("src/utils.tmp"), true, "src .gitignore"),
+            (base_path.join("src/components/Button.tsx"), true, "components .gitignore"),
+            (base_path.join("src/components/Modal.test.js"), true, "components .gitignore"),
+            (base_path.join("src/components/Header.tsx"), false, "should not be ignored"),
+            (base_path.join("src/main.js"), false, "should not be ignored"),
+            (base_path.join("README.md"), false, "should not be ignored"),
+        ];
+
+        for (file_path, should_be_ignored, reason) in test_cases {
+            let is_ignored = is_path_ignored_iterative(&file_path, &base_path, &ignore_config);
+            println!("File: {}, Ignored: {}, Expected: {}, Reason: {}",
+                    file_path.display(), is_ignored, should_be_ignored, reason);
+
+            // These assertions will likely fail with the current implementation
+            if should_be_ignored {
+                assert!(is_ignored, "File {} should be ignored ({})", file_path.display(), reason);
+            } else {
+                assert!(!is_ignored, "File {} should not be ignored ({})", file_path.display(), reason);
+            }
+        }
+    }
+
+    #[test]
+    fn test_gitignore_cache_key_problem() {
+        // This test specifically demonstrates the cache key problem
+        let temp_dir = create_test_gitignore_structure();
+        let base_path = temp_dir.path().to_path_buf();
+
+        clear_gitignore_cache();
+
+        let ignore_config = IgnoreConfig {
+            use_gitignore: true,
+            use_default_ignores: false,
+            include_binary_files: false,
+            extra_ignore_patterns: vec![],
+        };
+
+        // Check if gitignore files exist
+        println!("Root .gitignore exists: {}", base_path.join(".gitignore").exists());
+        println!("Src .gitignore exists: {}", base_path.join("src/.gitignore").exists());
+        println!("Components .gitignore exists: {}", base_path.join("src/components/.gitignore").exists());
+
+        // The problem: cache is keyed only by base_dir
+        // So get_cached_gitignore_matcher(base_path) returns the same matcher
+        // for all files, regardless of their location in the directory hierarchy
+
+        let matcher1 = get_cached_gitignore_matcher_for_context(&base_path, &base_path);
+        let matcher2 = get_cached_gitignore_matcher_for_context(&base_path.join("src"), &base_path);
+        let matcher3 = get_cached_gitignore_matcher_for_context(&base_path.join("src/components"), &base_path);
+
+        // The current implementation will return the same matcher for all these calls
+        // because it only caches by base_dir, which is wrong
+
+        // For demonstration, let's see how files are handled
+        let button_file = base_path.join("src/components/Button.tsx");
+        let is_ignored = is_path_ignored_iterative(&button_file, &base_path, &ignore_config);
+
+        println!("Matcher1 (base): {:?}", matcher1.is_some());
+        println!("Matcher2 (src): {:?}", matcher2.is_some());
+        println!("Matcher3 (components): {:?}", matcher3.is_some());
+        println!("Button.tsx ignored: {}", is_ignored);
+
+        // Test the matcher manually
+        if let Some(matcher) = &matcher1 {
+            let is_dir = button_file.is_dir();
+            let match_result = matcher.matched_path_or_any_parents(&button_file, is_dir);
+            println!("Match result: {:?}", match_result);
+        }
+
+        // The issue is that all matchers are the same because they use the same cache key
+        // This test documents the problem without asserting (since we know it's broken)
+    }
+
+    #[test]
+    fn test_file_contextual_gitignore_requirements() {
+        // This test documents what the correct file-contextual behavior should be
+        let temp_dir = create_test_gitignore_structure();
+        let base_path = temp_dir.path().to_path_buf();
+
+        // For a file like src/components/Button.tsx, the gitignore check should:
+        // 1. Check src/components/.gitignore (most specific)
+        // 2. Check src/.gitignore
+        // 3. Check .gitignore (root, least specific)
+
+        // Currently, the implementation only builds one matcher from base_dir upward
+        // and uses it for all files, which is incorrect.
+
+        // The correct behavior would be to either:
+        // A) Build a file-specific matcher that considers the file's directory hierarchy
+        // B) Use a more sophisticated caching strategy that considers file context
+
+        // This test documents the requirement without implementation
+        let button_file = base_path.join("src/components/Button.tsx");
+
+        // Expected gitignore file check order for Button.tsx:
+        let expected_gitignore_files = vec![
+            base_path.join("src/components/.gitignore"),
+            base_path.join("src/.gitignore"),
+            base_path.join(".gitignore"),
+        ];
+
+        for gitignore_file in expected_gitignore_files {
+            assert!(gitignore_file.exists(), "Expected gitignore file should exist: {}", gitignore_file.display());
+        }
+
+        // The Button.tsx file should be matched by src/components/.gitignore
+        // which contains "Button.tsx" pattern
+
+        println!("Test setup complete. File-contextual gitignore checking is required.");
+        println!("File: {}", button_file.display());
+        println!("Should be ignored by: src/components/.gitignore");
+    }
+
+    /// Test deeply nested gitignore hierarchy to ensure no stack overflow
+    #[test]
+    fn test_deeply_nested_gitignore_hierarchy() {
+        let temp_dir = TempDir::new().expect("Failed to create temp directory");
+        let base_path = temp_dir.path().to_path_buf();
+
+        // Create a 15-level deep directory structure with gitignore files
+        let mut current_path = base_path.clone();
+        for level in 0..15 {
+            current_path = current_path.join(format!("level{}", level));
+            fs::create_dir_all(&current_path).unwrap();
+
+            // Create a gitignore file at each level with different patterns
+            let gitignore_path = current_path.join(".gitignore");
+            let mut gitignore_file = File::create(&gitignore_path).unwrap();
+            writeln!(gitignore_file, "# Level {} gitignore", level).unwrap();
+            writeln!(gitignore_file, "ignore_level_{}.txt", level).unwrap();
+
+            // Create a test file that should be ignored at this level
+            File::create(current_path.join(format!("ignore_level_{}.txt", level))).unwrap();
+        }
+
+        clear_gitignore_cache();
+
+        let ignore_config = IgnoreConfig {
+            use_gitignore: true,
+            use_default_ignores: false,
+            include_binary_files: false,
+            extra_ignore_patterns: vec![],
+        };
+
+        // Test file at the deepest level
+        let deepest_path = base_path.join("level0/level1/level2/level3/level4/level5/level6/level7/level8/level9/level10/level11/level12/level13/level14");
+        let test_file = deepest_path.join("ignore_level_14.txt");
+
+        // This should not cause stack overflow and should properly ignore the file
+        let is_ignored = is_path_ignored_iterative(&test_file, &base_path, &ignore_config);
+        assert!(is_ignored, "Deep nested file should be ignored by its local gitignore");
+    }
+
+    /// Test symlink edge cases that could cause infinite loops
+    #[test]
+    #[cfg(unix)] // Symlinks are primarily a Unix feature
+    fn test_symlink_edge_cases() {
+        let temp_dir = TempDir::new().expect("Failed to create temp directory");
+        let base_path = temp_dir.path().to_path_buf();
+
+        // Create test structure
+        let dir_a = base_path.join("dir_a");
+        let dir_b = base_path.join("dir_b");
+        let dir_c = base_path.join("dir_c");
+        fs::create_dir_all(&dir_a).unwrap();
+        fs::create_dir_all(&dir_b).unwrap();
+        fs::create_dir_all(&dir_c).unwrap();
+
+        // Create circular symlink: a -> b -> c -> a
+        std::os::unix::fs::symlink(&dir_b, dir_a.join("link_to_b")).unwrap();
+        std::os::unix::fs::symlink(&dir_c, dir_b.join("link_to_c")).unwrap();
+        std::os::unix::fs::symlink(&dir_a, dir_c.join("link_to_a")).unwrap();
+
+        // Create self-referencing symlink
+        std::os::unix::fs::symlink(&dir_a, dir_a.join("self_link")).unwrap();
+
+        let ignore_config = IgnoreConfig {
+            use_gitignore: false,
+            use_default_ignores: false,
+            include_binary_files: false,
+            extra_ignore_patterns: vec![],
+        };
+
+        // Test that collecting descendants doesn't cause infinite recursion
+        let mut descendants = HashSet::new();
+        let result = collect_folder_descendants(&dir_a, &base_path, &ignore_config, &mut descendants);
+
+        // Should complete without hanging or stack overflow
+        assert!(result.is_ok(), "Symlink traversal should not cause infinite recursion");
+    }
+
+    /// Test binary file detection with edge cases
+    #[test]
+    fn test_binary_file_detection_edge_cases() {
+        let temp_dir = TempDir::new().expect("Failed to create temp directory");
+        let base_path = temp_dir.path();
+
+        // Test empty file
+        let empty_file = base_path.join("empty.txt");
+        File::create(&empty_file).unwrap();
+        assert!(!is_binary_file(&empty_file), "Empty file should not be detected as binary");
+
+        // Test file with only null bytes
+        let null_file = base_path.join("null_bytes");
+        let mut null_file_handle = File::create(&null_file).unwrap();
+        null_file_handle.write_all(&[0u8; 100]).unwrap();
+        assert!(is_binary_file(&null_file), "File with null bytes should be detected as binary");
+
+        // Test file with mixed content
+        let mixed_file = base_path.join("mixed.txt");
+        let mut mixed_file_handle = File::create(&mixed_file).unwrap();
+        mixed_file_handle.write_all(b"Hello World\0\xFF\xFE").unwrap();
+        assert!(is_binary_file(&mixed_file), "File with mixed binary content should be detected as binary");
+
+        // Test pure text file
+        let text_file = base_path.join("text.txt");
+        let mut text_file_handle = File::create(&text_file).unwrap();
+        text_file_handle.write_all(b"Hello World\nThis is a text file\n").unwrap();
+        assert!(!is_binary_file(&text_file), "Pure text file should not be detected as binary");
+
+        // Test file with unicode content
+        let unicode_file = base_path.join("unicode.txt");
+        let mut unicode_file_handle = File::create(&unicode_file).unwrap();
+        unicode_file_handle.write_all("Hello 世界 🌍\n".as_bytes()).unwrap();
+        assert!(!is_binary_file(&unicode_file), "Unicode text file should not be detected as binary");
+    }
+
+    /// Test path normalization with edge cases
+    #[test]
+    fn test_path_normalization_edge_cases() {
+        // Test with backslashes to forward slashes conversion
+        assert_eq!(normalize_path("path\\to\\file"), "path/to/file");
+
+        // Test with mixed separators
+        assert_eq!(normalize_path("path\\to/file"), "path/to/file");
+
+        // Test with unicode characters
+        assert_eq!(normalize_path("пуäth\\tö\\файл"), "пуäth/tö/файл");
+
+        // Test with spaces and special characters
+        assert_eq!(normalize_path("path with spaces\\to\\file!@#"), "path with spaces/to/file!@#");
+
+        // Test empty string
+        assert_eq!(normalize_path(""), "");
+
+        // Test paths that are already normalized
+        assert_eq!(normalize_path("path/to/file"), "path/to/file");
+
+        // Test with only backslashes
+        assert_eq!(normalize_path("\\\\server\\share"), "//server/share");
+
+        // Test with current and parent directory references
+        assert_eq!(normalize_path(".\\path\\to\\file"), "./path/to/file");
+        assert_eq!(normalize_path("..\\path\\to\\file"), "../path/to/file");
+    }
+
+    /// Test complex gitignore patterns that are commonly problematic
+    #[test]
+    fn test_complex_gitignore_patterns() {
+        let temp_dir = TempDir::new().expect("Failed to create temp directory");
+        let base_path = temp_dir.path().to_path_buf();
+
+        // Create complex gitignore with various pattern types
+        let gitignore_path = base_path.join(".gitignore");
+        let mut gitignore_file = File::create(&gitignore_path).unwrap();
+        writeln!(gitignore_file, "# Complex patterns test").unwrap();
+        writeln!(gitignore_file, "*.log").unwrap();              // Simple wildcard
+        writeln!(gitignore_file, "test[0-9].txt").unwrap();      // Character class
+        writeln!(gitignore_file, "temp?.dat").unwrap();          // Single character wildcard
+        writeln!(gitignore_file, "**/build/").unwrap();          // Double asterisk
+        writeln!(gitignore_file, "!important.log").unwrap();     // Negation pattern
+        writeln!(gitignore_file, "path\\ with\\ spaces/").unwrap(); // Escaped spaces
+        writeln!(gitignore_file, "/root_only.txt").unwrap();     // Root-relative pattern
+
+        // Create test files
+        File::create(base_path.join("app.log")).unwrap();
+        File::create(base_path.join("test5.txt")).unwrap();
+        File::create(base_path.join("temp1.dat")).unwrap();
+        File::create(base_path.join("important.log")).unwrap();
+        File::create(base_path.join("root_only.txt")).unwrap();
+
+        let sub_dir = base_path.join("sub");
+        fs::create_dir_all(&sub_dir).unwrap();
+        File::create(sub_dir.join("root_only.txt")).unwrap(); // Should NOT be ignored (pattern is root-relative)
+
+        let build_dir = base_path.join("project/build");
+        fs::create_dir_all(&build_dir).unwrap();
+        File::create(build_dir.join("output.exe")).unwrap();
+
+        clear_gitignore_cache();
+
+        let ignore_config = IgnoreConfig {
+            use_gitignore: true,
+            use_default_ignores: false,
+            include_binary_files: false,
+            extra_ignore_patterns: vec![],
+        };
+
+        // Test various pattern matches
+        assert!(is_path_ignored_iterative(&base_path.join("app.log"), &base_path, &ignore_config),
+                "Simple wildcard should work");
+        assert!(is_path_ignored_iterative(&base_path.join("test5.txt"), &base_path, &ignore_config),
+                "Character class should work");
+        assert!(is_path_ignored_iterative(&base_path.join("temp1.dat"), &base_path, &ignore_config),
+                "Single char wildcard should work");
+        assert!(!is_path_ignored_iterative(&base_path.join("important.log"), &base_path, &ignore_config),
+                "Negation pattern should work");
+        assert!(is_path_ignored_iterative(&base_path.join("root_only.txt"), &base_path, &ignore_config),
+                "Root-relative pattern should work for root files");
+        assert!(!is_path_ignored_iterative(&sub_dir.join("root_only.txt"), &base_path, &ignore_config),
+                "Root-relative pattern should NOT match non-root files");
+    }
+
+    /// Test gitignore cache concurrency (basic test, full concurrency testing requires more setup)
+    #[test]
+    fn test_gitignore_cache_basic_concurrency() {
+        let temp_dir = TempDir::new().expect("Failed to create temp directory");
+        let base_path = temp_dir.path().to_path_buf();
+
+        // Create gitignore
+        let gitignore_path = base_path.join(".gitignore");
+        let mut gitignore_file = File::create(&gitignore_path).unwrap();
+        writeln!(gitignore_file, "*.log").unwrap();
+
+        clear_gitignore_cache();
+
+        // Test rapid successive calls to ensure cache consistency
+        for _ in 0..100 {
+            let matcher1 = get_cached_gitignore_matcher_for_context(&base_path, &base_path);
+            let matcher2 = get_cached_gitignore_matcher_for_context(&base_path, &base_path);
+
+            // Both should either be Some or None consistently
+            assert_eq!(matcher1.is_some(), matcher2.is_some(),
+                      "Cache should return consistent results");
+        }
+
+        // Clear cache and test again
+        clear_gitignore_cache();
+        let matcher_after_clear = get_cached_gitignore_matcher_for_context(&base_path, &base_path);
+        assert!(matcher_after_clear.is_some(), "Cache should rebuild after clear");
+    }
+
+    /// Test path ignored checks with permission edge cases
+    #[test]
+    #[cfg(unix)] // Permission testing is primarily Unix-specific
+    fn test_path_ignored_permissions() {
+        let temp_dir = TempDir::new().expect("Failed to create temp directory");
+        let base_path = temp_dir.path().to_path_buf();
+
+        // Create a directory and file
+        let sub_dir = base_path.join("restricted");
+        fs::create_dir_all(&sub_dir).unwrap();
+        let test_file = sub_dir.join("test.txt");
+        File::create(&test_file).unwrap();
+
+        let ignore_config = IgnoreConfig {
+            use_gitignore: false,
+            use_default_ignores: false,
+            include_binary_files: false,
+            extra_ignore_patterns: vec![],
+        };
+
+        // Test with normal permissions (should work)
+        assert!(!is_path_ignored_iterative(&test_file, &base_path, &ignore_config));
+
+        // Note: Actually changing permissions and testing would require more complex setup
+        // and might affect the test environment, so we test the basic case here
+    }
+
+    /// Test folder descendant collection with edge cases
+    #[test]
+    fn test_folder_descendants_edge_cases() {
+        let temp_dir = TempDir::new().expect("Failed to create temp directory");
+        let base_path = temp_dir.path().to_path_buf();
+
+        let ignore_config = IgnoreConfig {
+            use_gitignore: false,
+            use_default_ignores: false,
+            include_binary_files: false,
+            extra_ignore_patterns: vec![],
+        };
+
+        // Test empty directory
+        let empty_dir = base_path.join("empty");
+        fs::create_dir_all(&empty_dir).unwrap();
+        let mut descendants = HashSet::new();
+        let result = collect_folder_descendants(&empty_dir, &base_path, &ignore_config, &mut descendants);
+        assert!(result.is_ok());
+        assert!(descendants.is_empty(), "Empty directory should yield no descendants");
+
+        // Test nested empty directories
+        let nested_empty = base_path.join("nested/empty/dirs");
+        fs::create_dir_all(&nested_empty).unwrap();
+        descendants.clear();
+        let result = collect_folder_descendants(&base_path.join("nested"), &base_path, &ignore_config, &mut descendants);
+        assert!(result.is_ok());
+        assert!(descendants.len() >= 2, "Should find the nested directories");
+
+        // Test directory with many files
+        let many_files_dir = base_path.join("many_files");
+        fs::create_dir_all(&many_files_dir).unwrap();
+        for i in 0..100 {
+            File::create(many_files_dir.join(format!("file_{}.txt", i))).unwrap();
+        }
+        descendants.clear();
+        let result = collect_folder_descendants(&many_files_dir, &base_path, &ignore_config, &mut descendants);
+        assert!(result.is_ok());
+        assert_eq!(descendants.len(), 100, "Should find all 100 files");
+    }
+
+    /// Test the new file-contextual gitignore function directly
+    #[test]
+    fn test_get_cached_gitignore_matcher_for_context() {
+        let temp_dir = create_test_gitignore_structure();
+        let base_path = temp_dir.path().to_path_buf();
+        clear_gitignore_cache();
+
+        // Test that different contexts return different matchers
+        let root_matcher = get_cached_gitignore_matcher_for_context(&base_path, &base_path);
+        let src_matcher = get_cached_gitignore_matcher_for_context(&base_path.join("src"), &base_path);
+        let components_matcher = get_cached_gitignore_matcher_for_context(&base_path.join("src/components"), &base_path);
+
+        assert!(root_matcher.is_some());
+        assert!(src_matcher.is_some());
+        assert!(components_matcher.is_some());
+
+        // Test that Button.tsx is correctly ignored by components matcher
+        let button_file = base_path.join("src/components/Button.tsx");
+        if let Some(matcher) = components_matcher {
+            let match_result = matcher.matched_path_or_any_parents(&button_file, false);
+            assert!(match_result.is_ignore(), "Button.tsx should be ignored by components/.gitignore");
+        }
+    }
+
+    /// Test cache behavior for file-contextual gitignore
+    #[test]
+    fn test_gitignore_cache_per_directory() {
+        let temp_dir = create_test_gitignore_structure();
+        let base_path = temp_dir.path().to_path_buf();
+        clear_gitignore_cache();
+
+        // First call should populate cache
+        let _matcher1 = get_cached_gitignore_matcher_for_context(&base_path.join("src"), &base_path);
+
+        // Second call with same context should use cache
+        let _matcher2 = get_cached_gitignore_matcher_for_context(&base_path.join("src"), &base_path);
+
+        // Different context should create new cache entry
+        let _matcher3 = get_cached_gitignore_matcher_for_context(&base_path.join("src/components"), &base_path);
+
+        // Verify cache has separate entries for each context
+        // (This would require exposing cache contents or a cache size function)
+    }
+
+    /// Test gitignore inheritance hierarchy
+    #[test]
+    fn test_gitignore_inheritance_validation() {
+        let temp_dir = create_test_gitignore_structure();
+        let base_path = temp_dir.path().to_path_buf();
+        clear_gitignore_cache();
+
+        let ignore_config = IgnoreConfig {
+            use_gitignore: true,
+            use_default_ignores: false,
+            include_binary_files: false,
+            extra_ignore_patterns: vec![],
+        };
+
+        // File should be ignored by most specific .gitignore that matches
+        let test_cases = vec![
+            // Files ignored by root .gitignore
+            (base_path.join("app.log"), true, "*.log pattern from root"),
+            (base_path.join("src/nested.log"), true, "*.log pattern inherited from root"),
+
+            // Files ignored by src .gitignore
+            (base_path.join("src/debug.js"), true, "debug.js pattern from src"),
+            (base_path.join("src/test.tmp"), true, "*.tmp pattern from src"),
+
+            // Files ignored by components .gitignore
+            (base_path.join("src/components/Button.tsx"), true, "Button.tsx pattern from components"),
+            (base_path.join("src/components/test.test.js"), true, "*.test.js pattern from components"),
+
+            // Files that should NOT be ignored
+            (base_path.join("src/components/Header.tsx"), false, "not matching any ignore pattern"),
+            (base_path.join("src/main.js"), false, "not matching any ignore pattern"),
+            (base_path.join("README.md"), false, "not matching any ignore pattern"),
+        ];
+
+        for (file_path, expected_ignored, reason) in test_cases {
+            let is_ignored = is_path_ignored_iterative(&file_path, &base_path, &ignore_config);
+            assert_eq!(is_ignored, expected_ignored,
+                      "File {} - Expected: {}, Got: {}, Reason: {}",
+                      file_path.display(), expected_ignored, is_ignored, reason);
+        }
+    }
+
+    /// Test collect_folder_descendants_with_visited directly for comprehensive symlink handling
+    #[test]
+    #[cfg(unix)]
+    fn test_symlink_loop_detection_comprehensive() {
+        let temp_dir = TempDir::new().expect("Failed to create temp directory");
+        let base_path = temp_dir.path().to_path_buf();
+
+        // Create complex symlink structure
+        let dirs = ["a", "b", "c", "d"];
+        for dir in &dirs {
+            fs::create_dir_all(base_path.join(dir)).unwrap();
+        }
+
+        // Create various symlink patterns
+        // Circular: a -> b -> c -> a
+        std::os::unix::fs::symlink(&base_path.join("b"), base_path.join("a/link_b")).unwrap();
+        std::os::unix::fs::symlink(&base_path.join("c"), base_path.join("b/link_c")).unwrap();
+        std::os::unix::fs::symlink(&base_path.join("a"), base_path.join("c/link_a")).unwrap();
+
+        // Self-reference: d -> d
+        std::os::unix::fs::symlink(&base_path.join("d"), base_path.join("d/self_link")).unwrap();
+
+        // Double self-reference: d -> d/self -> d
+        std::os::unix::fs::symlink(&base_path.join("d"), base_path.join("d/double_self")).unwrap();
+
+        let ignore_config = IgnoreConfig::default();
+        let mut descendants = HashSet::new();
+        let mut visited = HashSet::new();
+
+        // Test that symlink detection prevents infinite recursion
+        let result = collect_folder_descendants_with_visited(
+            &base_path.join("a"),
+            &base_path,
+            &ignore_config,
+            &mut descendants,
+            &mut visited
+        );
+
+        assert!(result.is_ok(), "Should handle symlink loops without error");
+        assert!(!descendants.is_empty(), "Should collect some files despite loops");
+    }
+
+    /// Test performance with many symlinks
+    #[test]
+    #[cfg(unix)]
+    fn test_symlink_performance_stress() {
+        let temp_dir = TempDir::new().expect("Failed to create temp directory");
+        let base_path = temp_dir.path().to_path_buf();
+
+        // Create 50 directories each with self-referencing symlinks
+        for i in 0..50 {
+            let dir = base_path.join(format!("dir_{}", i));
+            fs::create_dir_all(&dir).unwrap();
+            std::os::unix::fs::symlink(&dir, dir.join("self_link")).unwrap();
+
+            // Add some regular files too
+            File::create(dir.join("file.txt")).unwrap();
+        }
+
+        let ignore_config = IgnoreConfig::default();
+        let mut descendants = HashSet::new();
+
+        let start = std::time::Instant::now();
+        let result = collect_folder_descendants(&base_path, &base_path, &ignore_config, &mut descendants);
+        let duration = start.elapsed();
+
+        assert!(result.is_ok(), "Should handle many symlinks efficiently");
+        assert!(duration.as_secs() < 5, "Should complete within 5 seconds");
+        assert_eq!(descendants.len(), 50, "Should find exactly 50 regular files");
+    }
+
+    /// Test symlink pointing to non-existent target
+    #[test]
+    #[cfg(unix)]
+    fn test_broken_symlink_handling() {
+        let temp_dir = TempDir::new().expect("Failed to create temp directory");
+        let base_path = temp_dir.path().to_path_buf();
+
+        let target_dir = base_path.join("target");
+        let broken_link = base_path.join("broken_link");
+
+        // Create symlink first, then remove target
+        fs::create_dir_all(&target_dir).unwrap();
+        std::os::unix::fs::symlink(&target_dir, &broken_link).unwrap();
+        fs::remove_dir(&target_dir).unwrap();
+
+        let ignore_config = IgnoreConfig::default();
+        let mut descendants = HashSet::new();
+
+        let result = collect_folder_descendants(&base_path, &base_path, &ignore_config, &mut descendants);
+
+        // Should handle broken symlinks gracefully
+        assert!(result.is_ok(), "Should handle broken symlinks without crashing");
+    }
+
+    /// Test corrected regex pattern
+    #[test]
+    fn test_regex_pattern_fix() {
+        // Test that the corrected regex pattern "^$" works as expected
+        let empty_regex = regex::Regex::new("^$").unwrap();
+
+        assert!(empty_regex.is_match(""), "Should match empty string");
+        assert!(!empty_regex.is_match("non-empty"), "Should not match non-empty string");
+
+        // Verify the old malformed pattern would have failed
+        let malformed_result = regex::Regex::new("\"$.^\"");
+        assert!(malformed_result.is_err(), "Malformed pattern should fail to compile");
+    }
+
+    /// Test gitignore cache clearing behavior
+    #[test]
+    fn test_gitignore_cache_management() {
+        let temp_dir = create_test_gitignore_structure();
+        let base_path = temp_dir.path().to_path_buf();
+
+        // Populate cache
+        let _matcher1 = get_cached_gitignore_matcher_for_context(&base_path, &base_path);
+        let _matcher2 = get_cached_gitignore_matcher_for_context(&base_path.join("src"), &base_path);
+
+        // Clear cache
+        clear_gitignore_cache();
+
+        // Cache should be cleared - new calls should rebuild
+        let matcher3 = get_cached_gitignore_matcher_for_context(&base_path, &base_path);
+        assert!(matcher3.is_some(), "Should rebuild matcher after cache clear");
+    }
+}
removed in remote
  base   100644 9b8c38c3c573532cdc8378c4c4ead84fd9d40be6 src/fs/mod.rs.notes
  our    100644 9b8c38c3c573532cdc8378c4c4ead84fd9d40be6 src/fs/mod.rs.notes
@@ -1,66 +0,0 @@
-# Notes on Refactoring: Identical if/else Blocks in src/fs/mod.rs
-
-## Context
-This notes file documents the changes made to src/fs/mod.rs to resolve Clippy warnings about identical code in if/else branches. The changes were made on [DATE] as part of a linting/code quality improvement pass.
-
-## Summary of Changes
-Two locations in src/fs/mod.rs contained if/else blocks where both branches performed the same action. This is redundant and can obscure the code's intent. The affected locations were:
-
-1. **add_items_recursively_inner (around line 158):**
-   - The match on `fs::read_dir(root)` had an `Err(e)` arm with:
-     ```rust
-     if e.kind() == io::ErrorKind::PermissionDenied {
-         return Ok(());
-     } else {
-         return Ok(());
-     }
-     ```
-   - **Change:** This was replaced with a single `return Ok(());` since both branches did the same thing.
-
-2. **collect_all_subdirs (around line 283):**
-   - The match on `fs::read_dir(&current)` had an `Err(e)` arm with:
-     ```rust
-     if e.kind() == io::ErrorKind::PermissionDenied {
-         continue;
-     } else {
-         continue;
-     }
-     ```
-   - **Change:** This was replaced with a single `continue;` since both branches did the same thing.
-
-## Rationale
-- **Clarity:** Removing redundant branches makes the code easier to read and maintain.
-- **Correctness:** The logic is unchanged, as both branches previously performed the same action.
-- **Linting:** This resolves the Clippy warning about identical if/else blocks, improving code quality.
-
-## How to Undo
-If you wish to revert these changes, simply restore the original if/else structure at the affected locations. For example:
-
-**For add_items_recursively_inner:**
-```rust
-Err(e) => {
-    if e.kind() == io::ErrorKind::PermissionDenied {
-        return Ok(());
-    } else {
-        return Ok(());
-    }
-}
-```
-
-**For collect_all_subdirs:**
-```rust
-Err(e) => {
-    if e.kind() == io::ErrorKind::PermissionDenied {
-        continue;
-    } else {
-        continue;
-    }
-}
-```
-
-## References
-- See inline comments in src/fs/mod.rs for references to this notes file and the specific lines changed.
-- Clippy lint: https://rust-lang.github.io/rust-clippy/master/index.html#if_same_then_else
-
----
-End of notes.
\ No newline at end of file
changed in both
  base   100644 556a25a45401c23fd82fcc462d1c650ce3df2c91 src/tui/app.rs
  our    100644 8aaa67c469eb46cb67aa266bdef122419a9d9d41 src/tui/app.rs
  their  100644 da73fc7fe604c458ff99c6165267c6ea48c4023d src/tui/app.rs
@@ -308,7 +308,11 @@
 
                     if should_cancel_operations && self.state.is_counting {
                         // Send cancel event to prevent race conditions
+<<<<<<< .our
                         if let Err(_) = self.event_tx.send(AppEvent::CancelSelectionOperations) {
+=======
+                        if self.event_tx.send(AppEvent::CancelSelectionOperations).is_err() {
+>>>>>>> .their
                             // Channel closed - handle gracefully
                             self.state.is_counting = false;
                             self.state.counting_path = None;
@@ -553,4 +557,141 @@
     },
     /// Event to cancel ongoing selection operations (prevent race conditions)
     CancelSelectionOperations,
+<<<<<<< .our
+=======
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::models::{AppConfig, IgnoreConfig};
+    use tempfile::TempDir;
+    use std::sync::mpsc;
+    use std::time::Duration;
+
+    /// Test AppEvent enum completeness
+    #[test]
+    fn test_app_event_variants() {
+        // Ensure all expected event types exist
+        let _selection_complete = AppEvent::SelectionCountComplete(
+            PathBuf::from("/test"),
+            42,
+            None,
+            HashSet::new()
+        );
+
+        let _select_all_complete = AppEvent::SelectAllScanComplete {
+            total_potential_item_count: 100,
+            final_selection_set: HashSet::new(),
+            initial_optimistic_set: HashSet::new(),
+        };
+
+        let _cancel_operations = AppEvent::CancelSelectionOperations;
+
+        // Test that events can be sent through channel
+        let (tx, rx) = mpsc::channel::<AppEvent>();
+        tx.send(AppEvent::CancelSelectionOperations).unwrap();
+
+        match rx.recv_timeout(Duration::from_millis(100)) {
+            Ok(AppEvent::CancelSelectionOperations) => {}, // Expected
+            _ => panic!("Should receive CancelSelectionOperations event"),
+        }
+    }
+
+    /// Test App creation with event channel
+    #[test]
+    fn test_app_creation_with_event_channel() {
+        let temp_dir = TempDir::new().expect("Failed to create temp directory");
+        let config = AppConfig::default();
+        let ignore_config = IgnoreConfig::default();
+
+        let app_result = App::new(
+            config,
+            temp_dir.path().to_path_buf(),
+            ignore_config,
+        );
+
+        assert!(app_result.is_ok(), "App creation should succeed");
+
+        let app = app_result.unwrap();
+        // Verify event channel was created
+        // Note: We can't easily test the channel directly due to private fields,
+        // but we can verify the app was created successfully
+        assert_eq!(app.state.current_dir, temp_dir.path().to_path_buf());
+    }
+
+    /// Test event handling doesn't block main thread
+    #[test]
+    fn test_event_channel_non_blocking() {
+        let (tx, rx) = mpsc::channel::<AppEvent>();
+
+        // Send multiple events rapidly
+        for i in 0..1000 {
+            let event = AppEvent::SelectionCountComplete(
+                PathBuf::from(format!("/test/{}", i)),
+                i,
+                None,
+                HashSet::new(),
+            );
+
+            // This should not block
+            let send_result = tx.send(event);
+            if send_result.is_err() {
+                // Channel receiver might be dropped, but shouldn't panic
+                break;
+            }
+        }
+
+        // Send cancellation event
+        tx.send(AppEvent::CancelSelectionOperations).unwrap();
+
+        // Verify events can be received
+        let mut received_cancel = false;
+        while let Ok(event) = rx.try_recv() {
+            if matches!(event, AppEvent::CancelSelectionOperations) {
+                received_cancel = true;
+                break;
+            }
+        }
+
+        assert!(received_cancel, "Should receive cancellation event");
+    }
+
+    /// Mock test for race condition prevention
+    #[test]
+    fn test_selection_operation_cancellation_pattern() {
+        let (tx, rx) = mpsc::channel::<AppEvent>();
+
+        // Simulate scenario where user input triggers cancellation
+        // while background selection operation is running
+
+        // 1. Start background operation (simulated)
+        tx.send(AppEvent::SelectionCountComplete(
+            PathBuf::from("/test"),
+            100,
+            None,
+            HashSet::new(),
+        )).unwrap();
+
+        // 2. User input causes cancellation
+        tx.send(AppEvent::CancelSelectionOperations).unwrap();
+
+        // 3. Another background operation result arrives late
+        tx.send(AppEvent::SelectAllScanComplete {
+            total_potential_item_count: 200,
+            final_selection_set: HashSet::new(),
+            initial_optimistic_set: HashSet::new(),
+        }).unwrap();
+
+        // Event handler should process cancellation and ignore late results
+        let events: Vec<_> = rx.try_iter().collect();
+        assert_eq!(events.len(), 3);
+
+        // Verify cancellation event is present
+        let has_cancellation = events.iter().any(|e| {
+            matches!(e, AppEvent::CancelSelectionOperations)
+        });
+        assert!(has_cancellation, "Should contain cancellation event");
+    }
+>>>>>>> .their
 }
changed in both
  base   100644 c301460874b1b7962595437d8e06454f2d70faea src/tui/handlers/file_ops.rs
  our    100644 1a41c0c84d76eba8e12e7d15464caf637ddc701c src/tui/handlers/file_ops.rs
  their  100644 6f7d73d9ccd335ae12e0b1da7b2f0811d131072b src/tui/handlers/file_ops.rs
@@ -244,11 +244,15 @@
                             parent.display()
                         ));
                         app_state.current_dir = parent.to_path_buf();
+                        // Clear gitignore cache when changing directories
+                        crate::fs::clear_gitignore_cache();
                     }
                 } else {
                     log_event(&format!("handle_enter: entering dir {}", path.display()));
                     // Memory optimization: Use clone_from for more efficient PathBuf copying
                     app_state.current_dir.clone_from(path);
+                    // Clear gitignore cache when changing directories
+                    crate::fs::clear_gitignore_cache();
                 }
 
                 app_state.is_searching = false;
changed in both
  base   100644 ae5c2fb24bafcce34bf6d631a20c1dab0ab275b3 src/tui/handlers/keyboard.rs
  our    100644 f9b7f67ebf9ee87c43c25c4c1acba61585cf5951 src/tui/handlers/keyboard.rs
  their  100644 14911e3af9f709383d3153507886beb46b41a260 src/tui/handlers/keyboard.rs
@@ -207,6 +207,8 @@
                                 if let Some(parent) = app_state.current_dir.parent() {
                                     if parent != app_state.current_dir { // Ensure we don't go "up" from root to root
                                         app_state.current_dir = parent.to_path_buf();
+                                        // Clear gitignore cache when changing directories
+                                        crate::fs::clear_gitignore_cache();
                                         FileOpsHandler::load_items(app_state)?;
                                         selection_state.list_state.select(Some(0));
                                     }
changed in both
  base   100644 a883ecf73a191101e5ffe7f608eda59bab8826fa src/tui/state/app_state.rs
  our    100644 6825ec0b0ce739d9059a335d758de6b07654d8cf src/tui/state/app_state.rs
  their  100644 8008285936563d724c050d9656ff5c9215337366 src/tui/state/app_state.rs
@@ -22,7 +22,7 @@
     app_config::Node, constants, AppConfig, CopyStats, IgnoreConfig, OutputFormat,
 };
 use crate::tui::components::modal::Modal;
-use ignore::{gitignore::GitignoreBuilder, Match};
+use ignore::Match;
 use ratatui::widgets::ListState;
 use std::collections::{BTreeMap, HashSet};
 use std::io;
@@ -382,18 +382,16 @@
             }
         }
         if self.ignore_config.use_gitignore {
-            let mut builder = GitignoreBuilder::new(&self.current_dir);
-            let mut dir = self.current_dir.clone();
-            while let Some(parent) = dir.parent() {
-                let gitignore = dir.join(".gitignore");
-                if gitignore.exists() && builder.add(gitignore).is_some() {
-                    break;
-                }
-                dir = parent.to_path_buf();
-            }
-            if let Ok(gitignore) = builder.build() {
+            // Use the file's parent directory as context for gitignore matching
+            let context_dir = if path.is_file() {
+                path.parent().unwrap_or(&self.current_dir).to_path_buf()
+            } else {
+                path.to_path_buf()
+            };
+            
+            if let Some(gitignore_matcher) = crate::fs::get_cached_gitignore_matcher_for_context(&context_dir, &self.current_dir) {
                 let is_dir = path.is_dir();
-                if let Match::Ignore(_) = gitignore.matched_path_or_any_parents(path, is_dir) {
+                if let Match::Ignore(_) = gitignore_matcher.matched_path_or_any_parents(path, is_dir) {
                     return true;
                 }
             }
changed in both
  base   100644 5110cc629e2646d61a8887a71d0d3631ceae892a src/tui/state/search.rs
  our    100644 0d75087fce8b46649727b856c66b4321e88263e4 src/tui/state/search.rs
  their  100644 f15d922f0af69c71f68278b85e66c281a89c2c76 src/tui/state/search.rs
@@ -6,17 +6,19 @@
 //!
 //! ## Organization
 //! - [`SearchState`]: State struct for search queries and selection.
+<<<<<<< .our
 //! - [`perform_search`]: Utility for performing search on item lists.
+=======
+>>>>>>> .their
 //!
 //! ## Usage
 //! Use [`SearchState`] to manage search queries and selection state during interactive search.
 //!
 //! # Examples
 //! ```rust
-//! use crate::tui::state::search::{SearchState, perform_search};
+//! use crate::tui::state::search::SearchState;
 //! let mut state = SearchState::new();
 //! state.search_query = "main".to_string();
-//! let indices = perform_search(&items, &state.search_query);
 //! ```
 
 use glob::Pattern;
@@ -36,7 +38,22 @@
 /// let mut state = SearchState::new();
 /// state.search_query = "foo".to_string();
 /// ```
+<<<<<<< .our
 #[doc(alias = "search-state")]
+=======
+///
+/// # Fields
+/// * `search_query` - The current search query string.
+/// * `is_searching` - Whether search mode is active.
+/// * `selected_items` - Set of selected items in the search results.
+///
+/// # Examples
+/// ```rust
+/// use crate::tui::state::search::SearchState;
+/// let mut state = SearchState::new();
+/// state.search_query = "foo".to_string();
+/// ```
+>>>>>>> .their
 #[derive(Default)]
 pub struct SearchState {
     pub search_query: String,
