From ae86c7ba05567264ca2d115a7f96d887bc576457 Mon Sep 17 00:00:00 2001 From: joshualitt Date: Thu, 31 Jul 2025 09:31:14 -0700 Subject: [PATCH] bug(core): UI reporting for truncated read_file. (#5155) Co-authored-by: Jacob Richman --- packages/core/src/tools/read-file.test.ts | 2 +- packages/core/src/utils/fileUtils.test.ts | 65 ++++++++++++++++++++++- packages/core/src/utils/fileUtils.ts | 15 +++++- 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/packages/core/src/tools/read-file.test.ts b/packages/core/src/tools/read-file.test.ts index f4086a2b..fa1e458c 100644 --- a/packages/core/src/tools/read-file.test.ts +++ b/packages/core/src/tools/read-file.test.ts @@ -222,7 +222,7 @@ describe('ReadFileTool', () => { 'Line 7', 'Line 8', ].join('\n'), - returnDisplay: '(truncated)', + returnDisplay: 'Read lines 6-8 of 20 from paginated.txt', }); }); diff --git a/packages/core/src/utils/fileUtils.test.ts b/packages/core/src/utils/fileUtils.test.ts index b8e75561..ca121bca 100644 --- a/packages/core/src/utils/fileUtils.test.ts +++ b/packages/core/src/utils/fileUtils.test.ts @@ -420,7 +420,7 @@ describe('fileUtils', () => { expect(result.llmContent).toContain( '[File content truncated: showing lines 6-10 of 20 total lines. Use offset/limit parameters to view more.]', ); - expect(result.returnDisplay).toBe('(truncated)'); + expect(result.returnDisplay).toBe('Read lines 6-10 of 20 from test.txt'); expect(result.isTruncated).toBe(true); expect(result.originalLineCount).toBe(20); expect(result.linesShown).toEqual([6, 10]); @@ -465,9 +465,72 @@ describe('fileUtils', () => { expect(result.llmContent).toContain( '[File content partially truncated: some lines exceeded maximum length of 2000 characters.]', ); + expect(result.returnDisplay).toBe( + 'Read all 3 lines from test.txt (some lines were shortened)', + ); expect(result.isTruncated).toBe(true); }); + it('should truncate when line count exceeds the limit', async () => { + const lines = Array.from({ length: 11 }, (_, i) => `Line ${i + 1}`); + actualNodeFs.writeFileSync(testTextFilePath, lines.join('\n')); + + // Read 5 lines, but there are 11 total + const result = await processSingleFileContent( + testTextFilePath, + tempRootDir, + 0, + 5, + ); + + expect(result.isTruncated).toBe(true); + expect(result.returnDisplay).toBe('Read lines 1-5 of 11 from test.txt'); + }); + + it('should truncate when a line length exceeds the character limit', async () => { + const longLine = 'b'.repeat(2500); + const lines = Array.from({ length: 10 }, (_, i) => `Line ${i + 1}`); + lines.push(longLine); // Total 11 lines + actualNodeFs.writeFileSync(testTextFilePath, lines.join('\n')); + + // Read all 11 lines, including the long one + const result = await processSingleFileContent( + testTextFilePath, + tempRootDir, + 0, + 11, + ); + + expect(result.isTruncated).toBe(true); + expect(result.returnDisplay).toBe( + 'Read all 11 lines from test.txt (some lines were shortened)', + ); + }); + + it('should truncate both line count and line length when both exceed limits', async () => { + const linesWithLongInMiddle = Array.from( + { length: 20 }, + (_, i) => `Line ${i + 1}`, + ); + linesWithLongInMiddle[4] = 'c'.repeat(2500); + actualNodeFs.writeFileSync( + testTextFilePath, + linesWithLongInMiddle.join('\n'), + ); + + // Read 10 lines out of 20, including the long line + const result = await processSingleFileContent( + testTextFilePath, + tempRootDir, + 0, + 10, + ); + expect(result.isTruncated).toBe(true); + expect(result.returnDisplay).toBe( + 'Read lines 1-10 of 20 from test.txt (some lines were shortened)', + ); + }); + it('should return an error if the file size exceeds 20MB', async () => { // Create a file just over 20MB const twentyOneMB = 21 * 1024 * 1024; diff --git a/packages/core/src/utils/fileUtils.ts b/packages/core/src/utils/fileUtils.ts index 6b5ce42c..c016cd4a 100644 --- a/packages/core/src/utils/fileUtils.ts +++ b/packages/core/src/utils/fileUtils.ts @@ -310,9 +310,22 @@ export async function processSingleFileContent( } llmTextContent += formattedLines.join('\n'); + // By default, return nothing to streamline the common case of a successful read_file. + let returnDisplay = ''; + if (contentRangeTruncated) { + returnDisplay = `Read lines ${ + actualStartLine + 1 + }-${endLine} of ${originalLineCount} from ${relativePathForDisplay}`; + if (linesWereTruncatedInLength) { + returnDisplay += ' (some lines were shortened)'; + } + } else if (linesWereTruncatedInLength) { + returnDisplay = `Read all ${originalLineCount} lines from ${relativePathForDisplay} (some lines were shortened)`; + } + return { llmContent: llmTextContent, - returnDisplay: isTruncated ? '(truncated)' : '', + returnDisplay, isTruncated, originalLineCount, linesShown: [actualStartLine + 1, endLine],