diff --git a/docs/tools/multi-file.md b/docs/tools/multi-file.md index 68c1a3ae..7aaff147 100644 --- a/docs/tools/multi-file.md +++ b/docs/tools/multi-file.md @@ -29,6 +29,7 @@ Use `read_many_files` to read content from multiple files specified by paths or `read_many_files` searches for files matching the provided `paths` and `include` patterns, while respecting `exclude` patterns and default excludes (if enabled). - For text files: it reads the content of each matched file (attempting to skip binary files not explicitly requested as image/PDF) and concatenates it into a single string, with a separator `--- {filePath} ---` between the content of each file. Uses UTF-8 encoding by default. +- The tool inserts a `--- End of content ---` after the last file. - For image and PDF files: if explicitly requested by name or extension (e.g., `paths: ["logo.png"]` or `include: ["*.pdf"]`), the tool reads the file and returns its content as a base64 encoded string. - The tool attempts to detect and skip other binary files (those not matching common image/PDF types or not explicitly requested) by checking for null bytes in their initial content. diff --git a/packages/cli/src/ui/hooks/atCommandProcessor.ts b/packages/cli/src/ui/hooks/atCommandProcessor.ts index 3d139db8..e350cf1e 100644 --- a/packages/cli/src/ui/hooks/atCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/atCommandProcessor.ts @@ -447,7 +447,6 @@ export async function handleAtCommand({ processedQueryParts.push(part); } } - processedQueryParts.push({ text: '\n--- End of content ---' }); } else { onDebugMessage( 'read_many_files tool returned no content or empty content.', diff --git a/packages/cli/src/zed-integration/zedIntegration.ts b/packages/cli/src/zed-integration/zedIntegration.ts index 9a10e1a8..79f90637 100644 --- a/packages/cli/src/zed-integration/zedIntegration.ts +++ b/packages/cli/src/zed-integration/zedIntegration.ts @@ -750,7 +750,6 @@ class Session { processedQueryParts.push(part); } } - processedQueryParts.push({ text: '\n--- End of content ---' }); } else { console.warn( 'read_many_files tool returned no content or empty content.', diff --git a/packages/core/src/tools/read-many-files.test.ts b/packages/core/src/tools/read-many-files.test.ts index 61ae8310..3145dd26 100644 --- a/packages/core/src/tools/read-many-files.test.ts +++ b/packages/core/src/tools/read-many-files.test.ts @@ -221,6 +221,7 @@ describe('ReadManyFilesTool', () => { const expectedPath = path.join(tempRootDir, 'file1.txt'); expect(result.llmContent).toEqual([ `--- ${expectedPath} ---\n\nContent of file1\n\n`, + `\n--- End of content ---`, ]); expect(result.returnDisplay).toContain( 'Successfully read and concatenated content from **1 file(s)**', @@ -285,7 +286,10 @@ describe('ReadManyFilesTool', () => { const result = await invocation.execute(new AbortController().signal); const content = result.llmContent as string[]; const expectedPath = path.join(tempRootDir, 'src/main.ts'); - expect(content).toEqual([`--- ${expectedPath} ---\n\nMain content\n\n`]); + expect(content).toEqual([ + `--- ${expectedPath} ---\n\nMain content\n\n`, + `\n--- End of content ---`, + ]); expect( content.find((c) => c.includes('src/main.test.ts')), ).toBeUndefined(); @@ -314,7 +318,10 @@ describe('ReadManyFilesTool', () => { const result = await invocation.execute(new AbortController().signal); const content = result.llmContent as string[]; const expectedPath = path.join(tempRootDir, 'src/app.js'); - expect(content).toEqual([`--- ${expectedPath} ---\n\napp code\n\n`]); + expect(content).toEqual([ + `--- ${expectedPath} ---\n\napp code\n\n`, + `\n--- End of content ---`, + ]); expect( content.find((c) => c.includes('node_modules/some-lib/index.js')), ).toBeUndefined(); @@ -367,6 +374,7 @@ describe('ReadManyFilesTool', () => { mimeType: 'image/png', }, }, + '\n--- End of content ---', ]); expect(result.returnDisplay).toContain( 'Successfully read and concatenated content from **1 file(s)**', @@ -390,6 +398,7 @@ describe('ReadManyFilesTool', () => { mimeType: 'image/png', }, }, + '\n--- End of content ---', ]); }); @@ -426,6 +435,7 @@ describe('ReadManyFilesTool', () => { mimeType: 'application/pdf', }, }, + '\n--- End of content ---', ]); }); @@ -441,6 +451,7 @@ describe('ReadManyFilesTool', () => { mimeType: 'application/pdf', }, }, + '\n--- End of content ---', ]); }); @@ -549,6 +560,7 @@ describe('ReadManyFilesTool', () => { Content of receive-detail `, + `\n--- End of content ---`, ]); expect(result.returnDisplay).toContain( 'Successfully read and concatenated content from **1 file(s)**', @@ -567,6 +579,7 @@ Content of receive-detail Content of file[1] `, + `\n--- End of content ---`, ]); expect(result.returnDisplay).toContain( 'Successfully read and concatenated content from **1 file(s)**', @@ -634,9 +647,10 @@ Content of file[1] const invocation = tool.build(params); const result = await invocation.execute(new AbortController().signal); - // Verify all files were processed + // Verify all files were processed. The content should have fileCount + // entries + 1 for the output terminator. const content = result.llmContent as string[]; - expect(content).toHaveLength(fileCount); + expect(content).toHaveLength(fileCount + 1); for (let i = 0; i < fileCount; i++) { expect(content.join('')).toContain(`Batch test ${i}`); } diff --git a/packages/core/src/tools/read-many-files.ts b/packages/core/src/tools/read-many-files.ts index 1d48dd63..9f3a1161 100644 --- a/packages/core/src/tools/read-many-files.ts +++ b/packages/core/src/tools/read-many-files.ts @@ -143,6 +143,7 @@ const DEFAULT_EXCLUDES: string[] = [ ]; const DEFAULT_OUTPUT_SEPARATOR_FORMAT = '--- {filePath} ---'; +const DEFAULT_OUTPUT_TERMINATOR = '\n--- End of content ---'; class ReadManyFilesToolInvocation extends BaseToolInvocation< ReadManyFilesParams, @@ -534,7 +535,9 @@ ${finalExclusionPatternsForDescription displayMessage += `No files were read and concatenated based on the criteria.\n`; } - if (contentParts.length === 0) { + if (contentParts.length > 0) { + contentParts.push(DEFAULT_OUTPUT_TERMINATOR); + } else { contentParts.push( 'No files matching the criteria were found or all were skipped.', ); @@ -636,7 +639,7 @@ This tool is useful when you need to understand or analyze a collection of files - Gathering context from multiple configuration files. - When the user asks to "read all files in X directory" or "show me the content of all Y files". -Use this tool when the user's query implies needing the content of several files simultaneously for context, analysis, or summarization. For text files, it uses default UTF-8 encoding and a '--- {filePath} ---' separator between file contents. Ensure paths are relative to the target directory. Glob patterns like 'src/**/*.js' are supported. Avoid using for single files if a more specific single-file reading tool is available, unless the user specifically requests to process a list containing just one file via this tool. Other binary files (not explicitly requested as image/PDF) are generally skipped. Default excludes apply to common non-text files (except for explicitly requested images/PDFs) and large dependency directories unless 'useDefaultExcludes' is false.`, +Use this tool when the user's query implies needing the content of several files simultaneously for context, analysis, or summarization. For text files, it uses default UTF-8 encoding and a '--- {filePath} ---' separator between file contents. The tool inserts a '--- End of content ---' after the last file. Ensure paths are relative to the target directory. Glob patterns like 'src/**/*.js' are supported. Avoid using for single files if a more specific single-file reading tool is available, unless the user specifically requests to process a list containing just one file via this tool. Other binary files (not explicitly requested as image/PDF) are generally skipped. Default excludes apply to common non-text files (except for explicitly requested images/PDFs) and large dependency directories unless 'useDefaultExcludes' is false.`, Kind.Read, parameterSchema, );