Repository navigation
fix(cli): skip eager recursive file reading for @<directory> references - #29617
jvargassanchez-dot wants to merge 3 commits into
Conversation
Resolve @<directory> references to their relative workspace path without converting them into recursive '**' globs for client-side ReadManyFilesTool execution. This prevents inlining the entire contents of all files in a directory into the initial prompt and allows the model to evaluate the user request and select the appropriate tool (e.g., list_directory or read_file), reducing unnecessary token usage. - Add isDirectory flag to ResolvedFile in atCommandProcessor.ts. - Keep relative directory pathSpec instead of expanding directories to '**' in resolveFilePaths. - Filter out directory entries in readLocalFiles before invoking ReadManyFilesTool. - Update and add unit tests in atCommandProcessor.test.ts for directory and mixed @<file> + @<directory> references.
|
📊 PR Size: size/M
|
Summary of ChangesHello, 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! This pull request optimizes how the CLI processes directory references provided in user prompts. Previously, referencing a directory would cause the system to recursively expand and inline the contents of all files within that directory before sending the prompt to the model. The changes introduce a mechanism to identify directory references and exclude them from the eager file-reading process, allowing the model to interact with the directory structure more efficiently and reducing unnecessary prompt overhead. Highlights
Using Gemini Code AssistThe 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 by creating a comment using either
Customization To customize the 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 Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request prevents eagerly reading all files when resolving directory paths (such as @<directory>) by introducing an optional isDirectory property to the ResolvedFile interface and filtering out directories before reading local files. Feedback is provided to explicitly handle the optional isDirectory property using the nullish coalescing operator (??) instead of relying on implicit falsiness, ensuring strict adherence to coding standards for optional properties.
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request prevents the eager reading of all files when resolving directory paths (e.g., @<directory>) in the @ command processor. It introduces an isDirectory flag to the ResolvedFile interface, filters out directories from the list of files to be read locally, and updates the corresponding unit tests to verify this behavior. There are no review comments to address, and I have no feedback to provide.
kschaab
left a comment
There was a problem hiding this comment.
Summary
Thanks for putting this together! Skipping eager recursive ReadManyFilesTool reads on @<directory> references makes sense for avoiding context window blowups on large directories when the user only wants to list or search files.
Key Observations
- Edge Case with Workspace Root Directory (
relativePath === ""): When an absolute path to the workspace root directory itself is referenced (e.g.,@/path/to/workspace),resolveAtCommandPathreturnsrelativePath: ""(frompath.relative(dir, dir)). Previouslypath.join('', '**')turned this into'**', which was truthy. WithpathSpec: relativePath,pathSpecbecomes"", causingconstructInitialQuery(const resolved = replacementMap.get(part); content = resolved ? ... : part.content) to treatresolvedas falsy and leave the raw path un-normalized. - Dead Glob Check in
readLocalFiles: Therf.pathSpec.endsWith('**')check inreadLocalFileswas only needed when directories were rewritten topath.join(relativePath, '**')and can now be simplified. - Documentation & ACP Consistency:
- User-facing docs in
docs/reference/commands.mdand the JSDoc onhandleAtCommandstill state that@<directory>reads all files in the directory and subdirectories viaread_many_files. packages/cli/src/acp/acpSession.tshas a parallel@<directory>handler that still expands directories to${pathName}/**forReadManyFilesTool—should ACP mode be updated to match?- Please also link the relevant GitHub issue under
## Related Issuesin the PR description.
- User-facing docs in
| if (stats.isDirectory()) { | ||
| const pathSpec = path.join(relativePath, '**'); | ||
| resolvedFiles.push({ | ||
| part, | ||
| pathSpec, | ||
| pathSpec: relativePath, | ||
| displayLabel: path.isAbsolute(pathName) ? relativePath : pathName, | ||
| absolutePath, | ||
| isDirectory: true, | ||
| }); | ||
| onDebugMessage( | ||
| `Path ${pathName} resolved to directory, using glob: ${pathSpec}`, | ||
| `Path ${pathName} resolved to directory: ${absolutePath}, using relative path: ${relativePath}`, | ||
| ); |
There was a problem hiding this comment.
Edge Case & Duplication ([TEST-002], [DRY-001]): Handle empty relativePath for workspace root and consolidate if (stats.isDirectory()) / else branches
Issue:
- When
pathNameis an absolute path to the workspace root directory itself (pathName === dir),resolveAtCommandPathcomputespath.relative(dir, pathName)which is""(empty string). Previously,path.join('', '**')produced'**'(truthy). SettingpathSpec: relativePathdirectly leavespathSpecas"", which is falsy inconstructInitialQuery(content = resolved ? \@${resolved}` : part.content) and logs an emptyusing relative path: `. - Now that directories and files share the same
pathSpec,displayLabel, andonDebugMessageformat, theif (stats.isDirectory())andelseblocks are near-duplicates.
Suggestion:
Normalize const normalizedRelativePath = relativePath || '.'; and combine both branches:
const isDirectory = stats.isDirectory();
const normalizedRelativePath = relativePath || '.';
resolvedFiles.push({
part,
pathSpec: normalizedRelativePath,
displayLabel: path.isAbsolute(pathName)
? normalizedRelativePath
: pathName,
absolutePath,
isDirectory,
});
onDebugMessage(
`Path ${pathName} resolved to ${isDirectory ? 'directory' : 'file'}: ${absolutePath}, using relative path: ${normalizedRelativePath}`,
);There was a problem hiding this comment.
I have updated the code to apply relative path normalization and combine the branches.
Thank you for your feedback.
| ); | ||
|
|
||
| const pathSpecsToRead = resolvedFiles.map((rf) => { | ||
| const pathSpecsToRead = filesToRead.map((rf) => { |
There was a problem hiding this comment.
Cleanup ([COMPLEXITY-003]): Remove dead rf.pathSpec.endsWith('**') check
Issue:
rf.pathSpec.endsWith('**') ? path.join(rf.absolutePath, '**') : rf.absolutePath (lines 563–570) was only needed when resolveFilePaths rewrote directories to path.join(relativePath, '**'). Since directories are no longer rewritten to ** and are filtered out via filesToRead above, this branch is now dead code and can be simplified:
const pathSpecsToRead = filesToRead.map(
(rf) => rf.absolutePath ?? rf.pathSpec,
);There was a problem hiding this comment.
Done, I simplified pathSpecsToRead to filesToRead.map((rf) => rf.absolutePath ?? rf.pathSpec) and removed the unused endsWith('**') branch.
| expect(result).toEqual({ | ||
| processedQuery: [ | ||
| { | ||
| text: `Compare @${relativeFilePath} with files in @Documents/internal/`, | ||
| }, | ||
| { text: '\n--- Content from referenced files ---' }, | ||
| { text: `\nContent from @${relativeFilePath}:\n` }, | ||
| { text: fileContent }, | ||
| { text: '\n--- End of content ---' }, | ||
| ], | ||
| }); | ||
| expect(mockOnDebugMessage).toHaveBeenCalledWith( | ||
| `Path ${dirPath} resolved to directory, using glob: ${resolvedGlob}`, | ||
| ); | ||
| }); |
There was a problem hiding this comment.
Test Coverage ([TEST-002]): Also assert mockAddItem tool-group display in mixed queries
Suggestion:
In this mixed @<file> + @<directory> test, please also assert that mockAddItem was called for the read file (expect(mockAddItem).toHaveBeenCalledWith(expect.objectContaining({ type: 'tool_group' }), 1271)), and consider adding a test case for @${testRootDir} (referencing the workspace root directory via its absolute path).
There was a problem hiding this comment.
Done, I added the mockAddItem tool_group assertion to the mixed @<file> + @<directory> test and added a unit test verifying that @${testRootDir} normalizes to @..
…andling
- Normalize empty relativePath to '.' in resolveFilePaths when an @ command references the workspace root directory via an absolute path, and consolidate the directory/file resolution branches.
- Simplify pathSpecsToRead in readLocalFiles by removing the dead endsWith('**') check.
- Update handleAtCommand JSDoc and docs/reference/commands.md to reflect that @<directory> resolves the directory path without eagerly reading nested files.
- Add unit test for workspace root directory resolution and assert tool_group display in mixed @<file> + @<directory> queries.
Summary
Updates
@<path>command processing inatCommandProcessor.tsso that directory references (@<directory>) resolve to their relative workspace path without eagerly expanding into a recursive**glob and inlining the contents of all files viaReadManyFilesTool. This allows the model to evaluate the user's request first and select the appropriate tool (such aslist_directory,glob,grep_search, orread_file), significantly reducing prompt token usage when referencing directories.Details
Root Cause
When a user referenced a directory using
@<directory>(for example,List all files stored in @Documents/internal/),resolveFilePathsinpackages/cli/src/ui/hooks/atCommandProcessor.tsconverted the directory path into a recursive glob (path.join(relativePath, '**')) and passed it toreadLocalFiles, which executedReadManyFilesToolclient-side prior to sending the prompt to the model. For directories containing many files or large nested subdirectories, this inlined the full text content of every file into the initial user prompt even when the user only requested a directory listing.Key Changes
packages/cli/src/ui/hooks/atCommandProcessor.ts:isDirectory?: booleantoResolvedFile.resolveFilePathsto record directory paths withpathSpec: relativePathandisDirectory: trueinstead of rewriting them topath.join(relativePath, '**').readLocalFilesto filter out directory entries (resolvedFiles.filter((rf) => !rf.isDirectory)) before invokingReadManyFilesTool, while preserving eager content reading for explicit@<file>references.packages/cli/src/ui/hooks/atCommandProcessor.test.ts:@<directory>prompts and mixed@<file>+@<directory>queries.Related Issues
How to Validate
packages/cli/src/ui/hooks/atCommandProcessor.test.ts.npm run typecheckandnpm run lint.Pre-Merge Checklist