diff --git a/src/core/tools/__tests__/writeToFileTool.spec.ts b/src/core/tools/__tests__/writeToFileTool.spec.ts index 78e60cbaa58..ced394cd6a0 100644 --- a/src/core/tools/__tests__/writeToFileTool.spec.ts +++ b/src/core/tools/__tests__/writeToFileTool.spec.ts @@ -339,11 +339,82 @@ describe("writeToFileTool", () => { }) it("processes files with very large line counts", async () => { - await executeWriteFileTool({ line_count: "999999" }) + // Create content that matches the line count to avoid mismatch error + const largeContent = Array(999).fill("Line content").join("\n") + await executeWriteFileTool({ + content: largeContent, + line_count: "999", + }) // Should process normally without issues expect(mockCline.consecutiveMistakeCount).toBe(0) }) + + it("detects potential truncation when line count is 0 and content exceeds 7000 lines", async () => { + // Create content with more than 7000 lines + const largeContent = Array(7500).fill("Line content").join("\n") + + await executeWriteFileTool({ + content: largeContent, + line_count: "0", // Missing line count + }) + + expect(mockCline.consecutiveMistakeCount).toBe(1) + expect(mockCline.recordToolError).toHaveBeenCalledWith("write_to_file") + expect(mockCline.say).toHaveBeenCalledWith("error", expect.stringContaining("7500 lines")) + expect(mockCline.diffViewProvider.reset).toHaveBeenCalled() + }) + + it("detects line count mismatch indicating truncation", async () => { + // Create content with 7001 lines but claim it should have 10000 + const truncatedContent = Array(7001).fill("Line content").join("\n") + + // Need to capture the tool result + mockPushToolResult = vi.fn((result: ToolResponse) => { + toolResult = result + }) + + await executeWriteFileTool({ + content: truncatedContent, + line_count: "10000", // Expected more lines + }) + + expect(mockCline.consecutiveMistakeCount).toBe(1) + expect(mockCline.recordToolError).toHaveBeenCalledWith("write_to_file") + expect(mockCline.say).toHaveBeenCalledWith("error", expect.stringContaining("Line count mismatch")) + // The error message should mention truncation since we have >7000 lines + expect(toolResult).toContain("Content appears to be truncated") + expect(toolResult).toContain("7001 lines") + expect(mockCline.diffViewProvider.revertChanges).toHaveBeenCalled() + }) + + it("allows small line count differences within tolerance", async () => { + // Create content with 95 lines when expecting 100 (5% difference) + const content = Array(95).fill("Line content").join("\n") + + await executeWriteFileTool({ + content: content, + line_count: "100", + }) + + // Should process normally as difference is within 10% tolerance + expect(mockCline.consecutiveMistakeCount).toBe(0) + expect(mockCline.diffViewProvider.saveChanges).toHaveBeenCalled() + }) + + it("handles exact line count match for large files", async () => { + // Create content with exactly 8000 lines + const largeContent = Array(8000).fill("Line content").join("\n") + + await executeWriteFileTool({ + content: largeContent, + line_count: "8000", + }) + + // Should process normally + expect(mockCline.consecutiveMistakeCount).toBe(0) + expect(mockCline.diffViewProvider.saveChanges).toHaveBeenCalled() + }) }) describe("partial block handling", () => { diff --git a/src/core/tools/writeToFileTool.ts b/src/core/tools/writeToFileTool.ts index fd9d158f3f7..2a6e9d24690 100644 --- a/src/core/tools/writeToFileTool.ts +++ b/src/core/tools/writeToFileTool.ts @@ -71,6 +71,29 @@ export async function writeToFileTool( cline.diffViewProvider.editType = fileExists ? "modify" : "create" } + // Check for potential truncation in large files + const actualLineCount = newContent.split("\n").length + const LARGE_FILE_THRESHOLD = 7000 + + // If the file has many lines and no line_count was provided, it might be truncated + if (actualLineCount > LARGE_FILE_THRESHOLD && predictedLineCount === 0) { + cline.consecutiveMistakeCount++ + cline.recordToolError("write_to_file") + + await cline.say( + "error", + `The file content appears to be very large (${actualLineCount} lines). The assistant may have reached its output limit and the content could be truncated. Please verify the file is complete or consider using a different approach for large files.`, + ) + + pushToolResult( + formatResponse.toolError( + `Large file detected (${actualLineCount} lines). The content may be truncated due to output limits. Consider breaking the file into smaller chunks or using a different approach.`, + ), + ) + await cline.diffViewProvider.reset() + return + } + // pre-processing newContent for cases where weaker models might add artifacts like markdown codeblock markers (deepseek/llama) or extra escape characters (gemini) if (newContent.startsWith("```")) { // cline handles cases where it includes language specifiers like ```python ```js @@ -147,6 +170,42 @@ export async function writeToFileTool( return } + // Validate line count matches actual content + const actualLineCount = newContent.split("\n").length + const lineCountMismatchThreshold = 0.1 // 10% tolerance + const lineCountDifference = Math.abs(actualLineCount - predictedLineCount) + const lineCountDifferencePercent = lineCountDifference / predictedLineCount + + // Check for significant mismatch that might indicate truncation + if (predictedLineCount > 100 && lineCountDifferencePercent > lineCountMismatchThreshold) { + cline.consecutiveMistakeCount++ + cline.recordToolError("write_to_file") + + await cline.say( + "error", + `Line count mismatch detected. Expected ${predictedLineCount} lines but got ${actualLineCount} lines. This may indicate the content was truncated.`, + ) + + // Check if this looks like truncation at output limit + const TYPICAL_OUTPUT_LIMIT_LINES = 7000 + if (actualLineCount > TYPICAL_OUTPUT_LIMIT_LINES && actualLineCount < predictedLineCount) { + pushToolResult( + formatResponse.toolError( + `Content appears to be truncated. The file should have ${predictedLineCount} lines but only ${actualLineCount} lines were provided. This often happens when files exceed ~7000 lines due to output token limits. Consider using apply_diff for large file modifications or breaking the content into smaller chunks.`, + ), + ) + } else { + pushToolResult( + formatResponse.toolError( + `Line count mismatch: expected ${predictedLineCount} lines but got ${actualLineCount} lines. Please verify the content is complete.`, + ), + ) + } + + await cline.diffViewProvider.revertChanges() + return + } + cline.consecutiveMistakeCount = 0 // if isEditingFile false, that means we have the full contents of the file already.