Skip to content

Conversation

DevMassive
Copy link
Contributor

@DevMassive DevMassive commented Aug 6, 2025

TLDR

This commit enables fuzzy file path completion for the @ command using fzf-for-js.

Dive Deeper

The @ command now supports fuzzy matching for file paths using the fzf-for-js library. When the input is a literal string (not a glob), the FileSearch class uses fzf-for-js to perform fuzzy filtering over the file list.

Note: The implementation is synchronous and does not reuse the FZF instance across calls, for simplicity. See async finder and usage notes for possible future enhancements.

Reviewer Test Plan

  1. Verify fuzzy matching
    Run the CLI and use the @ command with a literal string (e.g., @h/uat). Confirm that fuzzy matching is working as expected.

  2. Verify glob patterns
    Use the @ command with glob patterns (e.g., @src/**/*.ts) and confirm that fuzzy matching is not applied.

Testing Matrix

🍏 🪟 🐧
npm run
npx
Docker
Podman - -
Seatbelt - -

Linked issues / bugs

Resolves #1870

This commit enhances the file path completion for the `@` command by integrating the `fzf` command-line tool for faster and more efficient filtering.

Changes introduced:
*   **Leveraged `fzf` for file filtering**: When `fzf` is installed and the search pattern is a literal string (not a glob), the `FileSearch` class now pipes the list of all files to `fzf` for fuzzy matching,
significantly improving performance.
*   **Added `fzfFilter.ts` module**: A new module was created to encapsulate the logic for `fzf` availability detection and filtering.
*   **Included corresponding tests**: Tests were added to verify the `fzf` integration.
@DevMassive DevMassive requested a review from a team as a code owner August 6, 2025 03:51
Copy link
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.

Summary of Changes

Hello @DevMassive, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

I've implemented a significant enhancement to the @ command's file path completion by integrating the fzf command-line tool. This change aims to drastically improve the performance and user experience for file path suggestions, especially for large codebases, by leveraging fzf's highly optimized fuzzy matching capabilities. The system is designed to be robust, gracefully falling back to the existing completion logic if fzf is not installed or if the search pattern involves glob characters.

Highlights

  • fzf Integration: The @ command's file path completion now integrates fzf for faster and more efficient fuzzy matching. This integration is conditional, activating only when fzf is installed and the search pattern is a literal string (not a glob).
  • Conditional Logic and Fallback: The system intelligently checks for fzf availability upon initialization and dynamically decides whether to use fzf or the existing filtering mechanism. It falls back to the traditional filter for glob patterns or if fzf is not found on the system.
  • New fzfFilter Module: A new module, fzfFilter.ts, has been introduced to encapsulate all fzf-related logic, including checking for its availability (checkCanUseFzf) and performing the fuzzy filtering (filterByFzf) by piping data to fzf's stdin.
  • Enhanced Test Coverage: New test cases have been added to fileSearch.test.ts to specifically verify the correct behavior of the fzf integration, ensuring it's used when available and bypassed when not or when glob patterns are present.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments or fill out our survey to provide feedback.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
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 introduces a performance enhancement by integrating fzf for file path filtering. The implementation is well-structured, with the fzf logic correctly encapsulated in a new fzfFilter.ts module and corresponding tests added.

My main concern, which is critical, is a command injection vulnerability in fzfFilter.ts where user input is passed directly to child_process.exec. I've provided a detailed comment and a suggested fix to address this security risk by using execFile and improving error handling.

Once the security issue is resolved, this will be a solid contribution.

Comment on lines 35 to 42
const stdout = await new Promise<string>((resolve) => {
const { stdin } = exec(`fzf --filter "${pattern}"`, {}, (_err, stdout) => {
resolve(stdout);
});

stdin?.write(allPaths.join('\n'));
stdin?.end();
});
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

This implementation has a critical security vulnerability and lacks proper error handling.

  1. Command Injection: The pattern is directly concatenated into the command string for exec. A malicious pattern (e.g., foo"; rm -rf /) could lead to arbitrary command execution. You should use execFile or spawn to pass arguments safely, which avoids shell interpretation.

  2. Error Handling: The _err object from the exec callback is ignored. If fzf fails for any reason (e.g., exits with an error code), the error is suppressed, and the function will likely return an empty array, hiding the underlying issue. The promise should be rejected on error. Note that fzf's exit code 1 means "no matches", which should be handled as a non-error case.

Please also update the import at the top of the file from exec to execFile.

const stdout = await new Promise<string>((resolve, reject) => {
  // Use execFile to prevent command injection vulnerabilities.
  const { stdin } = require('child_process').execFile('fzf', ['--filter', pattern], (err, stdout) => {
    // fzf exits with code 1 if there are no matches. This is not an error for us.
    if (err && err.code !== 1) {
      return reject(err);
    }
    resolve(stdout);
  });

  // The stdin stream should be handled to pass the file paths to fzf.
  stdin?.write(allPaths.join('\n'));
  stdin?.end();
});

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in edde1ff

Replaced `child_process.exec` with `child_process.execFile` for executing fzf commands.
This change enhances security by preventing shell injection and improves error handling by correctly interpreting fzf's exit code 1 (no matches) as a non-error condition.

google-gemini#5650 (comment)
@DevMassive
Copy link
Contributor Author

/gemini review

Copy link
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 introduces fzf integration for faster file path completion. The changes are generally good, but I've identified a critical robustness issue in the new fzfFilter.ts module related to handling the fzf child process, which could lead to unhandled promise rejections and crash the application. Additionally, there's a correctness issue in fileSearch.ts where glob pattern detection is too simplistic, potentially leading to incorrect filtering behavior. I've provided suggestions to address both of these issues.

Comment on lines 35 to 48
const stdout = await new Promise<string>((resolve, reject) => {
const { stdin } = execFile('fzf', ['--filter', pattern], (err, stdout) => {
// fzf exits with code 1 if there are no matches. This is not an error for us.
if (err && err.code !== 1) {
return reject(err);
}
resolve(stdout);
});

stdin?.write(allPaths.join('\n'));
stdin?.end();
});

return stdout.split('\n').filter(Boolean);
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

The current implementation of filterByFzf has two potential issues that could affect robustness and performance:

  1. Unhandled stdin errors: The stdin.write() call can throw an error (e.g., EPIPE) if the fzf process closes its stdin stream unexpectedly (for example, if it finds a match and exits early). This would be an unhandled exception within the Promise constructor, leading to an unhandled promise rejection that could crash the Node.js process.
  2. High memory usage: allPaths.join('\n') creates a single large string containing all file paths. For projects with a very large number of files, this can consume a significant amount of memory.

A more robust approach is to stream the paths to fzf's stdin and handle potential errors on the stream. This can be achieved by refactoring the function to return the promise directly instead of using await inside.

  return new Promise<string[]>((resolve, reject) => {
    const child = execFile('fzf', ['--filter', pattern], (err, stdout) => {
      // fzf exits with code 1 if there are no matches. This is not an error for us.
      if (err && err.code !== 1) {
        return reject(err);
      }
      resolve(stdout.split('\n').filter(Boolean));
    });

    if (!child.stdin) {
      return reject(new Error('Could not get stdin for fzf process.'));
    }

    child.stdin.on('error', (err) => {
      // Ignore EPIPE errors, which can happen if fzf exits before we're done writing.
    });

    for (const p of allPaths) {
      child.stdin.write(p + '\n');
    }
    child.stdin.end();
  });

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seeking human feedback.

Comment on lines 143 to 145
filteredCandidates = await (pattern.includes('*') || !this.canUseFzf
? filter(candidates, pattern, options.signal)
: filterByFzf(this.allFiles, pattern));
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The check pattern.includes('*') to determine if a pattern is a glob is not fully reliable. It will fail to detect other glob characters like ?, [...], (...), etc. This can lead to incorrect behavior where a glob pattern is passed to fzf, which does not interpret glob syntax and will treat it as a literal string for fuzzy matching.

For a more robust way to detect if a pattern is a glob, you can use picomatch.scan().

Suggested change
filteredCandidates = await (pattern.includes('*') || !this.canUseFzf
? filter(candidates, pattern, options.signal)
: filterByFzf(this.allFiles, pattern));
const isGlob = picomatch.scan(pattern).isGlob;
filteredCandidates = await (isGlob || !this.canUseFzf
? filter(candidates, pattern, options.signal)
: filterByFzf(this.allFiles, pattern));

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seeking human feedback.

@jacob314
Copy link
Collaborator

jacob314 commented Aug 6, 2025

We just landed significantly faster @ command handling a couple hours ago. Please take a look to see how the performance is now.

@DevMassive
Copy link
Contributor Author

DevMassive commented Aug 6, 2025

@jacob314
Thank you for the update — the improvements in #5136 are impressive!

This PR focuses on a different aspect: enabling fuzzy file search (e.g. typing @huat suggests packages/cli/src/ui/hooks/useAtCompletion.ts) using fzf.
It’s aimed at improving discoverability of files rather than just speeding things up.

@jacob314
Copy link
Collaborator

jacob314 commented Aug 6, 2025

I would prefer we support fuzzy matching without requiring running a shell command. I would suggest we integrate fuzzy matching using a node library if possible building on the changes that were just landed.

@jacob314 Thank you for the update — the improvements in #5136 are impressive!

This PR focuses on a different aspect: enabling fuzzy file search (e.g. typing @huat suggests packages/cli/src/ui/hooks/useAtCompletion.ts) using fzf. It’s aimed at improving discoverability of files rather than just speeding things up.

What do you think of using the fzf-for-js instead so we don't have to add a binary dependency which will make this trickier to get right with sandboxing and the need to support windows? Would the performance of that approach be adequate?
https://github.com/ajitid/fzf-for-js?tab=readme-ov-file

Replaces the reliance on the external `fzf` binary with the `fzf` npm package for file filtering. This change simplifies the implementation by removing `child_process` calls and improves cross-platform compatibility by eliminating the need for an external binary.

google-gemini#5650 (comment)
'src/index.js',
'src/components/Button.tsx',
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The order of results has slightly changed due to fuzzy sorting by fzf-for-js.

@DevMassive
Copy link
Contributor Author

@jacob314 Replaced the CLI tool fzf with fzf-for-js.

Note:

  • No async optimization was done. See: async finder
  • The FZF instance could be reused if the list doesn’t change, but this PR keeps things simple and doesn’t implement that. See: usage notes

Copy link
Collaborator

@jacob314 jacob314 left a comment

Choose a reason for hiding this comment

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

This is a big improvement! Thanks so much for landing.
I'm much happier for the queries I tested.
After:
Image
Before:
Image

@jacob314 jacob314 enabled auto-merge August 6, 2025 23:31
@jacob314 jacob314 added this pull request to the merge queue Aug 6, 2025
Merged via the queue into google-gemini:main with commit 9ac3e8b Aug 6, 2025
14 checks passed
agarwalravikant pushed a commit to agarwalravikant/gemini-cli that referenced this pull request Aug 7, 2025
thacio added a commit to thacio/auditaria that referenced this pull request Aug 7, 2025
acoliver pushed a commit to acoliver/llxprt-code that referenced this pull request Aug 7, 2025
agarwalravikant pushed a commit to agarwalravikant/gemini-cli that referenced this pull request Aug 8, 2025
skeshive pushed a commit that referenced this pull request Aug 8, 2025
skeshive pushed a commit that referenced this pull request Aug 8, 2025
skeshive pushed a commit that referenced this pull request Aug 8, 2025
skeshive pushed a commit that referenced this pull request Aug 8, 2025
JeongJaeSoon pushed a commit to JeongJaeSoon/gemini-cli that referenced this pull request Aug 21, 2025
Gosling-dude pushed a commit to Gosling-dude/gemini-cli that referenced this pull request Aug 23, 2025
involvex pushed a commit to involvex/gemini-cli that referenced this pull request Sep 11, 2025
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.

Improve fuzzy search for the @ filename select
2 participants