From 05b2e2764a8bafcd80f47b458edd322721b83209 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Fri, 27 Jun 2025 06:19:10 +0700 Subject: [PATCH 01/21] feat(tools): add image support to read_file tool - Add support for reading and displaying image files (PNG, JPG, JPEG, GIF, WebP, SVG, BMP, ICO, TIFF) - Implement readImageAsDataUrl function to convert images to base64 data URLs with proper MIME types - Return multi-part responses containing both XML metadata and image data - Add dimension extraction for PNG files when possible - Add comprehensive test coverage for image reading functionality including format detection, error handling, and edge cases - Maintain backward compatibility for non-image binary files --- src/core/tools/__tests__/readFileTool.spec.ts | 323 +++++++++++++++++- src/core/tools/readFileTool.ts | 110 +++++- 2 files changed, 425 insertions(+), 8 deletions(-) diff --git a/src/core/tools/__tests__/readFileTool.spec.ts b/src/core/tools/__tests__/readFileTool.spec.ts index 44be1d3b92..e33684b980 100644 --- a/src/core/tools/__tests__/readFileTool.spec.ts +++ b/src/core/tools/__tests__/readFileTool.spec.ts @@ -20,17 +20,20 @@ vi.mock("path", async () => { } }) -vi.mock("fs/promises", () => ({ - mkdir: vi.fn().mockResolvedValue(undefined), - writeFile: vi.fn().mockResolvedValue(undefined), - readFile: vi.fn().mockResolvedValue("{}"), -})) +// Already mocked above with hoisted fsPromises vi.mock("isbinaryfile") vi.mock("../../../integrations/misc/line-counter") vi.mock("../../../integrations/misc/read-lines") +// Mock fs/promises readFile for image tests +const fsPromises = vi.hoisted(() => ({ + readFile: vi.fn(), + stat: vi.fn().mockResolvedValue({ size: 1024 }), +})) +vi.mock("fs/promises", () => fsPromises) + // Mock input content for tests let mockInputContent = "" @@ -48,6 +51,53 @@ const addLineNumbersMock = vi.fn().mockImplementation((text, startLine = 1) => { const extractTextFromFileMock = vi.fn() const getSupportedBinaryFormatsMock = vi.fn(() => [".pdf", ".docx", ".ipynb"]) +// Mock formatResponse - use vi.hoisted to ensure mocks are available before vi.mock +const { toolResultMock, imageBlocksMock } = vi.hoisted(() => { + const toolResultMock = vi.fn((text: string, images?: string[]) => { + if (images && images.length > 0) { + return [ + { type: "text", text }, + ...images.map((img) => { + const [header, data] = img.split(",") + const media_type = header.match(/:(.*?);/)?.[1] || "image/png" + return { type: "image", source: { type: "base64", media_type, data } } + }), + ] + } + return text + }) + const imageBlocksMock = vi.fn((images?: string[]) => { + return images + ? images.map((img) => { + const [header, data] = img.split(",") + const media_type = header.match(/:(.*?);/)?.[1] || "image/png" + return { type: "image", source: { type: "base64", media_type, data } } + }) + : [] + }) + return { toolResultMock, imageBlocksMock } +}) + +vi.mock("../../prompts/responses", () => ({ + formatResponse: { + toolDenied: vi.fn(() => "The user denied this operation."), + toolDeniedWithFeedback: vi.fn( + (feedback?: string) => + `The user denied this operation and provided the following feedback:\n\n${feedback}\n`, + ), + toolApprovedWithFeedback: vi.fn( + (feedback?: string) => + `The user approved this operation and provided the following context:\n\n${feedback}\n`, + ), + rooIgnoreError: vi.fn( + (path: string) => + `Access to ${path} is blocked by the .rooignore file settings. You must try to continue in the task without using this file, or ask the user to update the .rooignore file.`, + ), + toolResult: toolResultMock, + imageBlocks: imageBlocksMock, + }, +})) + vi.mock("../../ignore/RooIgnoreController", () => ({ RooIgnoreController: class { initialize() { @@ -520,3 +570,266 @@ describe("read_file tool XML output structure", () => { }) }) }) + +describe("read_file tool with image support", () => { + const testImagePath = "test/image.png" + const absoluteImagePath = "/test/image.png" + const base64ImageData = + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" + const imageBuffer = Buffer.from(base64ImageData, "base64") + + const mockedCountFileLines = vi.mocked(countFileLines) + const mockedIsBinaryFile = vi.mocked(isBinaryFile) + const mockedPathResolve = vi.mocked(path.resolve) + const mockedFsReadFile = vi.mocked(fsPromises.readFile) + const mockedExtractTextFromFile = vi.mocked(extractTextFromFile) + + const mockCline: any = {} + let mockProvider: any + let toolResult: ToolResponse | undefined + + beforeEach(() => { + vi.clearAllMocks() + + mockedPathResolve.mockReturnValue(absoluteImagePath) + mockedIsBinaryFile.mockResolvedValue(true) + mockedCountFileLines.mockResolvedValue(0) + mockedFsReadFile.mockResolvedValue(imageBuffer) + + mockProvider = { + getState: vi.fn().mockResolvedValue({ maxReadFileLine: -1 }), + deref: vi.fn().mockReturnThis(), + } + + mockCline.cwd = "/" + mockCline.task = "Test" + mockCline.providerRef = mockProvider + mockCline.rooIgnoreController = { + validateAccess: vi.fn().mockReturnValue(true), + } + mockCline.say = vi.fn().mockResolvedValue(undefined) + mockCline.ask = vi.fn().mockResolvedValue({ response: "yesButtonClicked" }) + mockCline.presentAssistantMessage = vi.fn() + mockCline.handleError = vi.fn().mockResolvedValue(undefined) + mockCline.pushToolResult = vi.fn() + mockCline.removeClosingTag = vi.fn((tag, content) => content) + + mockCline.fileContextTracker = { + trackFileContext: vi.fn().mockResolvedValue(undefined), + } + + mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) + mockCline.recordToolError = vi.fn().mockReturnValue(undefined) + + toolResult = undefined + }) + + async function executeReadImageTool(imagePath: string = testImagePath): Promise { + const argsContent = `${imagePath}` + const toolUse: ReadFileToolUse = { + type: "tool_use", + name: "read_file", + params: { args: argsContent }, + partial: false, + } + + await readFileTool( + mockCline, + toolUse, + mockCline.ask, + vi.fn(), + (result: ToolResponse) => { + toolResult = result + }, + (_: ToolParamName, content?: string) => content ?? "", + ) + + return toolResult + } + + describe("Image Format Detection", () => { + it.each([ + [".png", "image.png", "image/png"], + [".jpg", "photo.jpg", "image/jpeg"], + [".jpeg", "picture.jpeg", "image/jpeg"], + [".gif", "animation.gif", "image/gif"], + [".bmp", "bitmap.bmp", "image/bmp"], + [".svg", "vector.svg", "image/svg+xml"], + [".webp", "modern.webp", "image/webp"], + [".ico", "favicon.ico", "image/x-icon"], + [".avif", "new-format.avif", "image/avif"], + ])("should detect %s as an image format", async (ext, filename, expectedMimeType) => { + // Setup + const imagePath = `test/${filename}` + const absolutePath = `/test/${filename}` + mockedPathResolve.mockReturnValue(absolutePath) + + // Execute + const result = await executeReadImageTool(imagePath) + + // Verify result is a multi-part response + expect(Array.isArray(result)).toBe(true) + const textPart = (result as any[]).find((p) => p.type === "text")?.text + const imagePart = (result as any[]).find((p) => p.type === "image") + + // Verify text part + expect(textPart).toContain(`${imagePath}`) + expect(textPart).not.toContain("") + expect(textPart).toContain(`Image file`) + + // Verify image part + expect(imagePart).toBeDefined() + expect(imagePart.source.media_type).toBe(expectedMimeType) + expect(imagePart.source.data).toBe(base64ImageData) + }) + }) + + describe("Image Reading Functionality", () => { + it("should read image file and return a multi-part response", async () => { + // Execute + const result = await executeReadImageTool() + + // Verify result is a multi-part response + expect(Array.isArray(result)).toBe(true) + const textPart = (result as any[]).find((p) => p.type === "text")?.text + const imagePart = (result as any[]).find((p) => p.type === "image") + + // Verify text part + expect(textPart).toContain(`${testImagePath}`) + expect(textPart).not.toContain(``) + expect(textPart).toContain(`Image file`) + + // Verify image part + expect(imagePart).toBeDefined() + expect(imagePart.source.media_type).toBe("image/png") + expect(imagePart.source.data).toBe(base64ImageData) + }) + + it("should call formatResponse.toolResult with text and image data", async () => { + // Execute + await executeReadImageTool() + + // Verify toolResultMock was called correctly + expect(toolResultMock).toHaveBeenCalledTimes(1) + const callArgs = toolResultMock.mock.calls[0] + const textArg = callArgs[0] + const imagesArg = callArgs[1] + + expect(textArg).toContain(`${testImagePath}`) + expect(imagesArg).toBeDefined() + expect(imagesArg).toBeInstanceOf(Array) + expect(imagesArg!.length).toBe(1) + expect(imagesArg![0]).toBe(`data:image/png;base64,${base64ImageData}`) + }) + + it("should handle large image files", async () => { + // Setup - simulate a large image + const largeBase64 = "A".repeat(1000000) // 1MB of base64 data + const largeBuffer = Buffer.from(largeBase64, "base64") + mockedFsReadFile.mockResolvedValue(largeBuffer) + + // Execute + const result = await executeReadImageTool() + + // Verify it still works with large data + expect(Array.isArray(result)).toBe(true) + const imagePart = (result as any[]).find((p) => p.type === "image") + expect(imagePart).toBeDefined() + expect(imagePart.source.media_type).toBe("image/png") + expect(imagePart.source.data).toBe(largeBase64) + }) + + it("should handle errors when reading image files", async () => { + // Setup - simulate read error + mockedFsReadFile.mockRejectedValue(new Error("Failed to read image")) + + // Execute + const result = await executeReadImageTool() + + // Verify error handling + expect(result).toContain("Error reading image file: Failed to read image") + expect(mockCline.handleError).toHaveBeenCalled() + }) + }) + + describe("Binary File Handling", () => { + it("should not treat non-image binary files as images", async () => { + // Setup + const binaryPath = "test/document.pdf" + const absolutePath = "/test/document.pdf" + mockedPathResolve.mockReturnValue(absolutePath) + mockedExtractTextFromFile.mockResolvedValue("PDF content extracted") + + // Execute + const result = await executeReadImageTool(binaryPath) + + // Verify it uses extractTextFromFile instead + expect(result).not.toContain("") + expect(mockedExtractTextFromFile).toHaveBeenCalledWith(absolutePath) + }) + + it("should handle unknown binary formats", async () => { + // Setup + const binaryPath = "test/unknown.bin" + const absolutePath = "/test/unknown.bin" + mockedPathResolve.mockReturnValue(absolutePath) + mockedExtractTextFromFile.mockResolvedValue("") + + // Execute + const result = await executeReadImageTool(binaryPath) + + // Verify + expect(result).not.toContain("") + expect(result).toContain(' { + it("should handle case-insensitive image extensions", async () => { + // Test uppercase extensions + const uppercasePath = "test/IMAGE.PNG" + const absolutePath = "/test/IMAGE.PNG" + mockedPathResolve.mockReturnValue(absolutePath) + + // Execute + const result = await executeReadImageTool(uppercasePath) + + // Verify + expect(Array.isArray(result)).toBe(true) + const imagePart = (result as any[]).find((p) => p.type === "image") + expect(imagePart).toBeDefined() + expect(imagePart.source.media_type).toBe("image/png") + }) + + it("should handle files with multiple dots in name", async () => { + // Setup + const complexPath = "test/my.photo.backup.png" + const absolutePath = "/test/my.photo.backup.png" + mockedPathResolve.mockReturnValue(absolutePath) + + // Execute + const result = await executeReadImageTool(complexPath) + + // Verify + expect(Array.isArray(result)).toBe(true) + const imagePart = (result as any[]).find((p) => p.type === "image") + expect(imagePart).toBeDefined() + expect(imagePart.source.media_type).toBe("image/png") + }) + + it("should handle empty image files", async () => { + // Setup - empty buffer + mockedFsReadFile.mockResolvedValue(Buffer.from("")) + + // Execute + const result = await executeReadImageTool() + + // Verify - should still create valid data URL + expect(Array.isArray(result)).toBe(true) + const imagePart = (result as any[]).find((p) => p.type === "image") + expect(imagePart).toBeDefined() + expect(imagePart.source.media_type).toBe("image/png") + expect(imagePart.source.data).toBe("") + }) + }) +}) diff --git a/src/core/tools/readFileTool.ts b/src/core/tools/readFileTool.ts index 6de8dd5642..5ff34b21e5 100644 --- a/src/core/tools/readFileTool.ts +++ b/src/core/tools/readFileTool.ts @@ -14,6 +14,49 @@ import { readLines } from "../../integrations/misc/read-lines" import { extractTextFromFile, addLineNumbers, getSupportedBinaryFormats } from "../../integrations/misc/extract-text" import { parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter" import { parseXml } from "../../utils/xml" +import * as fs from "fs/promises" + +/** + * Supported image formats that can be displayed + */ +const SUPPORTED_IMAGE_FORMATS = [ + ".png", + ".jpg", + ".jpeg", + ".gif", + ".webp", + ".svg", + ".bmp", + ".ico", + ".tiff", + ".tif", +] as const + +/** + * Reads an image file and returns it as a base64 data URL + */ +async function readImageAsDataUrl(filePath: string): Promise { + const fileBuffer = await fs.readFile(filePath) + const base64 = fileBuffer.toString("base64") + const ext = path.extname(filePath).toLowerCase() + + // Map extensions to MIME types + const mimeTypes: Record = { + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".webp": "image/webp", + ".svg": "image/svg+xml", + ".bmp": "image/bmp", + ".ico": "image/x-icon", + ".tiff": "image/tiff", + ".tif": "image/tiff", + } + + const mimeType = mimeTypes[ext] || "image/png" + return `data:${mimeType};base64,${base64}` +} export function getReadFileToolDescription(blockName: string, blockParams: any): string { // Handle both single path and multiple files via args @@ -66,6 +109,7 @@ interface FileResult { notice?: string lineRanges?: LineRange[] xmlContent?: string // Final XML content for this file + imageDataUrl?: string // Image data URL for image files feedbackText?: string // User feedback text from approval/denial feedbackImages?: any[] // User feedback images from approval/denial } @@ -440,6 +484,55 @@ export async function readFileTool( const fileExtension = path.extname(relPath).toLowerCase() const supportedBinaryFormats = getSupportedBinaryFormats() + // Check if it's a supported image format + if (SUPPORTED_IMAGE_FORMATS.includes(fileExtension as any)) { + try { + const imageDataUrl = await readImageAsDataUrl(fullPath) + const imageStats = await fs.stat(fullPath) + const imageSizeInKB = Math.round(imageStats.size / 1024) + + // For images, get dimensions if possible + let dimensionsInfo = "" + if (fileExtension === ".png") { + // Simple PNG dimension extraction (first 24 bytes contain width/height) + const buffer = await fs.readFile(fullPath) + if (buffer.length >= 24) { + const width = buffer.readUInt32BE(16) + const height = buffer.readUInt32BE(20) + if (width && height) { + dimensionsInfo = `${width}x${height} pixels` + } + } + } + + // Track file read + await cline.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource) + + // Store image data URL separately - NOT in XML + const noticeText = dimensionsInfo + ? `Image file (${dimensionsInfo}, ${imageSizeInKB} KB)` + : `Image file (${imageSizeInKB} KB)` + + updateFileResult(relPath, { + xmlContent: `${relPath}\n${noticeText}\n`, + imageDataUrl: imageDataUrl, + }) + continue + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error) + updateFileResult(relPath, { + status: "error", + error: `Error reading image file: ${errorMsg}`, + xmlContent: `${relPath}Error reading image file: ${errorMsg}`, + }) + await handleError( + `reading image file ${relPath}`, + error instanceof Error ? error : new Error(errorMsg), + ) + continue + } + } + if (!supportedBinaryFormats.includes(fileExtension)) { updateFileResult(relPath, { notice: "Binary file", @@ -546,6 +639,11 @@ export async function readFileTool( const xmlResults = fileResults.filter((result) => result.xmlContent).map((result) => result.xmlContent) const filesXml = `\n${xmlResults.join("\n")}\n` + // Collect all image data URLs from file results + const fileImageUrls = fileResults + .filter((result) => result.imageDataUrl) + .map((result) => result.imageDataUrl as string) + // Process all feedback in a unified way without branching let statusMessage = "" let feedbackImages: any[] = [] @@ -573,20 +671,26 @@ export async function readFileTool( } } + // Combine all images: feedback images first, then file images + const allImages = [...feedbackImages, ...fileImageUrls] + // Push the result with appropriate formatting if (statusMessage) { - const result = formatResponse.toolResult(statusMessage, feedbackImages) + const result = formatResponse.toolResult(statusMessage, allImages) // Handle different return types from toolResult if (typeof result === "string") { pushToolResult(`${result}\n${filesXml}`) } else { - // For block-based results, we need to convert the filesXml to a text block and append it + // For block-based results, append the files XML as a text block const textBlock = { type: "text" as const, text: filesXml } pushToolResult([...result, textBlock]) } + } else if (allImages.length > 0) { + // If we have images but no status message, create blocks + pushToolResult([{ type: "text" as const, text: filesXml }, ...formatResponse.imageBlocks(allImages)]) } else { - // No status message, just push the files XML + // No images or status message, just push the files XML pushToolResult(filesXml) } } catch (error) { From 7f47b14ff86a2f06d3d6c1d0a313a0689a426656 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Tue, 1 Jul 2025 22:02:11 +0700 Subject: [PATCH 02/21] feat: add support for AVIF image format in readFileTool --- src/core/tools/__tests__/readFileTool.spec.ts | 34 ++++++++++++--- src/core/tools/readFileTool.ts | 42 +++++++++++++------ 2 files changed, 58 insertions(+), 18 deletions(-) diff --git a/src/core/tools/__tests__/readFileTool.spec.ts b/src/core/tools/__tests__/readFileTool.spec.ts index e33684b980..572f302347 100644 --- a/src/core/tools/__tests__/readFileTool.spec.ts +++ b/src/core/tools/__tests__/readFileTool.spec.ts @@ -38,7 +38,11 @@ vi.mock("fs/promises", () => fsPromises) let mockInputContent = "" // First create all the mocks -vi.mock("../../../integrations/misc/extract-text") +vi.mock("../../../integrations/misc/extract-text", () => ({ + extractTextFromFile: vi.fn(), + addLineNumbers: vi.fn(), + getSupportedBinaryFormats: vi.fn(() => [".pdf", ".docx", ".ipynb"]), +})) vi.mock("../../../services/tree-sitter") // Then create the mock functions @@ -743,12 +747,32 @@ describe("read_file tool with image support", () => { // Setup - simulate read error mockedFsReadFile.mockRejectedValue(new Error("Failed to read image")) - // Execute - const result = await executeReadImageTool() + // Create a spy for handleError + const handleErrorSpy = vi.fn() + + // Execute with the spy + const argsContent = `${testImagePath}` + const toolUse: ReadFileToolUse = { + type: "tool_use", + name: "read_file", + params: { args: argsContent }, + partial: false, + } + + await readFileTool( + mockCline, + toolUse, + mockCline.ask, + handleErrorSpy, // Use our spy here + (result: ToolResponse) => { + toolResult = result + }, + (_: ToolParamName, content?: string) => content ?? "", + ) // Verify error handling - expect(result).toContain("Error reading image file: Failed to read image") - expect(mockCline.handleError).toHaveBeenCalled() + expect(toolResult).toContain("Error reading image file: Failed to read image") + expect(handleErrorSpy).toHaveBeenCalled() }) }) diff --git a/src/core/tools/readFileTool.ts b/src/core/tools/readFileTool.ts index 5ff34b21e5..82ef172dd7 100644 --- a/src/core/tools/readFileTool.ts +++ b/src/core/tools/readFileTool.ts @@ -30,6 +30,7 @@ const SUPPORTED_IMAGE_FORMATS = [ ".ico", ".tiff", ".tif", + ".avif", ] as const /** @@ -52,6 +53,7 @@ async function readImageAsDataUrl(filePath: string): Promise { ".ico": "image/x-icon", ".tiff": "image/tiff", ".tif": "image/tiff", + ".avif": "image/avif", } const mimeType = mimeTypes[ext] || "image/png" @@ -533,14 +535,19 @@ export async function readFileTool( } } - if (!supportedBinaryFormats.includes(fileExtension)) { + // Check if it's a supported binary format that can be processed + if (supportedBinaryFormats && supportedBinaryFormats.includes(fileExtension)) { + // For supported binary formats (.pdf, .docx, .ipynb), continue to extractTextFromFile + // Fall through to the normal extractTextFromFile processing below + } else { + // Handle unknown binary format + const fileFormat = fileExtension.slice(1) || "bin" // Remove the dot, fallback to "bin" updateFileResult(relPath, { - notice: "Binary file", - xmlContent: `${relPath}\nBinary file\n`, + notice: `Binary file format: ${fileFormat}`, + xmlContent: `${relPath}\nBinary file - content not displayed\n`, }) continue } - // For supported binary formats (.pdf, .docx, .ipynb), continue to extractTextFromFile } // Handle range reads (bypass maxReadFileLine) @@ -675,20 +682,29 @@ export async function readFileTool( const allImages = [...feedbackImages, ...fileImageUrls] // Push the result with appropriate formatting - if (statusMessage) { - const result = formatResponse.toolResult(statusMessage, allImages) + if (statusMessage || allImages.length > 0) { + // Always use formatResponse.toolResult when we have a status message or images + const result = formatResponse.toolResult( + statusMessage || filesXml, + allImages.length > 0 ? allImages : undefined, + ) // Handle different return types from toolResult if (typeof result === "string") { - pushToolResult(`${result}\n${filesXml}`) + if (statusMessage) { + pushToolResult(`${result}\n${filesXml}`) + } else { + pushToolResult(result) + } } else { - // For block-based results, append the files XML as a text block - const textBlock = { type: "text" as const, text: filesXml } - pushToolResult([...result, textBlock]) + // For block-based results, append the files XML as a text block if not already included + if (statusMessage) { + const textBlock = { type: "text" as const, text: filesXml } + pushToolResult([...result, textBlock]) + } else { + pushToolResult(result) + } } - } else if (allImages.length > 0) { - // If we have images but no status message, create blocks - pushToolResult([{ type: "text" as const, text: filesXml }, ...formatResponse.imageBlocks(allImages)]) } else { // No images or status message, just push the files XML pushToolResult(filesXml) From 323276d84454c5ae96321fe142c29b3a0e0d802a Mon Sep 17 00:00:00 2001 From: sam hoang Date: Tue, 1 Jul 2025 22:16:17 +0700 Subject: [PATCH 03/21] feat: add image size limit check in readFileTool --- src/core/tools/readFileTool.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/core/tools/readFileTool.ts b/src/core/tools/readFileTool.ts index 82ef172dd7..390f175073 100644 --- a/src/core/tools/readFileTool.ts +++ b/src/core/tools/readFileTool.ts @@ -16,6 +16,11 @@ import { parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter" import { parseXml } from "../../utils/xml" import * as fs from "fs/promises" +/** + * Maximum allowed image file size in bytes (5MB) + */ +const MAX_IMAGE_FILE_SIZE_BYTES = 5 * 1024 * 1024 + /** * Supported image formats that can be displayed */ @@ -489,8 +494,23 @@ export async function readFileTool( // Check if it's a supported image format if (SUPPORTED_IMAGE_FORMATS.includes(fileExtension as any)) { try { - const imageDataUrl = await readImageAsDataUrl(fullPath) const imageStats = await fs.stat(fullPath) + + // Check if image file exceeds size limit + if (imageStats.size > MAX_IMAGE_FILE_SIZE_BYTES) { + const imageSizeInMB = (imageStats.size / (1024 * 1024)).toFixed(1) + const notice = `Image file is too large (${imageSizeInMB} MB). The maximum allowed size is 5 MB.` + + // Track file read + await cline.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource) + + updateFileResult(relPath, { + xmlContent: `${relPath}\n${notice}\n`, + }) + continue + } + + const imageDataUrl = await readImageAsDataUrl(fullPath) const imageSizeInKB = Math.round(imageStats.size / 1024) // For images, get dimensions if possible From 0b3915853531bc8e0f95a815d6bf90e0de67ea24 Mon Sep 17 00:00:00 2001 From: sam hoang Date: Tue, 1 Jul 2025 22:46:43 +0700 Subject: [PATCH 04/21] test: enhance image support tests for path normalization --- src/core/tools/__tests__/readFileTool.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/tools/__tests__/readFileTool.spec.ts b/src/core/tools/__tests__/readFileTool.spec.ts index 572f302347..68c92e5671 100644 --- a/src/core/tools/__tests__/readFileTool.spec.ts +++ b/src/core/tools/__tests__/readFileTool.spec.ts @@ -789,7 +789,10 @@ describe("read_file tool with image support", () => { // Verify it uses extractTextFromFile instead expect(result).not.toContain("") - expect(mockedExtractTextFromFile).toHaveBeenCalledWith(absolutePath) + // Make the test platform-agnostic by checking the call was made (path normalization can vary) + expect(mockedExtractTextFromFile).toHaveBeenCalledTimes(1) + const callArgs = mockedExtractTextFromFile.mock.calls[0] + expect(callArgs[0]).toMatch(/[\\\/]test[\\\/]document\.pdf$/) }) it("should handle unknown binary formats", async () => { From 6badb513f4a92cf14e355e452fd825c3dd49f27f Mon Sep 17 00:00:00 2001 From: Sam Hoang Van Date: Tue, 1 Jul 2025 22:58:23 +0700 Subject: [PATCH 05/21] fix pr comment --- src/core/tools/__tests__/readFileTool.spec.ts | 36 +++++++++++++++++++ src/core/tools/readFileTool.ts | 15 ++++---- src/i18n/locales/ca/tools.json | 3 +- src/i18n/locales/de/tools.json | 3 +- src/i18n/locales/en/tools.json | 3 +- src/i18n/locales/es/tools.json | 3 +- src/i18n/locales/fr/tools.json | 3 +- src/i18n/locales/hi/tools.json | 3 +- src/i18n/locales/id/tools.json | 3 +- src/i18n/locales/it/tools.json | 3 +- src/i18n/locales/ja/tools.json | 3 +- src/i18n/locales/ko/tools.json | 3 +- src/i18n/locales/nl/tools.json | 3 +- src/i18n/locales/pl/tools.json | 3 +- src/i18n/locales/pt-BR/tools.json | 3 +- src/i18n/locales/ru/tools.json | 3 +- src/i18n/locales/tr/tools.json | 3 +- src/i18n/locales/vi/tools.json | 3 +- src/i18n/locales/zh-CN/tools.json | 3 +- src/i18n/locales/zh-TW/tools.json | 3 +- 20 files changed, 80 insertions(+), 25 deletions(-) diff --git a/src/core/tools/__tests__/readFileTool.spec.ts b/src/core/tools/__tests__/readFileTool.spec.ts index 68c92e5671..cbd9879fb5 100644 --- a/src/core/tools/__tests__/readFileTool.spec.ts +++ b/src/core/tools/__tests__/readFileTool.spec.ts @@ -726,6 +726,42 @@ describe("read_file tool with image support", () => { expect(imagesArg![0]).toBe(`data:image/png;base64,${base64ImageData}`) }) + it("should extract and display PNG dimensions correctly", async () => { + // Setup - Create a proper PNG buffer with known dimensions (100x200 pixels) + const pngSignature = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) // PNG signature + const ihdrLength = Buffer.from([0x00, 0x00, 0x00, 0x0D]) // IHDR chunk length (13 bytes) + const ihdrType = Buffer.from("IHDR", "ascii") // IHDR chunk type + const width = Buffer.alloc(4) + width.writeUInt32BE(100, 0) // Width: 100 pixels + const height = Buffer.alloc(4) + height.writeUInt32BE(200, 0) // Height: 200 pixels + const ihdrData = Buffer.from([0x08, 0x02, 0x00, 0x00, 0x00]) // Bit depth, color type, compression, filter, interlace + const crc = Buffer.from([0x00, 0x00, 0x00, 0x00]) // Dummy CRC + + const pngBuffer = Buffer.concat([pngSignature, ihdrLength, ihdrType, width, height, ihdrData, crc]) + const pngBase64 = pngBuffer.toString("base64") + + mockedFsReadFile.mockResolvedValue(pngBuffer) + + // Execute + const result = await executeReadImageTool() + + // Verify result is a multi-part response + expect(Array.isArray(result)).toBe(true) + const textPart = (result as any[]).find((p) => p.type === "text")?.text + const imagePart = (result as any[]).find((p) => p.type === "image") + + // Verify text part contains dimensions + expect(textPart).toContain(`${testImagePath}`) + expect(textPart).toContain("100x200 pixels") // Should include the dimensions + expect(textPart).toContain(`Image file`) + + // Verify image part + expect(imagePart).toBeDefined() + expect(imagePart.source.media_type).toBe("image/png") + expect(imagePart.source.data).toBe(pngBase64) + }) + it("should handle large image files", async () => { // Setup - simulate a large image const largeBase64 = "A".repeat(1000000) // 1MB of base64 data diff --git a/src/core/tools/readFileTool.ts b/src/core/tools/readFileTool.ts index 390f175073..0adadc27f8 100644 --- a/src/core/tools/readFileTool.ts +++ b/src/core/tools/readFileTool.ts @@ -39,9 +39,9 @@ const SUPPORTED_IMAGE_FORMATS = [ ] as const /** - * Reads an image file and returns it as a base64 data URL + * Reads an image file and returns both the data URL and buffer */ -async function readImageAsDataUrl(filePath: string): Promise { +async function readImageAsDataUrlWithBuffer(filePath: string): Promise<{ dataUrl: string; buffer: Buffer }> { const fileBuffer = await fs.readFile(filePath) const base64 = fileBuffer.toString("base64") const ext = path.extname(filePath).toLowerCase() @@ -62,7 +62,9 @@ async function readImageAsDataUrl(filePath: string): Promise { } const mimeType = mimeTypes[ext] || "image/png" - return `data:${mimeType};base64,${base64}` + const dataUrl = `data:${mimeType};base64,${base64}` + + return { dataUrl, buffer: fileBuffer } } export function getReadFileToolDescription(blockName: string, blockParams: any): string { @@ -492,14 +494,14 @@ export async function readFileTool( const supportedBinaryFormats = getSupportedBinaryFormats() // Check if it's a supported image format - if (SUPPORTED_IMAGE_FORMATS.includes(fileExtension as any)) { + if (SUPPORTED_IMAGE_FORMATS.includes(fileExtension as (typeof SUPPORTED_IMAGE_FORMATS)[number])) { try { const imageStats = await fs.stat(fullPath) // Check if image file exceeds size limit if (imageStats.size > MAX_IMAGE_FILE_SIZE_BYTES) { const imageSizeInMB = (imageStats.size / (1024 * 1024)).toFixed(1) - const notice = `Image file is too large (${imageSizeInMB} MB). The maximum allowed size is 5 MB.` + const notice = t("tools:readFile.imageTooLarge", { size: imageSizeInMB, max: 5 }) // Track file read await cline.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource) @@ -510,14 +512,13 @@ export async function readFileTool( continue } - const imageDataUrl = await readImageAsDataUrl(fullPath) + const { dataUrl: imageDataUrl, buffer } = await readImageAsDataUrlWithBuffer(fullPath) const imageSizeInKB = Math.round(imageStats.size / 1024) // For images, get dimensions if possible let dimensionsInfo = "" if (fileExtension === ".png") { // Simple PNG dimension extraction (first 24 bytes contain width/height) - const buffer = await fs.readFile(fullPath) if (buffer.length >= 24) { const width = buffer.readUInt32BE(16) const height = buffer.readUInt32BE(20) diff --git a/src/i18n/locales/ca/tools.json b/src/i18n/locales/ca/tools.json index 5b3a228bde..96b97bfa7b 100644 --- a/src/i18n/locales/ca/tools.json +++ b/src/i18n/locales/ca/tools.json @@ -2,7 +2,8 @@ "readFile": { "linesRange": " (línies {{start}}-{{end}})", "definitionsOnly": " (només definicions)", - "maxLines": " (màxim {{max}} línies)" + "maxLines": " (màxim {{max}} línies)", + "imageTooLarge": "El fitxer d'imatge és massa gran ({{size}} MB). La mida màxima permesa és {{max}} MB." }, "toolRepetitionLimitReached": "Roo sembla estar atrapat en un bucle, intentant la mateixa acció ({{toolName}}) repetidament. Això podria indicar un problema amb la seva estratègia actual. Considera reformular la tasca, proporcionar instruccions més específiques o guiar-lo cap a un enfocament diferent.", "codebaseSearch": { diff --git a/src/i18n/locales/de/tools.json b/src/i18n/locales/de/tools.json index eb1afbc082..19b700ee14 100644 --- a/src/i18n/locales/de/tools.json +++ b/src/i18n/locales/de/tools.json @@ -2,7 +2,8 @@ "readFile": { "linesRange": " (Zeilen {{start}}-{{end}})", "definitionsOnly": " (nur Definitionen)", - "maxLines": " (maximal {{max}} Zeilen)" + "maxLines": " (maximal {{max}} Zeilen)", + "imageTooLarge": "Die Bilddatei ist zu groß ({{size}} MB). Die maximal erlaubte Größe beträgt {{max}} MB." }, "toolRepetitionLimitReached": "Roo scheint in einer Schleife festzustecken und versucht wiederholt dieselbe Aktion ({{toolName}}). Dies könnte auf ein Problem mit der aktuellen Strategie hindeuten. Überlege dir, die Aufgabe umzuformulieren, genauere Anweisungen zu geben oder Roo zu einem anderen Ansatz zu führen.", "codebaseSearch": { diff --git a/src/i18n/locales/en/tools.json b/src/i18n/locales/en/tools.json index 0265a84398..fe9b6c299f 100644 --- a/src/i18n/locales/en/tools.json +++ b/src/i18n/locales/en/tools.json @@ -2,7 +2,8 @@ "readFile": { "linesRange": " (lines {{start}}-{{end}})", "definitionsOnly": " (definitions only)", - "maxLines": " (max {{max}} lines)" + "maxLines": " (max {{max}} lines)", + "imageTooLarge": "Image file is too large ({{size}} MB). The maximum allowed size is {{max}} MB." }, "toolRepetitionLimitReached": "Roo appears to be stuck in a loop, attempting the same action ({{toolName}}) repeatedly. This might indicate a problem with its current strategy. Consider rephrasing the task, providing more specific instructions, or guiding it towards a different approach.", "codebaseSearch": { diff --git a/src/i18n/locales/es/tools.json b/src/i18n/locales/es/tools.json index 303f5365ed..410e7e1148 100644 --- a/src/i18n/locales/es/tools.json +++ b/src/i18n/locales/es/tools.json @@ -2,7 +2,8 @@ "readFile": { "linesRange": " (líneas {{start}}-{{end}})", "definitionsOnly": " (solo definiciones)", - "maxLines": " (máximo {{max}} líneas)" + "maxLines": " (máximo {{max}} líneas)", + "imageTooLarge": "El archivo de imagen es demasiado grande ({{size}} MB). El tamaño máximo permitido es {{max}} MB." }, "toolRepetitionLimitReached": "Roo parece estar atrapado en un bucle, intentando la misma acción ({{toolName}}) repetidamente. Esto podría indicar un problema con su estrategia actual. Considera reformular la tarea, proporcionar instrucciones más específicas o guiarlo hacia un enfoque diferente.", "codebaseSearch": { diff --git a/src/i18n/locales/fr/tools.json b/src/i18n/locales/fr/tools.json index a6c71aca33..6e3f3f05ce 100644 --- a/src/i18n/locales/fr/tools.json +++ b/src/i18n/locales/fr/tools.json @@ -2,7 +2,8 @@ "readFile": { "linesRange": " (lignes {{start}}-{{end}})", "definitionsOnly": " (définitions uniquement)", - "maxLines": " (max {{max}} lignes)" + "maxLines": " (max {{max}} lignes)", + "imageTooLarge": "Le fichier image est trop volumineux ({{size}} MB). La taille maximale autorisée est {{max}} MB." }, "toolRepetitionLimitReached": "Roo semble être bloqué dans une boucle, tentant la même action ({{toolName}}) de façon répétée. Cela pourrait indiquer un problème avec sa stratégie actuelle. Envisage de reformuler la tâche, de fournir des instructions plus spécifiques ou de le guider vers une approche différente.", "codebaseSearch": { diff --git a/src/i18n/locales/hi/tools.json b/src/i18n/locales/hi/tools.json index 0cb4aeb14e..24df270e66 100644 --- a/src/i18n/locales/hi/tools.json +++ b/src/i18n/locales/hi/tools.json @@ -2,7 +2,8 @@ "readFile": { "linesRange": " (पंक्तियाँ {{start}}-{{end}})", "definitionsOnly": " (केवल परिभाषाएँ)", - "maxLines": " (अधिकतम {{max}} पंक्तियाँ)" + "maxLines": " (अधिकतम {{max}} पंक्तियाँ)", + "imageTooLarge": "छवि फ़ाइल बहुत बड़ी है ({{size}} MB)। अधिकतम अनुमतित आकार {{max}} MB है।" }, "toolRepetitionLimitReached": "Roo एक लूप में फंसा हुआ लगता है, बार-बार एक ही क्रिया ({{toolName}}) को दोहरा रहा है। यह उसकी वर्तमान रणनीति में किसी समस्या का संकेत हो सकता है। कार्य को पुनः परिभाषित करने, अधिक विशिष्ट निर्देश देने, या उसे एक अलग दृष्टिकोण की ओर मार्गदर्शित करने पर विचार करें।", "codebaseSearch": { diff --git a/src/i18n/locales/id/tools.json b/src/i18n/locales/id/tools.json index 2e3c4f0c22..6745bfd474 100644 --- a/src/i18n/locales/id/tools.json +++ b/src/i18n/locales/id/tools.json @@ -2,7 +2,8 @@ "readFile": { "linesRange": " (baris {{start}}-{{end}})", "definitionsOnly": " (hanya definisi)", - "maxLines": " (maks {{max}} baris)" + "maxLines": " (maks {{max}} baris)", + "imageTooLarge": "File gambar terlalu besar ({{size}} MB). Ukuran maksimum yang diizinkan adalah {{max}} MB." }, "toolRepetitionLimitReached": "Roo tampaknya terjebak dalam loop, mencoba aksi yang sama ({{toolName}}) berulang kali. Ini mungkin menunjukkan masalah dengan strategi saat ini. Pertimbangkan untuk mengubah frasa tugas, memberikan instruksi yang lebih spesifik, atau mengarahkannya ke pendekatan yang berbeda.", "codebaseSearch": { diff --git a/src/i18n/locales/it/tools.json b/src/i18n/locales/it/tools.json index ffae474f1d..cfb631db7b 100644 --- a/src/i18n/locales/it/tools.json +++ b/src/i18n/locales/it/tools.json @@ -2,7 +2,8 @@ "readFile": { "linesRange": " (righe {{start}}-{{end}})", "definitionsOnly": " (solo definizioni)", - "maxLines": " (max {{max}} righe)" + "maxLines": " (max {{max}} righe)", + "imageTooLarge": "Il file immagine è troppo grande ({{size}} MB). La dimensione massima consentita è {{max}} MB." }, "toolRepetitionLimitReached": "Roo sembra essere bloccato in un ciclo, tentando ripetutamente la stessa azione ({{toolName}}). Questo potrebbe indicare un problema con la sua strategia attuale. Considera di riformulare l'attività, fornire istruzioni più specifiche o guidarlo verso un approccio diverso.", "codebaseSearch": { diff --git a/src/i18n/locales/ja/tools.json b/src/i18n/locales/ja/tools.json index 04a5fcc085..c70b778594 100644 --- a/src/i18n/locales/ja/tools.json +++ b/src/i18n/locales/ja/tools.json @@ -2,7 +2,8 @@ "readFile": { "linesRange": " ({{start}}-{{end}}行目)", "definitionsOnly": " (定義のみ)", - "maxLines": " (最大{{max}}行)" + "maxLines": " (最大{{max}}行)", + "imageTooLarge": "画像ファイルが大きすぎます({{size}} MB)。最大許可サイズは {{max}} MB です。" }, "toolRepetitionLimitReached": "Rooが同じ操作({{toolName}})を繰り返し試みるループに陥っているようです。これは現在の方法に問題がある可能性を示しています。タスクの言い換え、より具体的な指示の提供、または別のアプローチへの誘導を検討してください。", "codebaseSearch": { diff --git a/src/i18n/locales/ko/tools.json b/src/i18n/locales/ko/tools.json index e43a541794..6126aad34a 100644 --- a/src/i18n/locales/ko/tools.json +++ b/src/i18n/locales/ko/tools.json @@ -2,7 +2,8 @@ "readFile": { "linesRange": " ({{start}}-{{end}}행)", "definitionsOnly": " (정의만)", - "maxLines": " (최대 {{max}}행)" + "maxLines": " (최대 {{max}}행)", + "imageTooLarge": "이미지 파일이 너무 큽니다 ({{size}} MB). 최대 허용 크기는 {{max}} MB입니다." }, "toolRepetitionLimitReached": "Roo가 같은 동작({{toolName}})을 반복적으로 시도하면서 루프에 갇힌 것 같습니다. 이는 현재 전략에 문제가 있을 수 있음을 나타냅니다. 작업을 다시 표현하거나, 더 구체적인 지침을 제공하거나, 다른 접근 방식으로 안내해 보세요.", "codebaseSearch": { diff --git a/src/i18n/locales/nl/tools.json b/src/i18n/locales/nl/tools.json index 56a8cdbc46..b36c727e07 100644 --- a/src/i18n/locales/nl/tools.json +++ b/src/i18n/locales/nl/tools.json @@ -2,7 +2,8 @@ "readFile": { "linesRange": " (regels {{start}}-{{end}})", "definitionsOnly": " (alleen definities)", - "maxLines": " (max {{max}} regels)" + "maxLines": " (max {{max}} regels)", + "imageTooLarge": "Afbeeldingsbestand is te groot ({{size}} MB). De maximaal toegestane grootte is {{max}} MB." }, "toolRepetitionLimitReached": "Roo lijkt vast te zitten in een lus, waarbij hij herhaaldelijk dezelfde actie ({{toolName}}) probeert. Dit kan duiden op een probleem met de huidige strategie. Overweeg de taak te herformuleren, specifiekere instructies te geven of Roo naar een andere aanpak te leiden.", "codebaseSearch": { diff --git a/src/i18n/locales/pl/tools.json b/src/i18n/locales/pl/tools.json index 62568826aa..0ddfa11f83 100644 --- a/src/i18n/locales/pl/tools.json +++ b/src/i18n/locales/pl/tools.json @@ -2,7 +2,8 @@ "readFile": { "linesRange": " (linie {{start}}-{{end}})", "definitionsOnly": " (tylko definicje)", - "maxLines": " (maks. {{max}} linii)" + "maxLines": " (maks. {{max}} linii)", + "imageTooLarge": "Plik obrazu jest zbyt duży ({{size}} MB). Maksymalny dozwolony rozmiar to {{max}} MB." }, "toolRepetitionLimitReached": "Wygląda na to, że Roo utknął w pętli, wielokrotnie próbując wykonać tę samą akcję ({{toolName}}). Może to wskazywać na problem z jego obecną strategią. Rozważ przeformułowanie zadania, podanie bardziej szczegółowych instrukcji lub nakierowanie go na inne podejście.", "codebaseSearch": { diff --git a/src/i18n/locales/pt-BR/tools.json b/src/i18n/locales/pt-BR/tools.json index f74e0f8196..3f4c21e0f5 100644 --- a/src/i18n/locales/pt-BR/tools.json +++ b/src/i18n/locales/pt-BR/tools.json @@ -2,7 +2,8 @@ "readFile": { "linesRange": " (linhas {{start}}-{{end}})", "definitionsOnly": " (apenas definições)", - "maxLines": " (máx. {{max}} linhas)" + "maxLines": " (máx. {{max}} linhas)", + "imageTooLarge": "Arquivo de imagem é muito grande ({{size}} MB). O tamanho máximo permitido é {{max}} MB." }, "toolRepetitionLimitReached": "Roo parece estar preso em um loop, tentando a mesma ação ({{toolName}}) repetidamente. Isso pode indicar um problema com sua estratégia atual. Considere reformular a tarefa, fornecer instruções mais específicas ou guiá-lo para uma abordagem diferente.", "codebaseSearch": { diff --git a/src/i18n/locales/ru/tools.json b/src/i18n/locales/ru/tools.json index 1e59d10499..7fff609459 100644 --- a/src/i18n/locales/ru/tools.json +++ b/src/i18n/locales/ru/tools.json @@ -2,7 +2,8 @@ "readFile": { "linesRange": " (строки {{start}}-{{end}})", "definitionsOnly": " (только определения)", - "maxLines": " (макс. {{max}} строк)" + "maxLines": " (макс. {{max}} строк)", + "imageTooLarge": "Файл изображения слишком большой ({{size}} МБ). Максимально допустимый размер {{max}} МБ." }, "toolRepetitionLimitReached": "Похоже, что Roo застрял в цикле, многократно пытаясь выполнить одно и то же действие ({{toolName}}). Это может указывать на проблему с его текущей стратегией. Попробуйте переформулировать задачу, предоставить более конкретные инструкции или направить его к другому подходу.", "codebaseSearch": { diff --git a/src/i18n/locales/tr/tools.json b/src/i18n/locales/tr/tools.json index e4c73cdc4b..3eccc1bb35 100644 --- a/src/i18n/locales/tr/tools.json +++ b/src/i18n/locales/tr/tools.json @@ -2,7 +2,8 @@ "readFile": { "linesRange": " (satır {{start}}-{{end}})", "definitionsOnly": " (sadece tanımlar)", - "maxLines": " (maks. {{max}} satır)" + "maxLines": " (maks. {{max}} satır)", + "imageTooLarge": "Görüntü dosyası çok büyük ({{size}} MB). İzin verilen maksimum boyut {{max}} MB." }, "toolRepetitionLimitReached": "Roo bir döngüye takılmış gibi görünüyor, aynı eylemi ({{toolName}}) tekrar tekrar deniyor. Bu, mevcut stratejisinde bir sorun olduğunu gösterebilir. Görevi yeniden ifade etmeyi, daha spesifik talimatlar vermeyi veya onu farklı bir yaklaşıma yönlendirmeyi düşünün.", "codebaseSearch": { diff --git a/src/i18n/locales/vi/tools.json b/src/i18n/locales/vi/tools.json index 9811ee12c9..9138594299 100644 --- a/src/i18n/locales/vi/tools.json +++ b/src/i18n/locales/vi/tools.json @@ -2,7 +2,8 @@ "readFile": { "linesRange": " (dòng {{start}}-{{end}})", "definitionsOnly": " (chỉ định nghĩa)", - "maxLines": " (tối đa {{max}} dòng)" + "maxLines": " (tối đa {{max}} dòng)", + "imageTooLarge": "Tệp hình ảnh quá lớn ({{size}} MB). Kích thước tối đa cho phép là {{max}} MB." }, "toolRepetitionLimitReached": "Roo dường như đang bị mắc kẹt trong một vòng lặp, liên tục cố gắng thực hiện cùng một hành động ({{toolName}}). Điều này có thể cho thấy vấn đề với chiến lược hiện tại. Hãy cân nhắc việc diễn đạt lại nhiệm vụ, cung cấp hướng dẫn cụ thể hơn, hoặc hướng Roo theo một cách tiếp cận khác.", "codebaseSearch": { diff --git a/src/i18n/locales/zh-CN/tools.json b/src/i18n/locales/zh-CN/tools.json index 13641b8d43..d59f89ddab 100644 --- a/src/i18n/locales/zh-CN/tools.json +++ b/src/i18n/locales/zh-CN/tools.json @@ -2,7 +2,8 @@ "readFile": { "linesRange": " (第 {{start}}-{{end}} 行)", "definitionsOnly": " (仅定义)", - "maxLines": " (最多 {{max}} 行)" + "maxLines": " (最多 {{max}} 行)", + "imageTooLarge": "图片文件过大 ({{size}} MB)。允许的最大大小为 {{max}} MB。" }, "toolRepetitionLimitReached": "Roo 似乎陷入循环,反复尝试同一操作 ({{toolName}})。这可能表明当前策略存在问题。请考虑重新描述任务、提供更具体的指示或引导其尝试不同的方法。", "codebaseSearch": { diff --git a/src/i18n/locales/zh-TW/tools.json b/src/i18n/locales/zh-TW/tools.json index a726e3c919..0d985e7dfb 100644 --- a/src/i18n/locales/zh-TW/tools.json +++ b/src/i18n/locales/zh-TW/tools.json @@ -2,7 +2,8 @@ "readFile": { "linesRange": " (第 {{start}}-{{end}} 行)", "definitionsOnly": " (僅定義)", - "maxLines": " (最多 {{max}} 行)" + "maxLines": " (最多 {{max}} 行)", + "imageTooLarge": "圖片檔案過大 ({{size}} MB)。允許的最大大小為 {{max}} MB。" }, "toolRepetitionLimitReached": "Roo 似乎陷入循環,反覆嘗試同一操作 ({{toolName}})。這可能表明目前策略存在問題。請考慮重新描述工作、提供更具體的指示或引導其嘗試不同的方法。", "codebaseSearch": { From 28e0b28c3e08d1a493646421fa1bb92ace07ab95 Mon Sep 17 00:00:00 2001 From: Sam Hoang Date: Thu, 3 Jul 2025 16:06:26 +0700 Subject: [PATCH 06/21] add setting ui for max image file size --- packages/types/src/global-settings.ts | 1 + src/core/tools/readFileTool.ts | 10 +++--- src/core/webview/ClineProvider.ts | 3 ++ .../webview/__tests__/ClineProvider.spec.ts | 1 + src/core/webview/webviewMessageHandler.ts | 4 +++ src/shared/ExtensionMessage.ts | 1 + src/shared/WebviewMessage.ts | 1 + .../settings/ContextManagementSettings.tsx | 31 +++++++++++++++++++ .../src/components/settings/SettingsView.tsx | 3 ++ .../src/context/ExtensionStateContext.tsx | 4 +++ .../__tests__/ExtensionStateContext.spec.tsx | 1 + webview-ui/src/i18n/locales/ca/settings.json | 5 +++ webview-ui/src/i18n/locales/de/settings.json | 5 +++ webview-ui/src/i18n/locales/en/settings.json | 5 +++ webview-ui/src/i18n/locales/es/settings.json | 5 +++ webview-ui/src/i18n/locales/fr/settings.json | 5 +++ webview-ui/src/i18n/locales/hi/settings.json | 5 +++ webview-ui/src/i18n/locales/id/settings.json | 5 +++ webview-ui/src/i18n/locales/it/settings.json | 5 +++ webview-ui/src/i18n/locales/ja/settings.json | 5 +++ webview-ui/src/i18n/locales/ko/settings.json | 5 +++ webview-ui/src/i18n/locales/nl/settings.json | 5 +++ webview-ui/src/i18n/locales/pl/settings.json | 5 +++ .../src/i18n/locales/pt-BR/settings.json | 5 +++ webview-ui/src/i18n/locales/ru/settings.json | 5 +++ webview-ui/src/i18n/locales/tr/settings.json | 5 +++ webview-ui/src/i18n/locales/vi/settings.json | 5 +++ .../src/i18n/locales/zh-CN/settings.json | 5 +++ .../src/i18n/locales/zh-TW/settings.json | 5 +++ 29 files changed, 145 insertions(+), 5 deletions(-) diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index d5e76eccea..734cc36c9d 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -101,6 +101,7 @@ export const globalSettingsSchema = z.object({ maxWorkspaceFiles: z.number().optional(), showRooIgnoredFiles: z.boolean().optional(), maxReadFileLine: z.number().optional(), + maxImageFileSize: z.number().optional(), terminalOutputLineLimit: z.number().optional(), terminalOutputCharacterLimit: z.number().optional(), diff --git a/src/core/tools/readFileTool.ts b/src/core/tools/readFileTool.ts index 0adadc27f8..c51f9649ee 100644 --- a/src/core/tools/readFileTool.ts +++ b/src/core/tools/readFileTool.ts @@ -17,9 +17,9 @@ import { parseXml } from "../../utils/xml" import * as fs from "fs/promises" /** - * Maximum allowed image file size in bytes (5MB) + * Default maximum allowed image file size in bytes (5MB) */ -const MAX_IMAGE_FILE_SIZE_BYTES = 5 * 1024 * 1024 +const DEFAULT_MAX_IMAGE_FILE_SIZE_MB = 5 /** * Supported image formats that can be displayed @@ -482,7 +482,7 @@ export async function readFileTool( const relPath = fileResult.path const fullPath = path.resolve(cline.cwd, relPath) - const { maxReadFileLine = -1 } = (await cline.providerRef.deref()?.getState()) ?? {} + const { maxReadFileLine = -1, maxImageFileSize = DEFAULT_MAX_IMAGE_FILE_SIZE_MB } = (await cline.providerRef.deref()?.getState()) ?? {} // Process approved files try { @@ -499,9 +499,9 @@ export async function readFileTool( const imageStats = await fs.stat(fullPath) // Check if image file exceeds size limit - if (imageStats.size > MAX_IMAGE_FILE_SIZE_BYTES) { + if (imageStats.size > maxImageFileSize * 1024 * 1024) { const imageSizeInMB = (imageStats.size / (1024 * 1024)).toFixed(1) - const notice = t("tools:readFile.imageTooLarge", { size: imageSizeInMB, max: 5 }) + const notice = t("tools:readFile.imageTooLarge", { size: imageSizeInMB, max: maxImageFileSize }) // Track file read await cline.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 905e657b37..2efb6d96e2 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1425,6 +1425,7 @@ export class ClineProvider showRooIgnoredFiles, language, maxReadFileLine, + maxImageFileSize, terminalCompressProgressBar, historyPreviewCollapsed, cloudUserInfo, @@ -1532,6 +1533,7 @@ export class ClineProvider language: language ?? formatLanguage(vscode.env.language), renderContext: this.renderContext, maxReadFileLine: maxReadFileLine ?? -1, + maxImageFileSize: maxImageFileSize ?? 5, maxConcurrentFileReads: maxConcurrentFileReads ?? 5, settingsImportedAt: this.settingsImportedAt, terminalCompressProgressBar: terminalCompressProgressBar ?? true, @@ -1702,6 +1704,7 @@ export class ClineProvider telemetrySetting: stateValues.telemetrySetting || "unset", showRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? true, maxReadFileLine: stateValues.maxReadFileLine ?? -1, + maxImageFileSize: stateValues.maxImageFileSize ?? 5, maxConcurrentFileReads: stateValues.maxConcurrentFileReads ?? 5, historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false, cloudUserInfo, diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 344b098816..b3991d2973 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -533,6 +533,7 @@ describe("ClineProvider", () => { showRooIgnoredFiles: true, renderContext: "sidebar", maxReadFileLine: 500, + maxImageFileSize: 5, cloudUserInfo: null, organizationAllowList: ORGANIZATION_ALLOW_ALL, autoCondenseContext: true, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index c739c2ade8..2e1d848642 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1265,6 +1265,10 @@ export const webviewMessageHandler = async ( await updateGlobalState("maxReadFileLine", message.value) await provider.postStateToWebview() break + case "maxImageFileSize": + await updateGlobalState("maxImageFileSize", message.value) + await provider.postStateToWebview() + break case "maxConcurrentFileReads": const valueToSave = message.value // Capture the value intended for saving await updateGlobalState("maxConcurrentFileReads", valueToSave) diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 000762e317..49151a7e96 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -278,6 +278,7 @@ export type ExtensionState = Pick< maxWorkspaceFiles: number // Maximum number of files to include in current working directory details (0-500) showRooIgnoredFiles: boolean // Whether to show .rooignore'd files in listings maxReadFileLine: number // Maximum number of lines to read from a file before truncating + maxImageFileSize: number // Maximum size of image files to process in MB experiments: Experiments // Map of experiment IDs to their enabled state diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 795e276522..2ea42eb75e 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -162,6 +162,7 @@ export interface WebviewMessage { | "remoteBrowserEnabled" | "language" | "maxReadFileLine" + | "maxImageFileSize" | "maxConcurrentFileReads" | "includeDiagnosticMessages" | "maxDiagnosticMessages" diff --git a/webview-ui/src/components/settings/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx index 4530fdb1ba..b21ad8258a 100644 --- a/webview-ui/src/components/settings/ContextManagementSettings.tsx +++ b/webview-ui/src/components/settings/ContextManagementSettings.tsx @@ -20,6 +20,7 @@ type ContextManagementSettingsProps = HTMLAttributes & { maxWorkspaceFiles: number showRooIgnoredFiles?: boolean maxReadFileLine?: number + maxImageFileSize?: number maxConcurrentFileReads?: number profileThresholds?: Record includeDiagnosticMessages?: boolean @@ -32,6 +33,7 @@ type ContextManagementSettingsProps = HTMLAttributes & { | "maxWorkspaceFiles" | "showRooIgnoredFiles" | "maxReadFileLine" + | "maxImageFileSize" | "maxConcurrentFileReads" | "profileThresholds" | "includeDiagnosticMessages" @@ -49,6 +51,7 @@ export const ContextManagementSettings = ({ showRooIgnoredFiles, setCachedStateField, maxReadFileLine, + maxImageFileSize, maxConcurrentFileReads, profileThresholds = {}, includeDiagnosticMessages, @@ -206,6 +209,34 @@ export const ContextManagementSettings = ({ +
+
+ {t("settings:contextManagement.maxImageFileSize.label")} +
+ { + const newValue = parseInt(e.target.value, 10) + if (!isNaN(newValue) && newValue >= 1 && newValue <= 100) { + setCachedStateField("maxImageFileSize", newValue) + } + }} + onClick={(e) => e.currentTarget.select()} + data-testid="max-image-file-size-input" + /> + {t("settings:contextManagement.maxImageFileSize.mb")} +
+
+
+ {t("settings:contextManagement.maxImageFileSize.description")} +
+
+
(({ onDone, t showRooIgnoredFiles, remoteBrowserEnabled, maxReadFileLine, + maxImageFileSize, terminalCompressProgressBar, maxConcurrentFileReads, condensingApiConfigId, @@ -321,6 +322,7 @@ const SettingsView = forwardRef(({ onDone, t vscode.postMessage({ type: "maxWorkspaceFiles", value: maxWorkspaceFiles ?? 200 }) vscode.postMessage({ type: "showRooIgnoredFiles", bool: showRooIgnoredFiles }) vscode.postMessage({ type: "maxReadFileLine", value: maxReadFileLine ?? -1 }) + vscode.postMessage({ type: "maxImageFileSize", value: maxImageFileSize ?? 5 }) vscode.postMessage({ type: "maxConcurrentFileReads", value: cachedState.maxConcurrentFileReads ?? 5 }) vscode.postMessage({ type: "includeDiagnosticMessages", bool: includeDiagnosticMessages }) vscode.postMessage({ type: "maxDiagnosticMessages", value: maxDiagnosticMessages ?? 50 }) @@ -667,6 +669,7 @@ const SettingsView = forwardRef(({ onDone, t maxWorkspaceFiles={maxWorkspaceFiles ?? 200} showRooIgnoredFiles={showRooIgnoredFiles} maxReadFileLine={maxReadFileLine} + maxImageFileSize={maxImageFileSize} maxConcurrentFileReads={maxConcurrentFileReads} profileThresholds={profileThresholds} includeDiagnosticMessages={includeDiagnosticMessages} diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index ff1ce31c53..3d117aa7f6 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -120,6 +120,8 @@ export interface ExtensionStateContextType extends ExtensionState { setAwsUsePromptCache: (value: boolean) => void maxReadFileLine: number setMaxReadFileLine: (value: number) => void + maxImageFileSize: number + setMaxImageFileSize: (value: number) => void machineId?: string pinnedApiConfigs?: Record setPinnedApiConfigs: (value: Record) => void @@ -208,6 +210,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode showRooIgnoredFiles: true, // Default to showing .rooignore'd files with lock symbol (current behavior). renderContext: "sidebar", maxReadFileLine: -1, // Default max read file line limit + maxImageFileSize: 5, // Default max image file size in MB pinnedApiConfigs: {}, // Empty object for pinned API configs terminalZshOhMy: false, // Default Oh My Zsh integration setting maxConcurrentFileReads: 5, // Default concurrent file reads @@ -448,6 +451,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setRemoteBrowserEnabled: (value) => setState((prevState) => ({ ...prevState, remoteBrowserEnabled: value })), setAwsUsePromptCache: (value) => setState((prevState) => ({ ...prevState, awsUsePromptCache: value })), setMaxReadFileLine: (value) => setState((prevState) => ({ ...prevState, maxReadFileLine: value })), + setMaxImageFileSize: (value) => setState((prevState) => ({ ...prevState, maxImageFileSize: value })), setPinnedApiConfigs: (value) => setState((prevState) => ({ ...prevState, pinnedApiConfigs: value })), setTerminalCompressProgressBar: (value) => setState((prevState) => ({ ...prevState, terminalCompressProgressBar: value })), diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx index 1e5867d3fc..23f3940941 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx @@ -209,6 +209,7 @@ describe("mergeExtensionState", () => { sharingEnabled: false, profileThresholds: {}, hasOpenedModeSelector: false, // Add the new required property + maxImageFileSize: 5 } const prevState: ExtensionState = { diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index ab929d724d..8f7db4cac2 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -526,6 +526,11 @@ "profileDescription": "Llindar personalitzat només per a aquest perfil (substitueix el per defecte global)", "inheritDescription": "Aquest perfil hereta el llindar per defecte global ({{threshold}}%)", "usesGlobal": "(utilitza global {{threshold}}%)" + }, + "maxImageFileSize": { + "label": "Mida màxima d'arxiu d'imatge", + "mb": "MB", + "description": "Mida màxima (en MB) per a arxius d'imatge que poden ser processats per l'eina de lectura d'arxius." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 766bc891e4..1937cb053c 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -526,6 +526,11 @@ "profileDescription": "Benutzerdefinierter Schwellenwert nur für dieses Profil (überschreibt globalen Standard)", "inheritDescription": "Dieses Profil erbt den globalen Standard-Schwellenwert ({{threshold}}%)", "usesGlobal": "(verwendet global {{threshold}}%)" + }, + "maxImageFileSize": { + "label": "Maximale Bilddateigröße", + "mb": "MB", + "description": "Maximale Größe (in MB) für Bilddateien, die vom read file Tool verarbeitet werden können." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index cfd5b04286..c8ad99c62d 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -502,6 +502,11 @@ "lines": "lines", "always_full_read": "Always read entire file" }, + "maxImageFileSize": { + "label": "Max image file size", + "mb": "MB", + "description": "Maximum size (in MB) for image files that can be processed by the read file tool." + }, "diagnostics": { "includeMessages": { "label": "Automatically include diagnostics in context", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index f536863909..e54d770991 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -502,6 +502,11 @@ "label": "Límite de lecturas simultáneas", "description": "Número máximo de archivos que la herramienta 'read_file' puede procesar simultáneamente. Valores más altos pueden acelerar la lectura de múltiples archivos pequeños pero aumentan el uso de memoria." }, + "maxImageFileSize": { + "label": "Tamaño máximo de archivo de imagen", + "mb": "MB", + "description": "Tamaño máximo (en MB) para archivos de imagen que pueden ser procesados por la herramienta de lectura de archivos." + }, "diagnostics": { "includeMessages": { "label": "Incluir automáticamente diagnósticos en el contexto", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 0e12c58d38..54a1092c2f 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -502,6 +502,11 @@ "label": "Limite de lectures simultanées", "description": "Nombre maximum de fichiers que l'outil 'read_file' peut traiter simultanément. Des valeurs plus élevées peuvent accélérer la lecture de plusieurs petits fichiers mais augmentent l'utilisation de la mémoire." }, + "maxImageFileSize": { + "label": "Taille maximale des fichiers d'image", + "mb": "MB", + "description": "Taille maximale (en MB) pour les fichiers d'image qui peuvent être traités par l'outil de lecture de fichier." + }, "diagnostics": { "includeMessages": { "label": "Inclure automatiquement les diagnostics dans le contexte", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index d2cfa971ff..77252093ae 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -527,6 +527,11 @@ "profileDescription": "केवल इस प्रोफ़ाइल के लिए कस्टम सीमा (वैश्विक डिफ़ॉल्ट को ओवरराइड करता है)", "inheritDescription": "यह प्रोफ़ाइल वैश्विक डिफ़ॉल्ट सीमा को इनहेरिट करता है ({{threshold}}%)", "usesGlobal": "(वैश्विक {{threshold}}% का उपयोग करता है)" + }, + "maxImageFileSize": { + "label": "अधिकतम छवि फ़ाइल आकार", + "mb": "MB", + "description": "छवि फ़ाइलों के लिए अधिकतम आकार (MB में) जो read file tool द्वारा प्रसंस्कृत किया जा सकता है।" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 3f362f650d..8c6fd70432 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -531,6 +531,11 @@ "description": "Roo membaca sejumlah baris ini ketika model menghilangkan nilai start/end. Jika angka ini kurang dari total file, Roo menghasilkan indeks nomor baris dari definisi kode. Kasus khusus: -1 menginstruksikan Roo untuk membaca seluruh file (tanpa indexing), dan 0 menginstruksikannya untuk tidak membaca baris dan hanya menyediakan indeks baris untuk konteks minimal. Nilai yang lebih rendah meminimalkan penggunaan konteks awal, memungkinkan pembacaan rentang baris yang tepat selanjutnya. Permintaan start/end eksplisit tidak dibatasi oleh pengaturan ini.", "lines": "baris", "always_full_read": "Selalu baca seluruh file" + }, + "maxImageFileSize": { + "label": "Ukuran file gambar maksimum", + "mb": "MB", + "description": "Ukuran maksimum (dalam MB) untuk file gambar yang dapat diproses oleh alat baca file." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 14deda20ea..54b29140ea 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -527,6 +527,11 @@ "profileDescription": "Soglia personalizzata solo per questo profilo (sovrascrive il predefinito globale)", "inheritDescription": "Questo profilo eredita la soglia predefinita globale ({{threshold}}%)", "usesGlobal": "(usa globale {{threshold}}%)" + }, + "maxImageFileSize": { + "label": "Dimensione massima file immagine", + "mb": "MB", + "description": "Dimensione massima (in MB) per i file immagine che possono essere elaborati dallo strumento di lettura file." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index d7864e40db..e77b8f9d23 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -527,6 +527,11 @@ "profileDescription": "このプロファイルのみのカスタムしきい値(グローバルデフォルトを上書き)", "inheritDescription": "このプロファイルはグローバルデフォルトしきい値を継承します({{threshold}}%)", "usesGlobal": "(グローバル {{threshold}}% を使用)" + }, + "maxImageFileSize": { + "label": "最大画像ファイルサイズ", + "mb": "MB", + "description": "read fileツールで処理できる画像ファイルの最大サイズ(MB単位)。" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index adbe216463..d2ee15b67f 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -527,6 +527,11 @@ "profileDescription": "이 프로필만을 위한 사용자 정의 임계값 (글로벌 기본값 재정의)", "inheritDescription": "이 프로필은 글로벌 기본 임계값을 상속합니다 ({{threshold}}%)", "usesGlobal": "(글로벌 {{threshold}}% 사용)" + }, + "maxImageFileSize": { + "label": "최대 이미지 파일 크기", + "mb": "MB", + "description": "read file 도구로 처리할 수 있는 이미지 파일의 최대 크기(MB 단위)입니다." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 6085be8fb1..3dd0806cbd 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -502,6 +502,11 @@ "label": "Limiet gelijktijdige bestandslezingen", "description": "Maximum aantal bestanden dat de 'read_file' tool tegelijkertijd kan verwerken. Hogere waarden kunnen het lezen van meerdere kleine bestanden versnellen maar verhogen het geheugengebruik." }, + "maxImageFileSize": { + "label": "Maximum afbeeldingsbestandsgrootte", + "mb": "MB", + "description": "Maximale grootte (in MB) voor afbeeldingsbestanden die kunnen worden verwerkt door de read file tool." + }, "diagnostics": { "includeMessages": { "label": "Automatisch diagnostiek opnemen in context", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 9107fd959e..37586b2ebc 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -527,6 +527,11 @@ "profileDescription": "Niestandardowy próg tylko dla tego profilu (zastępuje globalny domyślny)", "inheritDescription": "Ten profil dziedziczy globalny domyślny próg ({{threshold}}%)", "usesGlobal": "(używa globalnego {{threshold}}%)" + }, + "maxImageFileSize": { + "label": "Maksymalny rozmiar pliku obrazu", + "mb": "MB", + "description": "Maksymalny rozmiar (w MB) plików obrazów, które mogą być przetwarzane przez narzędzie do czytania plików." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 8137d917ae..46cd6cfef3 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -527,6 +527,11 @@ "profileDescription": "Limite personalizado apenas para este perfil (substitui o padrão global)", "inheritDescription": "Este perfil herda o limite padrão global ({{threshold}}%)", "usesGlobal": "(usa global {{threshold}}%)" + }, + "maxImageFileSize": { + "label": "Tamanho máximo do arquivo de imagem", + "mb": "MB", + "description": "Tamanho máximo (em MB) para arquivos de imagem que podem ser processados pela ferramenta de leitura de arquivos." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index e93f24e9d7..b9163e910c 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -527,6 +527,11 @@ "profileDescription": "Пользовательский порог только для этого профиля (переопределяет глобальный по умолчанию)", "inheritDescription": "Этот профиль наследует глобальный порог по умолчанию ({{threshold}}%)", "usesGlobal": "(использует глобальный {{threshold}}%)" + }, + "maxImageFileSize": { + "label": "Максимальный размер файла изображения", + "mb": "MB", + "description": "Максимальный размер (в МБ) для файлов изображений, которые могут быть обработаны инструментом чтения файлов." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 450738183b..db0e45a835 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -527,6 +527,11 @@ "profileDescription": "Sadece bu profil için özel eşik (küresel varsayılanı geçersiz kılar)", "inheritDescription": "Bu profil küresel varsayılan eşiği miras alır ({{threshold}}%)", "usesGlobal": "(küresel {{threshold}}% kullanır)" + }, + "maxImageFileSize": { + "label": "Maksimum görüntü dosyası boyutu", + "mb": "MB", + "description": "Dosya okuma aracı tarafından işlenebilecek görüntü dosyaları için maksimum boyut (MB cinsinden)." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 87e99ce6f5..2116950fd5 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -527,6 +527,11 @@ "profileDescription": "Ngưỡng tùy chỉnh chỉ cho hồ sơ này (ghi đè mặc định toàn cục)", "inheritDescription": "Hồ sơ này kế thừa ngưỡng mặc định toàn cục ({{threshold}}%)", "usesGlobal": "(sử dụng toàn cục {{threshold}}%)" + }, + "maxImageFileSize": { + "label": "Kích thước tối đa của tệp hình ảnh", + "mb": "MB", + "description": "Kích thước tối đa (tính bằng MB) cho các tệp hình ảnh có thể được xử lý bởi công cụ đọc tệp." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index e94c857e65..b11202e271 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -527,6 +527,11 @@ "profileDescription": "仅此配置文件的自定义阈值(覆盖全局默认)", "inheritDescription": "此配置文件继承全局默认阈值({{threshold}}%)", "usesGlobal": "(使用全局 {{threshold}}%)" + }, + "maxImageFileSize": { + "label": "最大图像文件大小", + "mb": "MB", + "description": "read file工具可以处理的图像文件的最大大小(以MB为单位)。" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index fa10cf3028..023ade21ff 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -527,6 +527,11 @@ "profileDescription": "僅此檔案的自訂閾值(覆蓋全域預設)", "inheritDescription": "此檔案繼承全域預設閾值({{threshold}}%)", "usesGlobal": "(使用全域 {{threshold}}%)" + }, + "maxImageFileSize": { + "label": "最大圖像檔案大小", + "mb": "MB", + "description": "read file工具可以處理的圖像檔案的最大大小(以MB為單位)。" } }, "terminal": { From c165fa4d6c60fc7d49b5deb2d39d2e914587d1af Mon Sep 17 00:00:00 2001 From: Sam Hoang Date: Sat, 5 Jul 2025 23:20:56 +0700 Subject: [PATCH 07/21] handle model don support image --- src/core/tools/__tests__/readFileTool.spec.ts | 81 +++++++++++++++++++ src/core/tools/readFileTool.ts | 8 +- 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/src/core/tools/__tests__/readFileTool.spec.ts b/src/core/tools/__tests__/readFileTool.spec.ts index cbd9879fb5..5f825d8c40 100644 --- a/src/core/tools/__tests__/readFileTool.spec.ts +++ b/src/core/tools/__tests__/readFileTool.spec.ts @@ -181,6 +181,13 @@ describe("read_file tool with maxReadFileLine setting", () => { mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) mockCline.recordToolError = vi.fn().mockReturnValue(undefined) + // Setup default API handler that supports images + mockCline.api = { + getModel: vi.fn().mockReturnValue({ + info: { supportsImages: true } + }) + } + toolResult = undefined }) @@ -437,6 +444,13 @@ describe("read_file tool XML output structure", () => { mockCline.recordToolError = vi.fn().mockReturnValue(undefined) mockCline.didRejectTool = false + // Mock the API handler - required for image support check + mockCline.api = { + getModel: vi.fn().mockReturnValue({ + info: { supportsImages: true } + }) + } + toolResult = undefined }) @@ -625,6 +639,13 @@ describe("read_file tool with image support", () => { mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) mockCline.recordToolError = vi.fn().mockReturnValue(undefined) + // Setup default API handler that supports images + mockCline.api = { + getModel: vi.fn().mockReturnValue({ + info: { supportsImages: true } + }) + } + toolResult = undefined }) @@ -779,6 +800,66 @@ describe("read_file tool with image support", () => { expect(imagePart.source.data).toBe(largeBase64) }) + it("should exclude images when model does not support images", async () => { + // Setup - mock API handler that doesn't support images + mockCline.api = { + getModel: vi.fn().mockReturnValue({ + info: { supportsImages: false } + }) + } + + // Execute + const result = await executeReadImageTool() + + // When images are not supported, the tool should return just XML (not call formatResponse.toolResult) + expect(toolResultMock).not.toHaveBeenCalled() + expect(typeof result).toBe("string") + expect(result).toContain(`${testImagePath}`) + expect(result).toContain(`Image file`) + }) + + it("should include images when model supports images", async () => { + // Setup - mock API handler that supports images + mockCline.api = { + getModel: vi.fn().mockReturnValue({ + info: { supportsImages: true } + }) + } + + // Execute + const result = await executeReadImageTool() + + // Verify toolResultMock was called with images + expect(toolResultMock).toHaveBeenCalledTimes(1) + const callArgs = toolResultMock.mock.calls[0] + const textArg = callArgs[0] + const imagesArg = callArgs[1] + + expect(textArg).toContain(`${testImagePath}`) + expect(imagesArg).toBeDefined() // Images should be included + expect(imagesArg).toBeInstanceOf(Array) + expect(imagesArg!.length).toBe(1) + expect(imagesArg![0]).toBe(`data:image/png;base64,${base64ImageData}`) + }) + + it("should handle undefined supportsImages gracefully", async () => { + // Setup - mock API handler with undefined supportsImages + mockCline.api = { + getModel: vi.fn().mockReturnValue({ + info: { supportsImages: undefined } + }) + } + + // Execute + const result = await executeReadImageTool() + + // When supportsImages is undefined, should default to false and return just XML + expect(toolResultMock).not.toHaveBeenCalled() + expect(typeof result).toBe("string") + expect(result).toContain(`${testImagePath}`) + expect(result).toContain(`Image file`) + }) + it("should handle errors when reading image files", async () => { // Setup - simulate read error mockedFsReadFile.mockRejectedValue(new Error("Failed to read image")) diff --git a/src/core/tools/readFileTool.ts b/src/core/tools/readFileTool.ts index c51f9649ee..d25e8de00b 100644 --- a/src/core/tools/readFileTool.ts +++ b/src/core/tools/readFileTool.ts @@ -702,12 +702,16 @@ export async function readFileTool( // Combine all images: feedback images first, then file images const allImages = [...feedbackImages, ...fileImageUrls] + // Check if the current model supports images before including them + const supportsImages = cline.api.getModel().info.supportsImages ?? false + const imagesToInclude = supportsImages ? allImages : [] + // Push the result with appropriate formatting - if (statusMessage || allImages.length > 0) { + if (statusMessage || imagesToInclude.length > 0) { // Always use formatResponse.toolResult when we have a status message or images const result = formatResponse.toolResult( statusMessage || filesXml, - allImages.length > 0 ? allImages : undefined, + imagesToInclude.length > 0 ? imagesToInclude : undefined, ) // Handle different return types from toolResult From 822a84f36a2a8436de4c219c5a9ead0b03c7aebd Mon Sep 17 00:00:00 2001 From: Sam Hoang Date: Sun, 6 Jul 2025 09:49:08 +0700 Subject: [PATCH 08/21] add i18n --- src/core/tools/__tests__/readFileTool.spec.ts | 26 +++++++++++++++++++ src/core/tools/readFileTool.ts | 4 +-- src/i18n/locales/ca/tools.json | 4 ++- src/i18n/locales/de/tools.json | 4 ++- src/i18n/locales/en/tools.json | 4 ++- src/i18n/locales/es/tools.json | 4 ++- src/i18n/locales/fr/tools.json | 4 ++- src/i18n/locales/hi/tools.json | 4 ++- src/i18n/locales/id/tools.json | 4 ++- src/i18n/locales/it/tools.json | 4 ++- src/i18n/locales/ja/tools.json | 4 ++- src/i18n/locales/ko/tools.json | 4 ++- src/i18n/locales/nl/tools.json | 4 ++- src/i18n/locales/pl/tools.json | 4 ++- src/i18n/locales/pt-BR/tools.json | 4 ++- src/i18n/locales/ru/tools.json | 4 ++- src/i18n/locales/tr/tools.json | 4 ++- src/i18n/locales/vi/tools.json | 4 ++- src/i18n/locales/zh-CN/tools.json | 4 ++- src/i18n/locales/zh-TW/tools.json | 4 ++- 20 files changed, 82 insertions(+), 20 deletions(-) diff --git a/src/core/tools/__tests__/readFileTool.spec.ts b/src/core/tools/__tests__/readFileTool.spec.ts index 5f825d8c40..b326e1663b 100644 --- a/src/core/tools/__tests__/readFileTool.spec.ts +++ b/src/core/tools/__tests__/readFileTool.spec.ts @@ -117,6 +117,32 @@ vi.mock("../../../utils/fs", () => ({ fileExistsAtPath: vi.fn().mockReturnValue(true), })) +// Mock i18n translation function +vi.mock("../../../i18n", () => ({ + t: vi.fn((key: string, params?: Record) => { + // Map translation keys to English text + const translations: Record = { + "tools:readFile.imageWithDimensions": "Image file ({{dimensions}}, {{size}} KB)", + "tools:readFile.imageWithSize": "Image file ({{size}} KB)", + "tools:readFile.imageTooLarge": "Image file is too large ({{size}} MB). The maximum allowed size is {{max}} MB.", + "tools:readFile.linesRange": " (lines {{start}}-{{end}})", + "tools:readFile.definitionsOnly": " (definitions only)", + "tools:readFile.maxLines": " (max {{max}} lines)", + } + + let result = translations[key] || key + + // Simple template replacement + if (params) { + Object.entries(params).forEach(([param, value]) => { + result = result.replace(new RegExp(`{{${param}}}`, 'g'), String(value)) + }) + } + + return result + }), +})) + describe("read_file tool with maxReadFileLine setting", () => { // Test data const testFilePath = "test/file.txt" diff --git a/src/core/tools/readFileTool.ts b/src/core/tools/readFileTool.ts index d25e8de00b..aa6b3c06c7 100644 --- a/src/core/tools/readFileTool.ts +++ b/src/core/tools/readFileTool.ts @@ -533,8 +533,8 @@ export async function readFileTool( // Store image data URL separately - NOT in XML const noticeText = dimensionsInfo - ? `Image file (${dimensionsInfo}, ${imageSizeInKB} KB)` - : `Image file (${imageSizeInKB} KB)` + ? t("tools:readFile.imageWithDimensions", { dimensions: dimensionsInfo, size: imageSizeInKB }) + : t("tools:readFile.imageWithSize", { size: imageSizeInKB }) updateFileResult(relPath, { xmlContent: `${relPath}\n${noticeText}\n`, diff --git a/src/i18n/locales/ca/tools.json b/src/i18n/locales/ca/tools.json index 96b97bfa7b..a3ec48ec42 100644 --- a/src/i18n/locales/ca/tools.json +++ b/src/i18n/locales/ca/tools.json @@ -3,7 +3,9 @@ "linesRange": " (línies {{start}}-{{end}})", "definitionsOnly": " (només definicions)", "maxLines": " (màxim {{max}} línies)", - "imageTooLarge": "El fitxer d'imatge és massa gran ({{size}} MB). La mida màxima permesa és {{max}} MB." + "imageTooLarge": "El fitxer d'imatge és massa gran ({{size}} MB). La mida màxima permesa és {{max}} MB.", + "imageWithDimensions": "Fitxer d'imatge ({{dimensions}}, {{size}} KB)", + "imageWithSize": "Fitxer d'imatge ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo sembla estar atrapat en un bucle, intentant la mateixa acció ({{toolName}}) repetidament. Això podria indicar un problema amb la seva estratègia actual. Considera reformular la tasca, proporcionar instruccions més específiques o guiar-lo cap a un enfocament diferent.", "codebaseSearch": { diff --git a/src/i18n/locales/de/tools.json b/src/i18n/locales/de/tools.json index 19b700ee14..9dd72ddf6d 100644 --- a/src/i18n/locales/de/tools.json +++ b/src/i18n/locales/de/tools.json @@ -3,7 +3,9 @@ "linesRange": " (Zeilen {{start}}-{{end}})", "definitionsOnly": " (nur Definitionen)", "maxLines": " (maximal {{max}} Zeilen)", - "imageTooLarge": "Die Bilddatei ist zu groß ({{size}} MB). Die maximal erlaubte Größe beträgt {{max}} MB." + "imageTooLarge": "Die Bilddatei ist zu groß ({{size}} MB). Die maximal erlaubte Größe beträgt {{max}} MB.", + "imageWithDimensions": "Bilddatei ({{dimensions}}, {{size}} KB)", + "imageWithSize": "Bilddatei ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo scheint in einer Schleife festzustecken und versucht wiederholt dieselbe Aktion ({{toolName}}). Dies könnte auf ein Problem mit der aktuellen Strategie hindeuten. Überlege dir, die Aufgabe umzuformulieren, genauere Anweisungen zu geben oder Roo zu einem anderen Ansatz zu führen.", "codebaseSearch": { diff --git a/src/i18n/locales/en/tools.json b/src/i18n/locales/en/tools.json index fe9b6c299f..188e4dbcf2 100644 --- a/src/i18n/locales/en/tools.json +++ b/src/i18n/locales/en/tools.json @@ -3,7 +3,9 @@ "linesRange": " (lines {{start}}-{{end}})", "definitionsOnly": " (definitions only)", "maxLines": " (max {{max}} lines)", - "imageTooLarge": "Image file is too large ({{size}} MB). The maximum allowed size is {{max}} MB." + "imageTooLarge": "Image file is too large ({{size}} MB). The maximum allowed size is {{max}} MB.", + "imageWithDimensions": "Image file ({{dimensions}}, {{size}} KB)", + "imageWithSize": "Image file ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo appears to be stuck in a loop, attempting the same action ({{toolName}}) repeatedly. This might indicate a problem with its current strategy. Consider rephrasing the task, providing more specific instructions, or guiding it towards a different approach.", "codebaseSearch": { diff --git a/src/i18n/locales/es/tools.json b/src/i18n/locales/es/tools.json index 410e7e1148..f1cb00f9a5 100644 --- a/src/i18n/locales/es/tools.json +++ b/src/i18n/locales/es/tools.json @@ -3,7 +3,9 @@ "linesRange": " (líneas {{start}}-{{end}})", "definitionsOnly": " (solo definiciones)", "maxLines": " (máximo {{max}} líneas)", - "imageTooLarge": "El archivo de imagen es demasiado grande ({{size}} MB). El tamaño máximo permitido es {{max}} MB." + "imageTooLarge": "El archivo de imagen es demasiado grande ({{size}} MB). El tamaño máximo permitido es {{max}} MB.", + "imageWithDimensions": "Archivo de imagen ({{dimensions}}, {{size}} KB)", + "imageWithSize": "Archivo de imagen ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo parece estar atrapado en un bucle, intentando la misma acción ({{toolName}}) repetidamente. Esto podría indicar un problema con su estrategia actual. Considera reformular la tarea, proporcionar instrucciones más específicas o guiarlo hacia un enfoque diferente.", "codebaseSearch": { diff --git a/src/i18n/locales/fr/tools.json b/src/i18n/locales/fr/tools.json index 6e3f3f05ce..c9ff4d14c3 100644 --- a/src/i18n/locales/fr/tools.json +++ b/src/i18n/locales/fr/tools.json @@ -3,7 +3,9 @@ "linesRange": " (lignes {{start}}-{{end}})", "definitionsOnly": " (définitions uniquement)", "maxLines": " (max {{max}} lignes)", - "imageTooLarge": "Le fichier image est trop volumineux ({{size}} MB). La taille maximale autorisée est {{max}} MB." + "imageTooLarge": "Le fichier image est trop volumineux ({{size}} MB). La taille maximale autorisée est {{max}} MB.", + "imageWithDimensions": "Fichier image ({{dimensions}}, {{size}} Ko)", + "imageWithSize": "Fichier image ({{size}} Ko)" }, "toolRepetitionLimitReached": "Roo semble être bloqué dans une boucle, tentant la même action ({{toolName}}) de façon répétée. Cela pourrait indiquer un problème avec sa stratégie actuelle. Envisage de reformuler la tâche, de fournir des instructions plus spécifiques ou de le guider vers une approche différente.", "codebaseSearch": { diff --git a/src/i18n/locales/hi/tools.json b/src/i18n/locales/hi/tools.json index 24df270e66..0d262a2d08 100644 --- a/src/i18n/locales/hi/tools.json +++ b/src/i18n/locales/hi/tools.json @@ -3,7 +3,9 @@ "linesRange": " (पंक्तियाँ {{start}}-{{end}})", "definitionsOnly": " (केवल परिभाषाएँ)", "maxLines": " (अधिकतम {{max}} पंक्तियाँ)", - "imageTooLarge": "छवि फ़ाइल बहुत बड़ी है ({{size}} MB)। अधिकतम अनुमतित आकार {{max}} MB है।" + "imageTooLarge": "छवि फ़ाइल बहुत बड़ी है ({{size}} MB)। अधिकतम अनुमतित आकार {{max}} MB है।", + "imageWithDimensions": "छवि फ़ाइल ({{dimensions}}, {{size}} KB)", + "imageWithSize": "छवि फ़ाइल ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo एक लूप में फंसा हुआ लगता है, बार-बार एक ही क्रिया ({{toolName}}) को दोहरा रहा है। यह उसकी वर्तमान रणनीति में किसी समस्या का संकेत हो सकता है। कार्य को पुनः परिभाषित करने, अधिक विशिष्ट निर्देश देने, या उसे एक अलग दृष्टिकोण की ओर मार्गदर्शित करने पर विचार करें।", "codebaseSearch": { diff --git a/src/i18n/locales/id/tools.json b/src/i18n/locales/id/tools.json index 6745bfd474..692b2e1ac3 100644 --- a/src/i18n/locales/id/tools.json +++ b/src/i18n/locales/id/tools.json @@ -3,7 +3,9 @@ "linesRange": " (baris {{start}}-{{end}})", "definitionsOnly": " (hanya definisi)", "maxLines": " (maks {{max}} baris)", - "imageTooLarge": "File gambar terlalu besar ({{size}} MB). Ukuran maksimum yang diizinkan adalah {{max}} MB." + "imageTooLarge": "File gambar terlalu besar ({{size}} MB). Ukuran maksimum yang diizinkan adalah {{max}} MB.", + "imageWithDimensions": "File gambar ({{dimensions}}, {{size}} KB)", + "imageWithSize": "File gambar ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo tampaknya terjebak dalam loop, mencoba aksi yang sama ({{toolName}}) berulang kali. Ini mungkin menunjukkan masalah dengan strategi saat ini. Pertimbangkan untuk mengubah frasa tugas, memberikan instruksi yang lebih spesifik, atau mengarahkannya ke pendekatan yang berbeda.", "codebaseSearch": { diff --git a/src/i18n/locales/it/tools.json b/src/i18n/locales/it/tools.json index cfb631db7b..73cedd4520 100644 --- a/src/i18n/locales/it/tools.json +++ b/src/i18n/locales/it/tools.json @@ -3,7 +3,9 @@ "linesRange": " (righe {{start}}-{{end}})", "definitionsOnly": " (solo definizioni)", "maxLines": " (max {{max}} righe)", - "imageTooLarge": "Il file immagine è troppo grande ({{size}} MB). La dimensione massima consentita è {{max}} MB." + "imageTooLarge": "Il file immagine è troppo grande ({{size}} MB). La dimensione massima consentita è {{max}} MB.", + "imageWithDimensions": "File immagine ({{dimensions}}, {{size}} KB)", + "imageWithSize": "File immagine ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo sembra essere bloccato in un ciclo, tentando ripetutamente la stessa azione ({{toolName}}). Questo potrebbe indicare un problema con la sua strategia attuale. Considera di riformulare l'attività, fornire istruzioni più specifiche o guidarlo verso un approccio diverso.", "codebaseSearch": { diff --git a/src/i18n/locales/ja/tools.json b/src/i18n/locales/ja/tools.json index c70b778594..d1af6c4ce7 100644 --- a/src/i18n/locales/ja/tools.json +++ b/src/i18n/locales/ja/tools.json @@ -3,7 +3,9 @@ "linesRange": " ({{start}}-{{end}}行目)", "definitionsOnly": " (定義のみ)", "maxLines": " (最大{{max}}行)", - "imageTooLarge": "画像ファイルが大きすぎます({{size}} MB)。最大許可サイズは {{max}} MB です。" + "imageTooLarge": "画像ファイルが大きすぎます({{size}} MB)。最大許可サイズは {{max}} MB です。", + "imageWithDimensions": "画像ファイル({{dimensions}}、{{size}} KB)", + "imageWithSize": "画像ファイル({{size}} KB)" }, "toolRepetitionLimitReached": "Rooが同じ操作({{toolName}})を繰り返し試みるループに陥っているようです。これは現在の方法に問題がある可能性を示しています。タスクの言い換え、より具体的な指示の提供、または別のアプローチへの誘導を検討してください。", "codebaseSearch": { diff --git a/src/i18n/locales/ko/tools.json b/src/i18n/locales/ko/tools.json index 6126aad34a..f99a562bbc 100644 --- a/src/i18n/locales/ko/tools.json +++ b/src/i18n/locales/ko/tools.json @@ -3,7 +3,9 @@ "linesRange": " ({{start}}-{{end}}행)", "definitionsOnly": " (정의만)", "maxLines": " (최대 {{max}}행)", - "imageTooLarge": "이미지 파일이 너무 큽니다 ({{size}} MB). 최대 허용 크기는 {{max}} MB입니다." + "imageTooLarge": "이미지 파일이 너무 큽니다 ({{size}} MB). 최대 허용 크기는 {{max}} MB입니다.", + "imageWithDimensions": "이미지 파일 ({{dimensions}}, {{size}} KB)", + "imageWithSize": "이미지 파일 ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo가 같은 동작({{toolName}})을 반복적으로 시도하면서 루프에 갇힌 것 같습니다. 이는 현재 전략에 문제가 있을 수 있음을 나타냅니다. 작업을 다시 표현하거나, 더 구체적인 지침을 제공하거나, 다른 접근 방식으로 안내해 보세요.", "codebaseSearch": { diff --git a/src/i18n/locales/nl/tools.json b/src/i18n/locales/nl/tools.json index b36c727e07..18db1b6d9d 100644 --- a/src/i18n/locales/nl/tools.json +++ b/src/i18n/locales/nl/tools.json @@ -3,7 +3,9 @@ "linesRange": " (regels {{start}}-{{end}})", "definitionsOnly": " (alleen definities)", "maxLines": " (max {{max}} regels)", - "imageTooLarge": "Afbeeldingsbestand is te groot ({{size}} MB). De maximaal toegestane grootte is {{max}} MB." + "imageTooLarge": "Afbeeldingsbestand is te groot ({{size}} MB). De maximaal toegestane grootte is {{max}} MB.", + "imageWithDimensions": "Afbeeldingsbestand ({{dimensions}}, {{size}} KB)", + "imageWithSize": "Afbeeldingsbestand ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo lijkt vast te zitten in een lus, waarbij hij herhaaldelijk dezelfde actie ({{toolName}}) probeert. Dit kan duiden op een probleem met de huidige strategie. Overweeg de taak te herformuleren, specifiekere instructies te geven of Roo naar een andere aanpak te leiden.", "codebaseSearch": { diff --git a/src/i18n/locales/pl/tools.json b/src/i18n/locales/pl/tools.json index 0ddfa11f83..ab998b2d0d 100644 --- a/src/i18n/locales/pl/tools.json +++ b/src/i18n/locales/pl/tools.json @@ -3,7 +3,9 @@ "linesRange": " (linie {{start}}-{{end}})", "definitionsOnly": " (tylko definicje)", "maxLines": " (maks. {{max}} linii)", - "imageTooLarge": "Plik obrazu jest zbyt duży ({{size}} MB). Maksymalny dozwolony rozmiar to {{max}} MB." + "imageTooLarge": "Plik obrazu jest zbyt duży ({{size}} MB). Maksymalny dozwolony rozmiar to {{max}} MB.", + "imageWithDimensions": "Plik obrazu ({{dimensions}}, {{size}} KB)", + "imageWithSize": "Plik obrazu ({{size}} KB)" }, "toolRepetitionLimitReached": "Wygląda na to, że Roo utknął w pętli, wielokrotnie próbując wykonać tę samą akcję ({{toolName}}). Może to wskazywać na problem z jego obecną strategią. Rozważ przeformułowanie zadania, podanie bardziej szczegółowych instrukcji lub nakierowanie go na inne podejście.", "codebaseSearch": { diff --git a/src/i18n/locales/pt-BR/tools.json b/src/i18n/locales/pt-BR/tools.json index 3f4c21e0f5..6057723cca 100644 --- a/src/i18n/locales/pt-BR/tools.json +++ b/src/i18n/locales/pt-BR/tools.json @@ -3,7 +3,9 @@ "linesRange": " (linhas {{start}}-{{end}})", "definitionsOnly": " (apenas definições)", "maxLines": " (máx. {{max}} linhas)", - "imageTooLarge": "Arquivo de imagem é muito grande ({{size}} MB). O tamanho máximo permitido é {{max}} MB." + "imageTooLarge": "Arquivo de imagem é muito grande ({{size}} MB). O tamanho máximo permitido é {{max}} MB.", + "imageWithDimensions": "Arquivo de imagem ({{dimensions}}, {{size}} KB)", + "imageWithSize": "Arquivo de imagem ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo parece estar preso em um loop, tentando a mesma ação ({{toolName}}) repetidamente. Isso pode indicar um problema com sua estratégia atual. Considere reformular a tarefa, fornecer instruções mais específicas ou guiá-lo para uma abordagem diferente.", "codebaseSearch": { diff --git a/src/i18n/locales/ru/tools.json b/src/i18n/locales/ru/tools.json index 7fff609459..c1dddffcae 100644 --- a/src/i18n/locales/ru/tools.json +++ b/src/i18n/locales/ru/tools.json @@ -3,7 +3,9 @@ "linesRange": " (строки {{start}}-{{end}})", "definitionsOnly": " (только определения)", "maxLines": " (макс. {{max}} строк)", - "imageTooLarge": "Файл изображения слишком большой ({{size}} МБ). Максимально допустимый размер {{max}} МБ." + "imageTooLarge": "Файл изображения слишком большой ({{size}} МБ). Максимально допустимый размер {{max}} МБ.", + "imageWithDimensions": "Файл изображения ({{dimensions}}, {{size}} КБ)", + "imageWithSize": "Файл изображения ({{size}} КБ)" }, "toolRepetitionLimitReached": "Похоже, что Roo застрял в цикле, многократно пытаясь выполнить одно и то же действие ({{toolName}}). Это может указывать на проблему с его текущей стратегией. Попробуйте переформулировать задачу, предоставить более конкретные инструкции или направить его к другому подходу.", "codebaseSearch": { diff --git a/src/i18n/locales/tr/tools.json b/src/i18n/locales/tr/tools.json index 3eccc1bb35..1462bc221c 100644 --- a/src/i18n/locales/tr/tools.json +++ b/src/i18n/locales/tr/tools.json @@ -3,7 +3,9 @@ "linesRange": " (satır {{start}}-{{end}})", "definitionsOnly": " (sadece tanımlar)", "maxLines": " (maks. {{max}} satır)", - "imageTooLarge": "Görüntü dosyası çok büyük ({{size}} MB). İzin verilen maksimum boyut {{max}} MB." + "imageTooLarge": "Görüntü dosyası çok büyük ({{size}} MB). İzin verilen maksimum boyut {{max}} MB.", + "imageWithDimensions": "Görüntü dosyası ({{dimensions}}, {{size}} KB)", + "imageWithSize": "Görüntü dosyası ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo bir döngüye takılmış gibi görünüyor, aynı eylemi ({{toolName}}) tekrar tekrar deniyor. Bu, mevcut stratejisinde bir sorun olduğunu gösterebilir. Görevi yeniden ifade etmeyi, daha spesifik talimatlar vermeyi veya onu farklı bir yaklaşıma yönlendirmeyi düşünün.", "codebaseSearch": { diff --git a/src/i18n/locales/vi/tools.json b/src/i18n/locales/vi/tools.json index 9138594299..62ba7187ce 100644 --- a/src/i18n/locales/vi/tools.json +++ b/src/i18n/locales/vi/tools.json @@ -3,7 +3,9 @@ "linesRange": " (dòng {{start}}-{{end}})", "definitionsOnly": " (chỉ định nghĩa)", "maxLines": " (tối đa {{max}} dòng)", - "imageTooLarge": "Tệp hình ảnh quá lớn ({{size}} MB). Kích thước tối đa cho phép là {{max}} MB." + "imageTooLarge": "Tệp hình ảnh quá lớn ({{size}} MB). Kích thước tối đa cho phép là {{max}} MB.", + "imageWithDimensions": "Tệp hình ảnh ({{dimensions}}, {{size}} KB)", + "imageWithSize": "Tệp hình ảnh ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo dường như đang bị mắc kẹt trong một vòng lặp, liên tục cố gắng thực hiện cùng một hành động ({{toolName}}). Điều này có thể cho thấy vấn đề với chiến lược hiện tại. Hãy cân nhắc việc diễn đạt lại nhiệm vụ, cung cấp hướng dẫn cụ thể hơn, hoặc hướng Roo theo một cách tiếp cận khác.", "codebaseSearch": { diff --git a/src/i18n/locales/zh-CN/tools.json b/src/i18n/locales/zh-CN/tools.json index d59f89ddab..a7526cf8d1 100644 --- a/src/i18n/locales/zh-CN/tools.json +++ b/src/i18n/locales/zh-CN/tools.json @@ -3,7 +3,9 @@ "linesRange": " (第 {{start}}-{{end}} 行)", "definitionsOnly": " (仅定义)", "maxLines": " (最多 {{max}} 行)", - "imageTooLarge": "图片文件过大 ({{size}} MB)。允许的最大大小为 {{max}} MB。" + "imageTooLarge": "图片文件过大 ({{size}} MB)。允许的最大大小为 {{max}} MB。", + "imageWithDimensions": "图片文件 ({{dimensions}}, {{size}} KB)", + "imageWithSize": "图片文件 ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo 似乎陷入循环,反复尝试同一操作 ({{toolName}})。这可能表明当前策略存在问题。请考虑重新描述任务、提供更具体的指示或引导其尝试不同的方法。", "codebaseSearch": { diff --git a/src/i18n/locales/zh-TW/tools.json b/src/i18n/locales/zh-TW/tools.json index 0d985e7dfb..87c3fdee7f 100644 --- a/src/i18n/locales/zh-TW/tools.json +++ b/src/i18n/locales/zh-TW/tools.json @@ -3,7 +3,9 @@ "linesRange": " (第 {{start}}-{{end}} 行)", "definitionsOnly": " (僅定義)", "maxLines": " (最多 {{max}} 行)", - "imageTooLarge": "圖片檔案過大 ({{size}} MB)。允許的最大大小為 {{max}} MB。" + "imageTooLarge": "圖片檔案過大 ({{size}} MB)。允許的最大大小為 {{max}} MB。", + "imageWithDimensions": "圖片檔案 ({{dimensions}}, {{size}} KB)", + "imageWithSize": "圖片檔案 ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo 似乎陷入循環,反覆嘗試同一操作 ({{toolName}})。這可能表明目前策略存在問題。請考慮重新描述工作、提供更具體的指示或引導其嘗試不同的方法。", "codebaseSearch": { From 7ec9ae60928b7b90910e38d6a03308dc2f79f675 Mon Sep 17 00:00:00 2001 From: Sam Hoang Date: Sun, 6 Jul 2025 18:05:39 +0700 Subject: [PATCH 09/21] add max memory for total file --- packages/types/src/global-settings.ts | 1 + src/core/tools/__tests__/readFileTool.spec.ts | 701 +++++++++++++++--- src/core/tools/readFileTool.ts | 51 +- src/core/webview/ClineProvider.ts | 3 + .../webview/__tests__/ClineProvider.spec.ts | 1 + src/core/webview/webviewMessageHandler.ts | 4 + src/shared/ExtensionMessage.ts | 1 + src/shared/WebviewMessage.ts | 1 + .../settings/ContextManagementSettings.tsx | 31 + .../src/components/settings/SettingsView.tsx | 3 + .../src/context/ExtensionStateContext.tsx | 4 + .../__tests__/ExtensionStateContext.spec.tsx | 3 +- webview-ui/src/i18n/locales/ca/settings.json | 5 + webview-ui/src/i18n/locales/de/settings.json | 5 + webview-ui/src/i18n/locales/en/settings.json | 5 + webview-ui/src/i18n/locales/es/settings.json | 5 + webview-ui/src/i18n/locales/fr/settings.json | 5 + webview-ui/src/i18n/locales/hi/settings.json | 5 + webview-ui/src/i18n/locales/id/settings.json | 5 + webview-ui/src/i18n/locales/it/settings.json | 5 + webview-ui/src/i18n/locales/ja/settings.json | 5 + webview-ui/src/i18n/locales/ko/settings.json | 5 + webview-ui/src/i18n/locales/nl/settings.json | 5 + webview-ui/src/i18n/locales/pl/settings.json | 5 + .../src/i18n/locales/pt-BR/settings.json | 5 + webview-ui/src/i18n/locales/ru/settings.json | 5 + webview-ui/src/i18n/locales/tr/settings.json | 5 + webview-ui/src/i18n/locales/vi/settings.json | 5 + .../src/i18n/locales/zh-CN/settings.json | 5 + .../src/i18n/locales/zh-TW/settings.json | 5 + 30 files changed, 768 insertions(+), 126 deletions(-) diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 734cc36c9d..d49185c355 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -102,6 +102,7 @@ export const globalSettingsSchema = z.object({ showRooIgnoredFiles: z.boolean().optional(), maxReadFileLine: z.number().optional(), maxImageFileSize: z.number().optional(), + maxTotalImageMemory: z.number().optional(), terminalOutputLineLimit: z.number().optional(), terminalOutputCharacterLimit: z.number().optional(), diff --git a/src/core/tools/__tests__/readFileTool.spec.ts b/src/core/tools/__tests__/readFileTool.spec.ts index b326e1663b..9afe35c3b6 100644 --- a/src/core/tools/__tests__/readFileTool.spec.ts +++ b/src/core/tools/__tests__/readFileTool.spec.ts @@ -117,6 +117,37 @@ vi.mock("../../../utils/fs", () => ({ fileExistsAtPath: vi.fn().mockReturnValue(true), })) +// Global beforeEach to ensure clean mock state between all test suites +beforeEach(() => { + // NOTE: Removed vi.clearAllMocks() to prevent interference with setImageSupport calls + // Instead, individual suites clear their specific mocks to maintain isolation + + // Explicitly reset the hoisted mock implementations to prevent cross-suite pollution + toolResultMock.mockImplementation((text: string, images?: string[]) => { + if (images && images.length > 0) { + return [ + { type: "text", text }, + ...images.map((img) => { + const [header, data] = img.split(",") + const media_type = header.match(/:(.*?);/)?.[1] || "image/png" + return { type: "image", source: { type: "base64", media_type, data } } + }), + ] + } + return text + }) + + imageBlocksMock.mockImplementation((images?: string[]) => { + return images + ? images.map((img) => { + const [header, data] = img.split(",") + const media_type = header.match(/:(.*?);/)?.[1] || "image/png" + return { type: "image", source: { type: "base64", media_type, data } } + }) + : [] + }) +}) + // Mock i18n translation function vi.mock("../../../i18n", () => ({ t: vi.fn((key: string, params?: Record) => { @@ -143,6 +174,52 @@ vi.mock("../../../i18n", () => ({ }), })) +// Shared mock setup function to ensure consistent state across all test suites +function createMockCline(): any { + const mockProvider = { + getState: vi.fn(), + deref: vi.fn().mockReturnThis(), + } + + const mockCline: any = { + cwd: "/", + task: "Test", + providerRef: mockProvider, + rooIgnoreController: { + validateAccess: vi.fn().mockReturnValue(true), + }, + say: vi.fn().mockResolvedValue(undefined), + ask: vi.fn().mockResolvedValue({ response: "yesButtonClicked" }), + presentAssistantMessage: vi.fn(), + handleError: vi.fn().mockResolvedValue(undefined), + pushToolResult: vi.fn(), + removeClosingTag: vi.fn((tag, content) => content), + fileContextTracker: { + trackFileContext: vi.fn().mockResolvedValue(undefined), + }, + recordToolUsage: vi.fn().mockReturnValue(undefined), + recordToolError: vi.fn().mockReturnValue(undefined), + didRejectTool: false, + // CRITICAL: Always ensure image support is enabled + api: { + getModel: vi.fn().mockReturnValue({ + info: { supportsImages: true } + }) + } + } + + return { mockCline, mockProvider } +} + +// Helper function to set image support without affecting shared state +function setImageSupport(mockCline: any, supportsImages: boolean | undefined): void { + mockCline.api = { + getModel: vi.fn().mockReturnValue({ + info: { supportsImages } + }) + } +} + describe("read_file tool with maxReadFileLine setting", () => { // Test data const testFilePath = "test/file.txt" @@ -160,12 +237,27 @@ describe("read_file tool with maxReadFileLine setting", () => { const mockedIsBinaryFile = vi.mocked(isBinaryFile) const mockedPathResolve = vi.mocked(path.resolve) - const mockCline: any = {} + let mockCline: any let mockProvider: any let toolResult: ToolResponse | undefined beforeEach(() => { - vi.clearAllMocks() + // Clear specific mocks (not all mocks to preserve shared state) + mockedCountFileLines.mockClear() + mockedExtractTextFromFile.mockClear() + mockedIsBinaryFile.mockClear() + mockedPathResolve.mockClear() + addLineNumbersMock.mockClear() + extractTextFromFileMock.mockClear() + toolResultMock.mockClear() + + // Use shared mock setup function + const mocks = createMockCline() + mockCline = mocks.mockCline + mockProvider = mocks.mockProvider + + // Explicitly disable image support for text file tests to prevent cross-suite pollution + setImageSupport(mockCline, false) mockedPathResolve.mockReturnValue(absoluteFilePath) mockedIsBinaryFile.mockResolvedValue(false) @@ -182,38 +274,6 @@ describe("read_file tool with maxReadFileLine setting", () => { return Promise.resolve(addLineNumbersMock(mockInputContent)) }) - mockProvider = { - getState: vi.fn(), - deref: vi.fn().mockReturnThis(), - } - - mockCline.cwd = "/" - mockCline.task = "Test" - mockCline.providerRef = mockProvider - mockCline.rooIgnoreController = { - validateAccess: vi.fn().mockReturnValue(true), - } - mockCline.say = vi.fn().mockResolvedValue(undefined) - mockCline.ask = vi.fn().mockResolvedValue({ response: "yesButtonClicked" }) - mockCline.presentAssistantMessage = vi.fn() - mockCline.handleError = vi.fn().mockResolvedValue(undefined) - mockCline.pushToolResult = vi.fn() - mockCline.removeClosingTag = vi.fn((tag, content) => content) - - mockCline.fileContextTracker = { - trackFileContext: vi.fn().mockResolvedValue(undefined), - } - - mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) - mockCline.recordToolError = vi.fn().mockReturnValue(undefined) - - // Setup default API handler that supports images - mockCline.api = { - getModel: vi.fn().mockReturnValue({ - info: { supportsImages: true } - }) - } - toolResult = undefined }) @@ -235,7 +295,7 @@ describe("read_file tool with maxReadFileLine setting", () => { const maxReadFileLine = options.maxReadFileLine ?? 500 const totalLines = options.totalLines ?? 5 - mockProvider.getState.mockResolvedValue({ maxReadFileLine }) + mockProvider.getState.mockResolvedValue({ maxReadFileLine, maxImageFileSize: 20, maxTotalImageMemory: 20 }) mockedCountFileLines.mockResolvedValue(totalLines) // Reset the spy before each test @@ -428,12 +488,32 @@ describe("read_file tool XML output structure", () => { const mockedIsBinaryFile = vi.mocked(isBinaryFile) const mockedPathResolve = vi.mocked(path.resolve) - const mockCline: any = {} + let mockCline: any let mockProvider: any let toolResult: ToolResponse | undefined beforeEach(() => { - vi.clearAllMocks() + // Clear specific mocks (not all mocks to preserve shared state) + mockedCountFileLines.mockClear() + mockedExtractTextFromFile.mockClear() + mockedIsBinaryFile.mockClear() + mockedPathResolve.mockClear() + addLineNumbersMock.mockClear() + extractTextFromFileMock.mockClear() + toolResultMock.mockClear() + + // CRITICAL: Reset fsPromises mocks to prevent cross-test contamination + fsPromises.stat.mockClear() + fsPromises.stat.mockResolvedValue({ size: 1024 }) + fsPromises.readFile.mockClear() + + // Use shared mock setup function + const mocks = createMockCline() + mockCline = mocks.mockCline + mockProvider = mocks.mockProvider + + // Explicitly enable image support for this test suite (contains image memory tests) + setImageSupport(mockCline, true) mockedPathResolve.mockReturnValue(absoluteFilePath) mockedIsBinaryFile.mockResolvedValue(false) @@ -446,37 +526,11 @@ describe("read_file tool XML output structure", () => { mockInputContent = fileContent // Setup mock provider with default maxReadFileLine - mockProvider = { - getState: vi.fn().mockResolvedValue({ maxReadFileLine: -1 }), // Default to full file read - deref: vi.fn().mockReturnThis(), - } + mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, maxTotalImageMemory: 20 }) // Default to full file read - mockCline.cwd = "/" - mockCline.task = "Test" - mockCline.providerRef = mockProvider - mockCline.rooIgnoreController = { - validateAccess: vi.fn().mockReturnValue(true), - } - mockCline.say = vi.fn().mockResolvedValue(undefined) - mockCline.ask = vi.fn().mockResolvedValue({ response: "yesButtonClicked" }) - mockCline.presentAssistantMessage = vi.fn() + // Add additional properties needed for XML tests mockCline.sayAndCreateMissingParamError = vi.fn().mockResolvedValue("Missing required parameter") - mockCline.fileContextTracker = { - trackFileContext: vi.fn().mockResolvedValue(undefined), - } - - mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) - mockCline.recordToolError = vi.fn().mockReturnValue(undefined) - mockCline.didRejectTool = false - - // Mock the API handler - required for image support check - mockCline.api = { - getModel: vi.fn().mockReturnValue({ - info: { supportsImages: true } - }) - } - toolResult = undefined }) @@ -497,7 +551,7 @@ describe("read_file tool XML output structure", () => { const isBinary = options.isBinary ?? false const validateAccess = options.validateAccess ?? true - mockProvider.getState.mockResolvedValue({ maxReadFileLine }) + mockProvider.getState.mockResolvedValue({ maxReadFileLine, maxImageFileSize: 20, maxTotalImageMemory: 20 }) mockedCountFileLines.mockResolvedValue(totalLines) mockedIsBinaryFile.mockResolvedValue(isBinary) mockCline.rooIgnoreController.validateAccess = vi.fn().mockReturnValue(validateAccess) @@ -536,7 +590,7 @@ describe("read_file tool XML output structure", () => { addLineNumbersMock(mockInputContent) return Promise.resolve(numberedContent) }) - mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1 }) + mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, maxTotalImageMemory: 20 }) // Allow up to 20MB per image and total memory // Execute const result = await executeReadFileTool() @@ -565,7 +619,7 @@ describe("read_file tool XML output structure", () => { // Setup mockedCountFileLines.mockResolvedValue(0) mockedExtractTextFromFile.mockResolvedValue("") - mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1 }) + mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, maxTotalImageMemory: 20 }) // Allow up to 20MB per image and total memory // Execute const result = await executeReadFileTool({}, { totalLines: 0 }) @@ -575,6 +629,424 @@ describe("read_file tool XML output structure", () => { `\n${testFilePath}\nFile is empty\n\n`, ) }) + + describe("Total Image Memory Limit", () => { + const testImages = [ + { path: "test/image1.png", sizeKB: 5120 }, // 5MB + { path: "test/image2.jpg", sizeKB: 10240 }, // 10MB + { path: "test/image3.gif", sizeKB: 8192 }, // 8MB + ] + + beforeEach(() => { + // CRITICAL: Reset fsPromises mocks to prevent cross-test contamination within this suite + fsPromises.stat.mockClear() + fsPromises.readFile.mockClear() + }) + + async function executeReadMultipleImagesTool(imagePaths: string[]): Promise { + // Ensure image support is enabled before calling the tool + setImageSupport(mockCline, true) + + // Create args content for multiple files + const filesXml = imagePaths.map(path => `${path}`).join('') + const argsContent = filesXml + + const toolUse: ReadFileToolUse = { + type: "tool_use", + name: "read_file", + params: { args: argsContent }, + partial: false, + } + + let localResult: ToolResponse | undefined + await readFileTool( + mockCline, + toolUse, + mockCline.ask, + vi.fn(), + (result: ToolResponse) => { + localResult = result + }, + (_: ToolParamName, content?: string) => content ?? "", + ) + + return localResult + } + + it("should allow multiple images under the total memory limit", async () => { + // Setup required mocks (don't clear all mocks - preserve API setup) + mockedIsBinaryFile.mockResolvedValue(true) + mockedCountFileLines.mockResolvedValue(0) + fsPromises.readFile.mockResolvedValue(Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", "base64")) + + // Setup mockProvider + mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, maxTotalImageMemory: 20 }) // Allow up to 20MB per image and total memory + + // Setup mockCline properties (preserve existing API) + mockCline.cwd = "/" + mockCline.task = "Test" + mockCline.providerRef = mockProvider + mockCline.rooIgnoreController = { + validateAccess: vi.fn().mockReturnValue(true), + } + mockCline.say = vi.fn().mockResolvedValue(undefined) + mockCline.ask = vi.fn().mockResolvedValue({ response: "yesButtonClicked" }) + mockCline.presentAssistantMessage = vi.fn() + mockCline.handleError = vi.fn().mockResolvedValue(undefined) + mockCline.pushToolResult = vi.fn() + mockCline.removeClosingTag = vi.fn((tag, content) => content) + mockCline.fileContextTracker = { + trackFileContext: vi.fn().mockResolvedValue(undefined), + } + mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) + mockCline.recordToolError = vi.fn().mockReturnValue(undefined) + setImageSupport(mockCline, true) + + // Setup - images that fit within 20MB limit + const smallImages = [ + { path: "test/small1.png", sizeKB: 2048 }, // 2MB + { path: "test/small2.jpg", sizeKB: 3072 }, // 3MB + { path: "test/small3.gif", sizeKB: 4096 }, // 4MB + ] // Total: 9MB (under 20MB limit) + + // Mock file stats for each image + fsPromises.stat = vi.fn().mockImplementation((filePath) => { + const fileName = filePath.split('/').pop() + const image = smallImages.find(img => img.path.includes(fileName.split('.')[0])) + return Promise.resolve({ size: (image?.sizeKB || 1024) * 1024 }) + }) + + // Mock path.resolve for each image + mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) + + // Execute + const result = await executeReadMultipleImagesTool(smallImages.map(img => img.path)) + + // Verify all images were processed (should be a multi-part response) + expect(Array.isArray(result)).toBe(true) + const parts = result as any[] + + // Should have text part and 3 image parts + const textPart = parts.find(p => p.type === "text")?.text + const imageParts = parts.filter(p => p.type === "image") + + expect(textPart).toBeDefined() + expect(imageParts).toHaveLength(3) + + // Verify no memory limit notices + expect(textPart).not.toContain("Total image memory would exceed") + }) + + it("should skip images that would exceed the total memory limit", async () => { + // Setup required mocks (don't clear all mocks) + mockedIsBinaryFile.mockResolvedValue(true) + mockedCountFileLines.mockResolvedValue(0) + fsPromises.readFile.mockResolvedValue(Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", "base64")) + + // Setup mockProvider + mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 15, maxTotalImageMemory: 20 }) // Allow up to 15MB per image and 20MB total memory + + // Setup mockCline properties + mockCline.cwd = "/" + mockCline.task = "Test" + mockCline.providerRef = mockProvider + mockCline.rooIgnoreController = { + validateAccess: vi.fn().mockReturnValue(true), + } + mockCline.say = vi.fn().mockResolvedValue(undefined) + mockCline.ask = vi.fn().mockResolvedValue({ response: "yesButtonClicked" }) + mockCline.presentAssistantMessage = vi.fn() + mockCline.handleError = vi.fn().mockResolvedValue(undefined) + mockCline.pushToolResult = vi.fn() + mockCline.removeClosingTag = vi.fn((tag, content) => content) + mockCline.fileContextTracker = { + trackFileContext: vi.fn().mockResolvedValue(undefined), + } + mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) + mockCline.recordToolError = vi.fn().mockReturnValue(undefined) + setImageSupport(mockCline, true) + + // Setup - images where later ones would exceed 20MB total limit + // Each must be under 5MB per-file limit (5120KB) + const largeImages = [ + { path: "test/large1.png", sizeKB: 5017 }, // ~4.9MB + { path: "test/large2.jpg", sizeKB: 5017 }, // ~4.9MB + { path: "test/large3.gif", sizeKB: 5017 }, // ~4.9MB + { path: "test/large4.png", sizeKB: 5017 }, // ~4.9MB + { path: "test/large5.jpg", sizeKB: 5017 }, // ~4.9MB - This should be skipped (total would be ~24.5MB > 20MB) + ] + + // Mock file stats for each image + fsPromises.stat = vi.fn().mockImplementation((filePath) => { + const fileName = path.basename(filePath) + const baseName = path.parse(fileName).name + const image = largeImages.find(img => img.path.includes(baseName)) + return Promise.resolve({ size: (image?.sizeKB || 1024) * 1024 }) + }) + + // Mock path.resolve for each image + mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) + + // Execute + const result = await executeReadMultipleImagesTool(largeImages.map(img => img.path)) + + // Verify result structure - should be a mix of successful images and skipped notices + expect(Array.isArray(result)).toBe(true) + const parts = result as any[] + + const textPart = parts.find(p => p.type === "text")?.text + const imageParts = parts.filter(p => p.type === "image") + + expect(textPart).toBeDefined() + + // Debug: Show what we actually got vs expected + if (imageParts.length !== 4) { + throw new Error(`Expected 4 images, got ${imageParts.length}. Full result: ${JSON.stringify(result, null, 2)}. Text part: ${textPart}`) + } + expect(imageParts).toHaveLength(4) // First 4 images should be included (~19.6MB total) + + // Verify memory limit notice for the fifth image + expect(textPart).toContain("Total image memory would exceed 20MB limit") + expect(textPart).toContain("current: 19.6MB") // 4 * 4.9MB + expect(textPart).toContain("this file: 4.9MB") + }) + + it("should track memory usage correctly across multiple images", async () => { + // Setup mocks (don't clear all mocks) + + // Setup required mocks + mockedIsBinaryFile.mockResolvedValue(true) + mockedCountFileLines.mockResolvedValue(0) + fsPromises.readFile.mockResolvedValue(Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", "base64")) + + // Setup mockProvider + mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 15, maxTotalImageMemory: 20 }) // Allow up to 15MB per image and 20MB total memory + + // Setup mockCline properties + mockCline.cwd = "/" + mockCline.task = "Test" + mockCline.providerRef = mockProvider + mockCline.rooIgnoreController = { + validateAccess: vi.fn().mockReturnValue(true), + } + mockCline.say = vi.fn().mockResolvedValue(undefined) + mockCline.ask = vi.fn().mockResolvedValue({ response: "yesButtonClicked" }) + mockCline.presentAssistantMessage = vi.fn() + mockCline.handleError = vi.fn().mockResolvedValue(undefined) + mockCline.pushToolResult = vi.fn() + mockCline.removeClosingTag = vi.fn((tag, content) => content) + mockCline.fileContextTracker = { + trackFileContext: vi.fn().mockResolvedValue(undefined), + } + mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) + mockCline.recordToolError = vi.fn().mockReturnValue(undefined) + setImageSupport(mockCline, true) + + // Setup - images that exactly reach the limit + const exactLimitImages = [ + { path: "test/exact1.png", sizeKB: 10240 }, // 10MB + { path: "test/exact2.jpg", sizeKB: 10240 }, // 10MB - Total exactly 20MB + { path: "test/exact3.gif", sizeKB: 1024 }, // 1MB - This should be skipped + ] + + // Mock file stats with simpler logic + fsPromises.stat = vi.fn().mockImplementation((filePath) => { + if (filePath.includes('exact1')) { + return Promise.resolve({ size: 10240 * 1024 }) // 10MB + } else if (filePath.includes('exact2')) { + return Promise.resolve({ size: 10240 * 1024 }) // 10MB + } else if (filePath.includes('exact3')) { + return Promise.resolve({ size: 1024 * 1024 }) // 1MB + } + return Promise.resolve({ size: 1024 * 1024 }) // Default 1MB + }) + + // Mock path.resolve + mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) + + // Execute + const result = await executeReadMultipleImagesTool(exactLimitImages.map(img => img.path)) + + // Verify + expect(Array.isArray(result)).toBe(true) + const parts = result as any[] + + const textPart = parts.find(p => p.type === "text")?.text + const imageParts = parts.filter(p => p.type === "image") + + expect(imageParts).toHaveLength(2) // First 2 images should fit + expect(textPart).toContain("Total image memory would exceed 20MB limit") + expect(textPart).toContain("current: 20.0MB") // Should show exactly 20MB used + }) + + it("should handle individual image size limit and total memory limit together", async () => { + // Setup mocks (don't clear all mocks) + + // Setup required mocks + mockedIsBinaryFile.mockResolvedValue(true) + mockedCountFileLines.mockResolvedValue(0) + fsPromises.readFile.mockResolvedValue(Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", "base64")) + + // Setup mockProvider + mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, maxTotalImageMemory: 20 }) // Allow up to 20MB per image and total memory + + // Setup mockCline properties (complete setup) + mockCline.cwd = "/" + mockCline.task = "Test" + mockCline.providerRef = mockProvider + mockCline.rooIgnoreController = { + validateAccess: vi.fn().mockReturnValue(true), + } + mockCline.say = vi.fn().mockResolvedValue(undefined) + mockCline.ask = vi.fn().mockResolvedValue({ response: "yesButtonClicked" }) + mockCline.presentAssistantMessage = vi.fn() + mockCline.handleError = vi.fn().mockResolvedValue(undefined) + mockCline.pushToolResult = vi.fn() + mockCline.removeClosingTag = vi.fn((tag, content) => content) + mockCline.fileContextTracker = { + trackFileContext: vi.fn().mockResolvedValue(undefined), + } + mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) + mockCline.recordToolError = vi.fn().mockReturnValue(undefined) + setImageSupport(mockCline, true) + + // Setup - mix of images with individual size violations and total memory issues + const mixedImages = [ + { path: "test/ok.png", sizeKB: 3072 }, // 3MB - OK + { path: "test/too-big.jpg", sizeKB: 6144 }, // 6MB - Exceeds individual 5MB limit + { path: "test/ok2.gif", sizeKB: 4096 }, // 4MB - OK individually but might exceed total + ] + + // Mock file stats + fsPromises.stat = vi.fn().mockImplementation((filePath) => { + const fileName = path.basename(filePath) + const baseName = path.parse(fileName).name + const image = mixedImages.find(img => img.path.includes(baseName)) + return Promise.resolve({ size: (image?.sizeKB || 1024) * 1024 }) + }) + + // Mock provider state with 5MB individual limit + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 5, + maxTotalImageMemory: 20 + }) + + // Mock path.resolve + mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) + + // Execute + const result = await executeReadMultipleImagesTool(mixedImages.map(img => img.path)) + + // Verify + expect(Array.isArray(result)).toBe(true) + const parts = result as any[] + + const textPart = parts.find(p => p.type === "text")?.text + const imageParts = parts.filter(p => p.type === "image") + + // Should have 2 images (ok.png and ok2.gif) + expect(imageParts).toHaveLength(2) + + // Should show individual size limit violation + expect(textPart).toContain("Image file is too large (6.0 MB). The maximum allowed size is 5 MB.") + }) + + it("should reset total memory tracking for each tool invocation", async () => { + // Setup mocks (don't clear all mocks) + + // Setup required mocks for first batch + mockedIsBinaryFile.mockResolvedValue(true) + mockedCountFileLines.mockResolvedValue(0) + fsPromises.readFile.mockResolvedValue(Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", "base64")) + + // Setup mockProvider + mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, maxTotalImageMemory: 20 }) + + // Setup mockCline properties (complete setup) + mockCline.cwd = "/" + mockCline.task = "Test" + mockCline.providerRef = mockProvider + mockCline.rooIgnoreController = { + validateAccess: vi.fn().mockReturnValue(true), + } + mockCline.say = vi.fn().mockResolvedValue(undefined) + mockCline.ask = vi.fn().mockResolvedValue({ response: "yesButtonClicked" }) + mockCline.presentAssistantMessage = vi.fn() + mockCline.handleError = vi.fn().mockResolvedValue(undefined) + mockCline.pushToolResult = vi.fn() + mockCline.removeClosingTag = vi.fn((tag, content) => content) + mockCline.fileContextTracker = { + trackFileContext: vi.fn().mockResolvedValue(undefined), + } + mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) + mockCline.recordToolError = vi.fn().mockReturnValue(undefined) + setImageSupport(mockCline, true) + + // Setup - first call with images that use memory + const firstBatch = [{ path: "test/first.png", sizeKB: 10240 }] // 10MB + + fsPromises.stat = vi.fn().mockResolvedValue({ size: 10240 * 1024 }) + mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) + + // Execute first batch + await executeReadMultipleImagesTool(firstBatch.map(img => img.path)) + + // Setup second batch (don't clear all mocks) + mockedIsBinaryFile.mockResolvedValue(true) + mockedCountFileLines.mockResolvedValue(0) + fsPromises.readFile.mockResolvedValue(Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", "base64")) + mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, maxTotalImageMemory: 20 }) + + // Reset path resolving for second batch + mockedPathResolve.mockClear() + + // Re-setup mockCline properties for second batch (complete setup) + mockCline.cwd = "/" + mockCline.task = "Test" + mockCline.providerRef = mockProvider + mockCline.rooIgnoreController = { + validateAccess: vi.fn().mockReturnValue(true), + } + mockCline.say = vi.fn().mockResolvedValue(undefined) + mockCline.ask = vi.fn().mockResolvedValue({ response: "yesButtonClicked" }) + mockCline.presentAssistantMessage = vi.fn() + mockCline.handleError = vi.fn().mockResolvedValue(undefined) + mockCline.pushToolResult = vi.fn() + mockCline.removeClosingTag = vi.fn((tag, content) => content) + mockCline.fileContextTracker = { + trackFileContext: vi.fn().mockResolvedValue(undefined), + } + mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) + mockCline.recordToolError = vi.fn().mockReturnValue(undefined) + setImageSupport(mockCline, true) + + const secondBatch = [{ path: "test/second.png", sizeKB: 15360 }] // 15MB + + // Clear and reset file system mocks for second batch + fsPromises.stat.mockClear() + fsPromises.readFile.mockClear() + mockedIsBinaryFile.mockClear() + mockedCountFileLines.mockClear() + + // Reset mocks for second batch + fsPromises.stat = vi.fn().mockResolvedValue({ size: 15360 * 1024 }) + fsPromises.readFile.mockResolvedValue(Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", "base64")) + mockedIsBinaryFile.mockResolvedValue(true) + mockedCountFileLines.mockResolvedValue(0) + mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) + + // Execute second batch + const result = await executeReadMultipleImagesTool(secondBatch.map(img => img.path)) + + // Verify second batch is processed successfully (memory tracking was reset) + expect(Array.isArray(result)).toBe(true) + const parts = result as any[] + const imageParts = parts.filter(p => p.type === "image") + + expect(imageParts).toHaveLength(1) // Second image should be processed + }) + }) }) describe("Error Handling Tests", () => { @@ -628,49 +1100,38 @@ describe("read_file tool with image support", () => { const mockedFsReadFile = vi.mocked(fsPromises.readFile) const mockedExtractTextFromFile = vi.mocked(extractTextFromFile) - const mockCline: any = {} - let mockProvider: any + let localMockCline: any + let localMockProvider: any let toolResult: ToolResponse | undefined beforeEach(() => { - vi.clearAllMocks() + // Clear specific mocks (not all mocks to preserve shared state) + mockedPathResolve.mockClear() + mockedIsBinaryFile.mockClear() + mockedCountFileLines.mockClear() + mockedFsReadFile.mockClear() + mockedExtractTextFromFile.mockClear() + toolResultMock.mockClear() + + // CRITICAL: Reset fsPromises.stat to prevent cross-test contamination + fsPromises.stat.mockClear() + fsPromises.stat.mockResolvedValue({ size: 1024 }) + + // Use shared mock setup function with local variables + const mocks = createMockCline() + localMockCline = mocks.mockCline + localMockProvider = mocks.mockProvider + + // CRITICAL: Explicitly ensure image support is enabled for all tests in this suite + setImageSupport(localMockCline, true) mockedPathResolve.mockReturnValue(absoluteImagePath) mockedIsBinaryFile.mockResolvedValue(true) mockedCountFileLines.mockResolvedValue(0) mockedFsReadFile.mockResolvedValue(imageBuffer) - mockProvider = { - getState: vi.fn().mockResolvedValue({ maxReadFileLine: -1 }), - deref: vi.fn().mockReturnThis(), - } - - mockCline.cwd = "/" - mockCline.task = "Test" - mockCline.providerRef = mockProvider - mockCline.rooIgnoreController = { - validateAccess: vi.fn().mockReturnValue(true), - } - mockCline.say = vi.fn().mockResolvedValue(undefined) - mockCline.ask = vi.fn().mockResolvedValue({ response: "yesButtonClicked" }) - mockCline.presentAssistantMessage = vi.fn() - mockCline.handleError = vi.fn().mockResolvedValue(undefined) - mockCline.pushToolResult = vi.fn() - mockCline.removeClosingTag = vi.fn((tag, content) => content) - - mockCline.fileContextTracker = { - trackFileContext: vi.fn().mockResolvedValue(undefined), - } - - mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) - mockCline.recordToolError = vi.fn().mockReturnValue(undefined) - - // Setup default API handler that supports images - mockCline.api = { - getModel: vi.fn().mockReturnValue({ - info: { supportsImages: true } - }) - } + // Setup mock provider with default maxReadFileLine + localMockProvider.getState.mockResolvedValue({ maxReadFileLine: -1 }) toolResult = undefined }) @@ -684,10 +1145,14 @@ describe("read_file tool with image support", () => { partial: false, } + // Debug: Check if mock is working + console.log("Mock API:", localMockCline.api) + console.log("Supports images:", localMockCline.api?.getModel?.()?.info?.supportsImages) + await readFileTool( - mockCline, + localMockCline, toolUse, - mockCline.ask, + localMockCline.ask, vi.fn(), (result: ToolResponse) => { toolResult = result @@ -695,6 +1160,9 @@ describe("read_file tool with image support", () => { (_: ToolParamName, content?: string) => content ?? "", ) + console.log("Result type:", Array.isArray(toolResult) ? "array" : typeof toolResult) + console.log("Result:", toolResult) + return toolResult } @@ -715,6 +1183,9 @@ describe("read_file tool with image support", () => { const absolutePath = `/test/${filename}` mockedPathResolve.mockReturnValue(absolutePath) + // Ensure API mock supports images + setImageSupport(localMockCline, true) + // Execute const result = await executeReadImageTool(imagePath) @@ -828,11 +1299,7 @@ describe("read_file tool with image support", () => { it("should exclude images when model does not support images", async () => { // Setup - mock API handler that doesn't support images - mockCline.api = { - getModel: vi.fn().mockReturnValue({ - info: { supportsImages: false } - }) - } + setImageSupport(localMockCline, false) // Execute const result = await executeReadImageTool() @@ -846,11 +1313,7 @@ describe("read_file tool with image support", () => { it("should include images when model supports images", async () => { // Setup - mock API handler that supports images - mockCline.api = { - getModel: vi.fn().mockReturnValue({ - info: { supportsImages: true } - }) - } + setImageSupport(localMockCline, true) // Execute const result = await executeReadImageTool() @@ -870,11 +1333,7 @@ describe("read_file tool with image support", () => { it("should handle undefined supportsImages gracefully", async () => { // Setup - mock API handler with undefined supportsImages - mockCline.api = { - getModel: vi.fn().mockReturnValue({ - info: { supportsImages: undefined } - }) - } + setImageSupport(localMockCline, undefined) // Execute const result = await executeReadImageTool() @@ -903,9 +1362,9 @@ describe("read_file tool with image support", () => { } await readFileTool( - mockCline, + localMockCline, toolUse, - mockCline.ask, + localMockCline.ask, handleErrorSpy, // Use our spy here (result: ToolResponse) => { toolResult = result diff --git a/src/core/tools/readFileTool.ts b/src/core/tools/readFileTool.ts index aa6b3c06c7..fa55008d07 100644 --- a/src/core/tools/readFileTool.ts +++ b/src/core/tools/readFileTool.ts @@ -21,6 +21,12 @@ import * as fs from "fs/promises" */ const DEFAULT_MAX_IMAGE_FILE_SIZE_MB = 5 +/** + * Default maximum total memory usage for all images in a single read operation (20MB) + * This prevents memory issues when reading multiple large images simultaneously + */ +const DEFAULT_MAX_TOTAL_IMAGE_MEMORY_MB = 20 + /** * Supported image formats that can be displayed */ @@ -136,6 +142,10 @@ export async function readFileTool( const legacyStartLineStr: string | undefined = block.params.start_line const legacyEndLineStr: string | undefined = block.params.end_line + // Check if the current model supports images at the beginning + const modelInfo = cline.api.getModel().info + const supportsImages = modelInfo.supportsImages ?? false + // Handle partial message first if (block.partial) { let filePath = "" @@ -473,6 +483,11 @@ export async function readFileTool( } } + // Track total image memory usage across all files + let totalImageMemoryUsed = 0 + const state = await cline.providerRef.deref()?.getState() + const { maxReadFileLine = -1, maxImageFileSize = DEFAULT_MAX_IMAGE_FILE_SIZE_MB, maxTotalImageMemory = DEFAULT_MAX_TOTAL_IMAGE_MEMORY_MB } = state ?? {} + // Then process only approved files for (const fileResult of fileResults) { // Skip files that weren't approved @@ -482,7 +497,6 @@ export async function readFileTool( const relPath = fileResult.path const fullPath = path.resolve(cline.cwd, relPath) - const { maxReadFileLine = -1, maxImageFileSize = DEFAULT_MAX_IMAGE_FILE_SIZE_MB } = (await cline.providerRef.deref()?.getState()) ?? {} // Process approved files try { @@ -495,10 +509,23 @@ export async function readFileTool( // Check if it's a supported image format if (SUPPORTED_IMAGE_FORMATS.includes(fileExtension as (typeof SUPPORTED_IMAGE_FORMATS)[number])) { + // Skip image processing if model doesn't support images + if (!supportsImages) { + const notice = "Image file detected but current model does not support images. Skipping image processing." + + // Track file read + await cline.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource) + + updateFileResult(relPath, { + xmlContent: `${relPath}\n${notice}\n`, + }) + continue + } + try { const imageStats = await fs.stat(fullPath) - // Check if image file exceeds size limit + // Check if image file exceeds individual size limit if (imageStats.size > maxImageFileSize * 1024 * 1024) { const imageSizeInMB = (imageStats.size / (1024 * 1024)).toFixed(1) const notice = t("tools:readFile.imageTooLarge", { size: imageSizeInMB, max: maxImageFileSize }) @@ -512,6 +539,23 @@ export async function readFileTool( continue } + // Check if adding this image would exceed total memory limit + const imageSizeInMB = imageStats.size / (1024 * 1024) + if (totalImageMemoryUsed + imageSizeInMB > maxTotalImageMemory) { + const notice = `Image skipped to prevent memory issues. Total image memory would exceed ${maxTotalImageMemory}MB limit (current: ${totalImageMemoryUsed.toFixed(1)}MB, this file: ${imageSizeInMB.toFixed(1)}MB). Consider reading fewer images at once or reducing image sizes.` + + // Track file read + await cline.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource) + + updateFileResult(relPath, { + xmlContent: `${relPath}\n${notice}\n`, + }) + continue + } + + // Track memory usage for this image + totalImageMemoryUsed += imageSizeInMB + const { dataUrl: imageDataUrl, buffer } = await readImageAsDataUrlWithBuffer(fullPath) const imageSizeInKB = Math.round(imageStats.size / 1024) @@ -702,8 +746,7 @@ export async function readFileTool( // Combine all images: feedback images first, then file images const allImages = [...feedbackImages, ...fileImageUrls] - // Check if the current model supports images before including them - const supportsImages = cline.api.getModel().info.supportsImages ?? false + // Use the supportsImages check from the beginning of the function const imagesToInclude = supportsImages ? allImages : [] // Push the result with appropriate formatting diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 2efb6d96e2..c55f68b1fa 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1426,6 +1426,7 @@ export class ClineProvider language, maxReadFileLine, maxImageFileSize, + maxTotalImageMemory, terminalCompressProgressBar, historyPreviewCollapsed, cloudUserInfo, @@ -1534,6 +1535,7 @@ export class ClineProvider renderContext: this.renderContext, maxReadFileLine: maxReadFileLine ?? -1, maxImageFileSize: maxImageFileSize ?? 5, + maxTotalImageMemory: maxTotalImageMemory ?? 20, maxConcurrentFileReads: maxConcurrentFileReads ?? 5, settingsImportedAt: this.settingsImportedAt, terminalCompressProgressBar: terminalCompressProgressBar ?? true, @@ -1705,6 +1707,7 @@ export class ClineProvider showRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? true, maxReadFileLine: stateValues.maxReadFileLine ?? -1, maxImageFileSize: stateValues.maxImageFileSize ?? 5, + maxTotalImageMemory: stateValues.maxTotalImageMemory ?? 20, maxConcurrentFileReads: stateValues.maxConcurrentFileReads ?? 5, historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false, cloudUserInfo, diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index b3991d2973..21232f6649 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -534,6 +534,7 @@ describe("ClineProvider", () => { renderContext: "sidebar", maxReadFileLine: 500, maxImageFileSize: 5, + maxTotalImageMemory: 20, cloudUserInfo: null, organizationAllowList: ORGANIZATION_ALLOW_ALL, autoCondenseContext: true, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 2e1d848642..cd309f363b 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1269,6 +1269,10 @@ export const webviewMessageHandler = async ( await updateGlobalState("maxImageFileSize", message.value) await provider.postStateToWebview() break + case "maxTotalImageMemory": + await updateGlobalState("maxTotalImageMemory", message.value) + await provider.postStateToWebview() + break case "maxConcurrentFileReads": const valueToSave = message.value // Capture the value intended for saving await updateGlobalState("maxConcurrentFileReads", valueToSave) diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 49151a7e96..46dd8d66ad 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -279,6 +279,7 @@ export type ExtensionState = Pick< showRooIgnoredFiles: boolean // Whether to show .rooignore'd files in listings maxReadFileLine: number // Maximum number of lines to read from a file before truncating maxImageFileSize: number // Maximum size of image files to process in MB + maxTotalImageMemory: number // Maximum total memory for all images in a single read operation in MB experiments: Experiments // Map of experiment IDs to their enabled state diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 2ea42eb75e..76add0a43f 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -163,6 +163,7 @@ export interface WebviewMessage { | "language" | "maxReadFileLine" | "maxImageFileSize" + | "maxTotalImageMemory" | "maxConcurrentFileReads" | "includeDiagnosticMessages" | "maxDiagnosticMessages" diff --git a/webview-ui/src/components/settings/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx index b21ad8258a..7a207b6895 100644 --- a/webview-ui/src/components/settings/ContextManagementSettings.tsx +++ b/webview-ui/src/components/settings/ContextManagementSettings.tsx @@ -21,6 +21,7 @@ type ContextManagementSettingsProps = HTMLAttributes & { showRooIgnoredFiles?: boolean maxReadFileLine?: number maxImageFileSize?: number + maxTotalImageMemory?: number maxConcurrentFileReads?: number profileThresholds?: Record includeDiagnosticMessages?: boolean @@ -34,6 +35,7 @@ type ContextManagementSettingsProps = HTMLAttributes & { | "showRooIgnoredFiles" | "maxReadFileLine" | "maxImageFileSize" + | "maxTotalImageMemory" | "maxConcurrentFileReads" | "profileThresholds" | "includeDiagnosticMessages" @@ -52,6 +54,7 @@ export const ContextManagementSettings = ({ setCachedStateField, maxReadFileLine, maxImageFileSize, + maxTotalImageMemory, maxConcurrentFileReads, profileThresholds = {}, includeDiagnosticMessages, @@ -237,6 +240,34 @@ export const ContextManagementSettings = ({
+
+
+ {t("settings:contextManagement.maxTotalImageMemory.label")} +
+ { + const newValue = parseInt(e.target.value, 10) + if (!isNaN(newValue) && newValue >= 1 && newValue <= 500) { + setCachedStateField("maxTotalImageMemory", newValue) + } + }} + onClick={(e) => e.currentTarget.select()} + data-testid="max-total-image-memory-input" + /> + {t("settings:contextManagement.maxTotalImageMemory.mb")} +
+
+
+ {t("settings:contextManagement.maxTotalImageMemory.description")} +
+
+
(({ onDone, t remoteBrowserEnabled, maxReadFileLine, maxImageFileSize, + maxTotalImageMemory, terminalCompressProgressBar, maxConcurrentFileReads, condensingApiConfigId, @@ -323,6 +324,7 @@ const SettingsView = forwardRef(({ onDone, t vscode.postMessage({ type: "showRooIgnoredFiles", bool: showRooIgnoredFiles }) vscode.postMessage({ type: "maxReadFileLine", value: maxReadFileLine ?? -1 }) vscode.postMessage({ type: "maxImageFileSize", value: maxImageFileSize ?? 5 }) + vscode.postMessage({ type: "maxTotalImageMemory", value: maxTotalImageMemory ?? 20 }) vscode.postMessage({ type: "maxConcurrentFileReads", value: cachedState.maxConcurrentFileReads ?? 5 }) vscode.postMessage({ type: "includeDiagnosticMessages", bool: includeDiagnosticMessages }) vscode.postMessage({ type: "maxDiagnosticMessages", value: maxDiagnosticMessages ?? 50 }) @@ -670,6 +672,7 @@ const SettingsView = forwardRef(({ onDone, t showRooIgnoredFiles={showRooIgnoredFiles} maxReadFileLine={maxReadFileLine} maxImageFileSize={maxImageFileSize} + maxTotalImageMemory={maxTotalImageMemory} maxConcurrentFileReads={maxConcurrentFileReads} profileThresholds={profileThresholds} includeDiagnosticMessages={includeDiagnosticMessages} diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 3d117aa7f6..16434c35d8 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -122,6 +122,8 @@ export interface ExtensionStateContextType extends ExtensionState { setMaxReadFileLine: (value: number) => void maxImageFileSize: number setMaxImageFileSize: (value: number) => void + maxTotalImageMemory: number + setMaxTotalImageMemory: (value: number) => void machineId?: string pinnedApiConfigs?: Record setPinnedApiConfigs: (value: Record) => void @@ -211,6 +213,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode renderContext: "sidebar", maxReadFileLine: -1, // Default max read file line limit maxImageFileSize: 5, // Default max image file size in MB + maxTotalImageMemory: 20, // Default max total image memory in MB pinnedApiConfigs: {}, // Empty object for pinned API configs terminalZshOhMy: false, // Default Oh My Zsh integration setting maxConcurrentFileReads: 5, // Default concurrent file reads @@ -452,6 +455,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setAwsUsePromptCache: (value) => setState((prevState) => ({ ...prevState, awsUsePromptCache: value })), setMaxReadFileLine: (value) => setState((prevState) => ({ ...prevState, maxReadFileLine: value })), setMaxImageFileSize: (value) => setState((prevState) => ({ ...prevState, maxImageFileSize: value })), + setMaxTotalImageMemory: (value) => setState((prevState) => ({ ...prevState, maxTotalImageMemory: value })), setPinnedApiConfigs: (value) => setState((prevState) => ({ ...prevState, pinnedApiConfigs: value })), setTerminalCompressProgressBar: (value) => setState((prevState) => ({ ...prevState, terminalCompressProgressBar: value })), diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx index 23f3940941..5b4874a6ff 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx @@ -209,7 +209,8 @@ describe("mergeExtensionState", () => { sharingEnabled: false, profileThresholds: {}, hasOpenedModeSelector: false, // Add the new required property - maxImageFileSize: 5 + maxImageFileSize: 5, + maxTotalImageMemory: 20 } const prevState: ExtensionState = { diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 8f7db4cac2..5ae9f49720 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -531,6 +531,11 @@ "label": "Mida màxima d'arxiu d'imatge", "mb": "MB", "description": "Mida màxima (en MB) per a arxius d'imatge que poden ser processats per l'eina de lectura d'arxius." + }, + "maxTotalImageMemory": { + "label": "Memòria total màxima per a imatges", + "mb": "MB", + "description": "Memòria total màxima (en MB) per a totes les imatges en una sola operació de lectura. Prevé problemes de memòria en llegir múltiples imatges grans simultàniament." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 1937cb053c..36cd9a339d 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -531,6 +531,11 @@ "label": "Maximale Bilddateigröße", "mb": "MB", "description": "Maximale Größe (in MB) für Bilddateien, die vom read file Tool verarbeitet werden können." + }, + "maxTotalImageMemory": { + "label": "Maximaler Gesamtspeicher für Bilder", + "mb": "MB", + "description": "Maximaler Gesamtspeicher (in MB) für alle Bilder in einem einzelnen Lesevorgang. Verhindert Speicherprobleme beim gleichzeitigen Lesen mehrerer großer Bilder." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index c8ad99c62d..028eecacb6 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -507,6 +507,11 @@ "mb": "MB", "description": "Maximum size (in MB) for image files that can be processed by the read file tool." }, + "maxTotalImageMemory": { + "label": "Max total image memory", + "mb": "MB", + "description": "Maximum total memory (in MB) for all images in a single read operation. Prevents memory issues when reading multiple large images simultaneously." + }, "diagnostics": { "includeMessages": { "label": "Automatically include diagnostics in context", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index e54d770991..cccfbaa73c 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -507,6 +507,11 @@ "mb": "MB", "description": "Tamaño máximo (en MB) para archivos de imagen que pueden ser procesados por la herramienta de lectura de archivos." }, + "maxTotalImageMemory": { + "label": "Memoria total máxima para imágenes", + "mb": "MB", + "description": "Memoria total máxima (en MB) para todas las imágenes en una sola operación de lectura. Previene problemas de memoria al leer múltiples imágenes grandes simultáneamente." + }, "diagnostics": { "includeMessages": { "label": "Incluir automáticamente diagnósticos en el contexto", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 54a1092c2f..3f5b0e9530 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -507,6 +507,11 @@ "mb": "MB", "description": "Taille maximale (en MB) pour les fichiers d'image qui peuvent être traités par l'outil de lecture de fichier." }, + "maxTotalImageMemory": { + "label": "Mémoire totale maximale pour les images", + "mb": "MB", + "description": "Mémoire totale maximale (en MB) pour toutes les images dans une seule opération de lecture. Empêche les problèmes de mémoire lors de la lecture simultanée de plusieurs grandes images." + }, "diagnostics": { "includeMessages": { "label": "Inclure automatiquement les diagnostics dans le contexte", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 77252093ae..69fc029eaa 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -532,6 +532,11 @@ "label": "अधिकतम छवि फ़ाइल आकार", "mb": "MB", "description": "छवि फ़ाइलों के लिए अधिकतम आकार (MB में) जो read file tool द्वारा प्रसंस्कृत किया जा सकता है।" + }, + "maxTotalImageMemory": { + "label": "छवियों के लिए अधिकतम कुल मेमोरी", + "mb": "MB", + "description": "एक ही पठन ऑपरेशन में सभी छवियों के लिए अधिकतम कुल मेमोरी (MB में)। एक साथ कई बड़ी छवियों को पढ़ते समय मेमोरी समस्याओं को रोकता है।" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 8c6fd70432..252a20c1cd 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -536,6 +536,11 @@ "label": "Ukuran file gambar maksimum", "mb": "MB", "description": "Ukuran maksimum (dalam MB) untuk file gambar yang dapat diproses oleh alat baca file." + }, + "maxTotalImageMemory": { + "label": "Total memori maksimum untuk gambar", + "mb": "MB", + "description": "Total memori maksimum (dalam MB) untuk semua gambar dalam satu operasi baca. Mencegah masalah memori saat membaca beberapa gambar besar secara bersamaan." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 54b29140ea..eb6a6dfd5f 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -532,6 +532,11 @@ "label": "Dimensione massima file immagine", "mb": "MB", "description": "Dimensione massima (in MB) per i file immagine che possono essere elaborati dallo strumento di lettura file." + }, + "maxTotalImageMemory": { + "label": "Memoria totale massima per le immagini", + "mb": "MB", + "description": "Memoria totale massima (in MB) per tutte le immagini in una singola operazione di lettura. Previene problemi di memoria durante la lettura simultanea di più immagini grandi." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index e77b8f9d23..8e8d7dad4c 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -532,6 +532,11 @@ "label": "最大画像ファイルサイズ", "mb": "MB", "description": "read fileツールで処理できる画像ファイルの最大サイズ(MB単位)。" + }, + "maxTotalImageMemory": { + "label": "画像の最大合計メモリ", + "mb": "MB", + "description": "単一の読み取り操作ですべての画像に使用できる最大合計メモリ(MB単位)。複数の大きな画像を同時に読み取る際のメモリ問題を防ぎます。" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index d2ee15b67f..550e35320e 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -532,6 +532,11 @@ "label": "최대 이미지 파일 크기", "mb": "MB", "description": "read file 도구로 처리할 수 있는 이미지 파일의 최대 크기(MB 단위)입니다." + }, + "maxTotalImageMemory": { + "label": "이미지 최대 총 메모리", + "mb": "MB", + "description": "단일 읽기 작업에서 모든 이미지에 대한 최대 총 메모리(MB 단위). 여러 대용량 이미지를 동시에 읽을 때 메모리 문제를 방지합니다." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 3dd0806cbd..9cf0434274 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -507,6 +507,11 @@ "mb": "MB", "description": "Maximale grootte (in MB) voor afbeeldingsbestanden die kunnen worden verwerkt door de read file tool." }, + "maxTotalImageMemory": { + "label": "Maximaal totaal afbeeldingsgeheugen", + "mb": "MB", + "description": "Maximaal totaal geheugen (in MB) voor alle afbeeldingen in één leesbewerking. Voorkomt geheugenproblemen bij het gelijktijdig lezen van meerdere grote afbeeldingen." + }, "diagnostics": { "includeMessages": { "label": "Automatisch diagnostiek opnemen in context", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 37586b2ebc..bfacaea3b3 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -532,6 +532,11 @@ "label": "Maksymalny rozmiar pliku obrazu", "mb": "MB", "description": "Maksymalny rozmiar (w MB) plików obrazów, które mogą być przetwarzane przez narzędzie do czytania plików." + }, + "maxTotalImageMemory": { + "label": "Maksymalna całkowita pamięć dla obrazów", + "mb": "MB", + "description": "Maksymalna całkowita pamięć (w MB) dla wszystkich obrazów w jednej operacji odczytu. Zapobiega problemom z pamięcią podczas jednoczesnego odczytu wielu dużych obrazów." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 46cd6cfef3..52c6720373 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -532,6 +532,11 @@ "label": "Tamanho máximo do arquivo de imagem", "mb": "MB", "description": "Tamanho máximo (em MB) para arquivos de imagem que podem ser processados pela ferramenta de leitura de arquivos." + }, + "maxTotalImageMemory": { + "label": "Memória total máxima para imagens", + "mb": "MB", + "description": "Memória total máxima (em MB) para todas as imagens em uma única operação de leitura. Evita problemas de memória ao ler múltiplas imagens grandes simultaneamente." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index b9163e910c..2927c30f94 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -532,6 +532,11 @@ "label": "Максимальный размер файла изображения", "mb": "MB", "description": "Максимальный размер (в МБ) для файлов изображений, которые могут быть обработаны инструментом чтения файлов." + }, + "maxTotalImageMemory": { + "label": "Максимальная общая память для изображений", + "mb": "MB", + "description": "Максимальная общая память (в МБ) для всех изображений в одной операции чтения. Предотвращает проблемы с памятью при одновременном чтении нескольких больших изображений." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index db0e45a835..d3c225c646 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -532,6 +532,11 @@ "label": "Maksimum görüntü dosyası boyutu", "mb": "MB", "description": "Dosya okuma aracı tarafından işlenebilecek görüntü dosyaları için maksimum boyut (MB cinsinden)." + }, + "maxTotalImageMemory": { + "label": "Görüntüler için maksimum toplam bellek", + "mb": "MB", + "description": "Tek bir okuma işlemindeki tüm görüntüler için maksimum toplam bellek (MB cinsinden). Birden çok büyük görüntüyü eş zamanlı okurken bellek sorunlarını önler." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 2116950fd5..56b0c98859 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -532,6 +532,11 @@ "label": "Kích thước tối đa của tệp hình ảnh", "mb": "MB", "description": "Kích thước tối đa (tính bằng MB) cho các tệp hình ảnh có thể được xử lý bởi công cụ đọc tệp." + }, + "maxTotalImageMemory": { + "label": "Bộ nhớ tổng tối đa cho hình ảnh", + "mb": "MB", + "description": "Bộ nhớ tổng tối đa (tính bằng MB) cho tất cả hình ảnh trong một hoạt động đọc. Ngăn ngừa vấn đề bộ nhớ khi đọc đồng thời nhiều hình ảnh lớn." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index b11202e271..264d2d4933 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -532,6 +532,11 @@ "label": "最大图像文件大小", "mb": "MB", "description": "read file工具可以处理的图像文件的最大大小(以MB为单位)。" + }, + "maxTotalImageMemory": { + "label": "图像最大总内存", + "mb": "MB", + "description": "单次读取操作中所有图像的最大总内存(以MB为单位)。防止同时读取多个大图像时出现内存问题。" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 023ade21ff..be55a5de41 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -532,6 +532,11 @@ "label": "最大圖像檔案大小", "mb": "MB", "description": "read file工具可以處理的圖像檔案的最大大小(以MB為單位)。" + }, + "maxTotalImageMemory": { + "label": "圖像最大總記憶體", + "mb": "MB", + "description": "單次讀取操作中所有圖像的最大總記憶體(以MB為單位)。防止同時讀取多個大圖像時出現記憶體問題。" } }, "terminal": { From ba397ced2312c67ef1f0c726a10117d9a605cb4c Mon Sep 17 00:00:00 2001 From: Sam Hoang Date: Sun, 20 Jul 2025 01:39:13 +0700 Subject: [PATCH 10/21] refactor: consolidate MIME type mapping for image formats --- src/core/tools/readFileTool.ts | 50 ++++++++++++---------------------- 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/src/core/tools/readFileTool.ts b/src/core/tools/readFileTool.ts index fa55008d07..07b7753cda 100644 --- a/src/core/tools/readFileTool.ts +++ b/src/core/tools/readFileTool.ts @@ -44,6 +44,20 @@ const SUPPORTED_IMAGE_FORMATS = [ ".avif", ] as const +const IMAGE_MIME_TYPES: Record = { + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".webp": "image/webp", + ".svg": "image/svg+xml", + ".bmp": "image/bmp", + ".ico": "image/x-icon", + ".tiff": "image/tiff", + ".tif": "image/tiff", + ".avif": "image/avif", +} + /** * Reads an image file and returns both the data URL and buffer */ @@ -52,24 +66,9 @@ async function readImageAsDataUrlWithBuffer(filePath: string): Promise<{ dataUrl const base64 = fileBuffer.toString("base64") const ext = path.extname(filePath).toLowerCase() - // Map extensions to MIME types - const mimeTypes: Record = { - ".png": "image/png", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".gif": "image/gif", - ".webp": "image/webp", - ".svg": "image/svg+xml", - ".bmp": "image/bmp", - ".ico": "image/x-icon", - ".tiff": "image/tiff", - ".tif": "image/tiff", - ".avif": "image/avif", - } - - const mimeType = mimeTypes[ext] || "image/png" + const mimeType = IMAGE_MIME_TYPES[ext] || "image/png" const dataUrl = `data:${mimeType};base64,${base64}` - + return { dataUrl, buffer: fileBuffer } } @@ -559,26 +558,11 @@ export async function readFileTool( const { dataUrl: imageDataUrl, buffer } = await readImageAsDataUrlWithBuffer(fullPath) const imageSizeInKB = Math.round(imageStats.size / 1024) - // For images, get dimensions if possible - let dimensionsInfo = "" - if (fileExtension === ".png") { - // Simple PNG dimension extraction (first 24 bytes contain width/height) - if (buffer.length >= 24) { - const width = buffer.readUInt32BE(16) - const height = buffer.readUInt32BE(20) - if (width && height) { - dimensionsInfo = `${width}x${height} pixels` - } - } - } - // Track file read await cline.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource) // Store image data URL separately - NOT in XML - const noticeText = dimensionsInfo - ? t("tools:readFile.imageWithDimensions", { dimensions: dimensionsInfo, size: imageSizeInKB }) - : t("tools:readFile.imageWithSize", { size: imageSizeInKB }) + const noticeText = t("tools:readFile.imageWithSize", { size: imageSizeInKB }) updateFileResult(relPath, { xmlContent: `${relPath}\n${noticeText}\n`, From 802fa5dcaffa0e13383cdd2182b7c22b12226d5b Mon Sep 17 00:00:00 2001 From: Sam Hoang Date: Sun, 20 Jul 2025 01:43:11 +0700 Subject: [PATCH 11/21] fix: re-check model image support before including images in the result --- src/core/tools/readFileTool.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/tools/readFileTool.ts b/src/core/tools/readFileTool.ts index 07b7753cda..9ab2f9b26e 100644 --- a/src/core/tools/readFileTool.ts +++ b/src/core/tools/readFileTool.ts @@ -730,8 +730,9 @@ export async function readFileTool( // Combine all images: feedback images first, then file images const allImages = [...feedbackImages, ...fileImageUrls] - // Use the supportsImages check from the beginning of the function - const imagesToInclude = supportsImages ? allImages : [] + // Re-check if the model supports images before including them, in case it changed during execution. + const finalModelSupportsImages = cline.api.getModel().info.supportsImages ?? false + const imagesToInclude = finalModelSupportsImages ? allImages : [] // Push the result with appropriate formatting if (statusMessage || imagesToInclude.length > 0) { From ebca4ba33274da5542db72460e05244d61ec1d2a Mon Sep 17 00:00:00 2001 From: Sam Hoang Date: Mon, 21 Jul 2025 16:22:00 +0700 Subject: [PATCH 12/21] fix test --- src/core/tools/__tests__/readFileTool.spec.ts | 36 ------------------- 1 file changed, 36 deletions(-) diff --git a/src/core/tools/__tests__/readFileTool.spec.ts b/src/core/tools/__tests__/readFileTool.spec.ts index 9afe35c3b6..e8f44d35af 100644 --- a/src/core/tools/__tests__/readFileTool.spec.ts +++ b/src/core/tools/__tests__/readFileTool.spec.ts @@ -1244,42 +1244,6 @@ describe("read_file tool with image support", () => { expect(imagesArg![0]).toBe(`data:image/png;base64,${base64ImageData}`) }) - it("should extract and display PNG dimensions correctly", async () => { - // Setup - Create a proper PNG buffer with known dimensions (100x200 pixels) - const pngSignature = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) // PNG signature - const ihdrLength = Buffer.from([0x00, 0x00, 0x00, 0x0D]) // IHDR chunk length (13 bytes) - const ihdrType = Buffer.from("IHDR", "ascii") // IHDR chunk type - const width = Buffer.alloc(4) - width.writeUInt32BE(100, 0) // Width: 100 pixels - const height = Buffer.alloc(4) - height.writeUInt32BE(200, 0) // Height: 200 pixels - const ihdrData = Buffer.from([0x08, 0x02, 0x00, 0x00, 0x00]) // Bit depth, color type, compression, filter, interlace - const crc = Buffer.from([0x00, 0x00, 0x00, 0x00]) // Dummy CRC - - const pngBuffer = Buffer.concat([pngSignature, ihdrLength, ihdrType, width, height, ihdrData, crc]) - const pngBase64 = pngBuffer.toString("base64") - - mockedFsReadFile.mockResolvedValue(pngBuffer) - - // Execute - const result = await executeReadImageTool() - - // Verify result is a multi-part response - expect(Array.isArray(result)).toBe(true) - const textPart = (result as any[]).find((p) => p.type === "text")?.text - const imagePart = (result as any[]).find((p) => p.type === "image") - - // Verify text part contains dimensions - expect(textPart).toContain(`${testImagePath}`) - expect(textPart).toContain("100x200 pixels") // Should include the dimensions - expect(textPart).toContain(`Image file`) - - // Verify image part - expect(imagePart).toBeDefined() - expect(imagePart.source.media_type).toBe("image/png") - expect(imagePart.source.data).toBe(pngBase64) - }) - it("should handle large image files", async () => { // Setup - simulate a large image const largeBase64 = "A".repeat(1000000) // 1MB of base64 data From af05c5c5f4509a8a3825d201108d73b8240072b4 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Tue, 22 Jul 2025 11:05:28 -0500 Subject: [PATCH 13/21] refactor: move image processing helpers to separate file - Extract image-related constants and functions to src/core/tools/helpers/imageHelpers.ts - Keep readFileTool.ts focused on the main file reading logic - Update imports in test file to use the new helper module - All tests passing after refactoring --- src/core/tools/__tests__/readFileTool.spec.ts | 311 +++++++++++------- src/core/tools/helpers/imageHelpers.ts | 65 ++++ src/core/tools/readFileTool.ts | 81 ++--- 3 files changed, 276 insertions(+), 181 deletions(-) create mode 100644 src/core/tools/helpers/imageHelpers.ts diff --git a/src/core/tools/__tests__/readFileTool.spec.ts b/src/core/tools/__tests__/readFileTool.spec.ts index e8f44d35af..917abdd7c8 100644 --- a/src/core/tools/__tests__/readFileTool.spec.ts +++ b/src/core/tools/__tests__/readFileTool.spec.ts @@ -10,6 +10,7 @@ import { isBinaryFile } from "isbinaryfile" import { ReadFileToolUse, ToolParamName, ToolResponse } from "../../../shared/tools" import { readFileTool } from "../readFileTool" import { formatResponse } from "../../prompts/responses" +import { DEFAULT_MAX_IMAGE_FILE_SIZE_MB, DEFAULT_MAX_TOTAL_IMAGE_MEMORY_MB } from "../helpers/imageHelpers" vi.mock("path", async () => { const originalPath = await vi.importActual("path") @@ -121,7 +122,7 @@ vi.mock("../../../utils/fs", () => ({ beforeEach(() => { // NOTE: Removed vi.clearAllMocks() to prevent interference with setImageSupport calls // Instead, individual suites clear their specific mocks to maintain isolation - + // Explicitly reset the hoisted mock implementations to prevent cross-suite pollution toolResultMock.mockImplementation((text: string, images?: string[]) => { if (images && images.length > 0) { @@ -136,7 +137,7 @@ beforeEach(() => { } return text }) - + imageBlocksMock.mockImplementation((images?: string[]) => { return images ? images.map((img) => { @@ -155,21 +156,22 @@ vi.mock("../../../i18n", () => ({ const translations: Record = { "tools:readFile.imageWithDimensions": "Image file ({{dimensions}}, {{size}} KB)", "tools:readFile.imageWithSize": "Image file ({{size}} KB)", - "tools:readFile.imageTooLarge": "Image file is too large ({{size}} MB). The maximum allowed size is {{max}} MB.", + "tools:readFile.imageTooLarge": + "Image file is too large ({{size}} MB). The maximum allowed size is {{max}} MB.", "tools:readFile.linesRange": " (lines {{start}}-{{end}})", "tools:readFile.definitionsOnly": " (definitions only)", "tools:readFile.maxLines": " (max {{max}} lines)", } - + let result = translations[key] || key - + // Simple template replacement if (params) { Object.entries(params).forEach(([param, value]) => { - result = result.replace(new RegExp(`{{${param}}}`, 'g'), String(value)) + result = result.replace(new RegExp(`{{${param}}}`, "g"), String(value)) }) } - + return result }), })) @@ -203,9 +205,9 @@ function createMockCline(): any { // CRITICAL: Always ensure image support is enabled api: { getModel: vi.fn().mockReturnValue({ - info: { supportsImages: true } - }) - } + info: { supportsImages: true }, + }), + }, } return { mockCline, mockProvider } @@ -215,8 +217,8 @@ function createMockCline(): any { function setImageSupport(mockCline: any, supportsImages: boolean | undefined): void { mockCline.api = { getModel: vi.fn().mockReturnValue({ - info: { supportsImages } - }) + info: { supportsImages }, + }), } } @@ -590,7 +592,11 @@ describe("read_file tool XML output structure", () => { addLineNumbersMock(mockInputContent) return Promise.resolve(numberedContent) }) - mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, maxTotalImageMemory: 20 }) // Allow up to 20MB per image and total memory + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 20, + maxTotalImageMemory: 20, + }) // Allow up to 20MB per image and total memory // Execute const result = await executeReadFileTool() @@ -619,7 +625,11 @@ describe("read_file tool XML output structure", () => { // Setup mockedCountFileLines.mockResolvedValue(0) mockedExtractTextFromFile.mockResolvedValue("") - mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, maxTotalImageMemory: 20 }) // Allow up to 20MB per image and total memory + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 20, + maxTotalImageMemory: 20, + }) // Allow up to 20MB per image and total memory // Execute const result = await executeReadFileTool({}, { totalLines: 0 }) @@ -629,7 +639,7 @@ describe("read_file tool XML output structure", () => { `\n${testFilePath}\nFile is empty\n\n`, ) }) - + describe("Total Image Memory Limit", () => { const testImages = [ { path: "test/image1.png", sizeKB: 5120 }, // 5MB @@ -646,18 +656,18 @@ describe("read_file tool XML output structure", () => { async function executeReadMultipleImagesTool(imagePaths: string[]): Promise { // Ensure image support is enabled before calling the tool setImageSupport(mockCline, true) - + // Create args content for multiple files - const filesXml = imagePaths.map(path => `${path}`).join('') + const filesXml = imagePaths.map((path) => `${path}`).join("") const argsContent = filesXml - + const toolUse: ReadFileToolUse = { type: "tool_use", name: "read_file", params: { args: argsContent }, partial: false, } - + let localResult: ToolResponse | undefined await readFileTool( mockCline, @@ -669,19 +679,28 @@ describe("read_file tool XML output structure", () => { }, (_: ToolParamName, content?: string) => content ?? "", ) - + return localResult } - + it("should allow multiple images under the total memory limit", async () => { // Setup required mocks (don't clear all mocks - preserve API setup) mockedIsBinaryFile.mockResolvedValue(true) mockedCountFileLines.mockResolvedValue(0) - fsPromises.readFile.mockResolvedValue(Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", "base64")) - + fsPromises.readFile.mockResolvedValue( + Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", + "base64", + ), + ) + // Setup mockProvider - mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, maxTotalImageMemory: 20 }) // Allow up to 20MB per image and total memory - + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 20, + maxTotalImageMemory: 20, + }) // Allow up to 20MB per image and total memory + // Setup mockCline properties (preserve existing API) mockCline.cwd = "/" mockCline.task = "Test" @@ -701,51 +720,60 @@ describe("read_file tool XML output structure", () => { mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) mockCline.recordToolError = vi.fn().mockReturnValue(undefined) setImageSupport(mockCline, true) - + // Setup - images that fit within 20MB limit const smallImages = [ { path: "test/small1.png", sizeKB: 2048 }, // 2MB { path: "test/small2.jpg", sizeKB: 3072 }, // 3MB { path: "test/small3.gif", sizeKB: 4096 }, // 4MB ] // Total: 9MB (under 20MB limit) - + // Mock file stats for each image fsPromises.stat = vi.fn().mockImplementation((filePath) => { - const fileName = filePath.split('/').pop() - const image = smallImages.find(img => img.path.includes(fileName.split('.')[0])) + const fileName = filePath.split("/").pop() + const image = smallImages.find((img) => img.path.includes(fileName.split(".")[0])) return Promise.resolve({ size: (image?.sizeKB || 1024) * 1024 }) }) - + // Mock path.resolve for each image mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) - + // Execute - const result = await executeReadMultipleImagesTool(smallImages.map(img => img.path)) - + const result = await executeReadMultipleImagesTool(smallImages.map((img) => img.path)) + // Verify all images were processed (should be a multi-part response) expect(Array.isArray(result)).toBe(true) const parts = result as any[] - + // Should have text part and 3 image parts - const textPart = parts.find(p => p.type === "text")?.text - const imageParts = parts.filter(p => p.type === "image") - + const textPart = parts.find((p) => p.type === "text")?.text + const imageParts = parts.filter((p) => p.type === "image") + expect(textPart).toBeDefined() expect(imageParts).toHaveLength(3) - + // Verify no memory limit notices expect(textPart).not.toContain("Total image memory would exceed") }) - + it("should skip images that would exceed the total memory limit", async () => { // Setup required mocks (don't clear all mocks) mockedIsBinaryFile.mockResolvedValue(true) mockedCountFileLines.mockResolvedValue(0) - fsPromises.readFile.mockResolvedValue(Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", "base64")) - + fsPromises.readFile.mockResolvedValue( + Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", + "base64", + ), + ) + // Setup mockProvider - mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 15, maxTotalImageMemory: 20 }) // Allow up to 15MB per image and 20MB total memory - + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 15, + maxTotalImageMemory: 20, + }) // Allow up to 15MB per image and 20MB total memory + // Setup mockCline properties mockCline.cwd = "/" mockCline.task = "Test" @@ -765,7 +793,7 @@ describe("read_file tool XML output structure", () => { mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) mockCline.recordToolError = vi.fn().mockReturnValue(undefined) setImageSupport(mockCline, true) - + // Setup - images where later ones would exceed 20MB total limit // Each must be under 5MB per-file limit (5120KB) const largeImages = [ @@ -775,53 +803,64 @@ describe("read_file tool XML output structure", () => { { path: "test/large4.png", sizeKB: 5017 }, // ~4.9MB { path: "test/large5.jpg", sizeKB: 5017 }, // ~4.9MB - This should be skipped (total would be ~24.5MB > 20MB) ] - + // Mock file stats for each image fsPromises.stat = vi.fn().mockImplementation((filePath) => { const fileName = path.basename(filePath) const baseName = path.parse(fileName).name - const image = largeImages.find(img => img.path.includes(baseName)) + const image = largeImages.find((img) => img.path.includes(baseName)) return Promise.resolve({ size: (image?.sizeKB || 1024) * 1024 }) }) - + // Mock path.resolve for each image mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) - + // Execute - const result = await executeReadMultipleImagesTool(largeImages.map(img => img.path)) - + const result = await executeReadMultipleImagesTool(largeImages.map((img) => img.path)) + // Verify result structure - should be a mix of successful images and skipped notices expect(Array.isArray(result)).toBe(true) const parts = result as any[] - - const textPart = parts.find(p => p.type === "text")?.text - const imageParts = parts.filter(p => p.type === "image") - + + const textPart = parts.find((p) => p.type === "text")?.text + const imageParts = parts.filter((p) => p.type === "image") + expect(textPart).toBeDefined() - + // Debug: Show what we actually got vs expected if (imageParts.length !== 4) { - throw new Error(`Expected 4 images, got ${imageParts.length}. Full result: ${JSON.stringify(result, null, 2)}. Text part: ${textPart}`) + throw new Error( + `Expected 4 images, got ${imageParts.length}. Full result: ${JSON.stringify(result, null, 2)}. Text part: ${textPart}`, + ) } expect(imageParts).toHaveLength(4) // First 4 images should be included (~19.6MB total) - + // Verify memory limit notice for the fifth image expect(textPart).toContain("Total image memory would exceed 20MB limit") expect(textPart).toContain("current: 19.6MB") // 4 * 4.9MB expect(textPart).toContain("this file: 4.9MB") }) - + it("should track memory usage correctly across multiple images", async () => { // Setup mocks (don't clear all mocks) - + // Setup required mocks mockedIsBinaryFile.mockResolvedValue(true) mockedCountFileLines.mockResolvedValue(0) - fsPromises.readFile.mockResolvedValue(Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", "base64")) - + fsPromises.readFile.mockResolvedValue( + Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", + "base64", + ), + ) + // Setup mockProvider - mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 15, maxTotalImageMemory: 20 }) // Allow up to 15MB per image and 20MB total memory - + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 15, + maxTotalImageMemory: 20, + }) // Allow up to 15MB per image and 20MB total memory + // Setup mockCline properties mockCline.cwd = "/" mockCline.task = "Test" @@ -841,55 +880,64 @@ describe("read_file tool XML output structure", () => { mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) mockCline.recordToolError = vi.fn().mockReturnValue(undefined) setImageSupport(mockCline, true) - + // Setup - images that exactly reach the limit const exactLimitImages = [ { path: "test/exact1.png", sizeKB: 10240 }, // 10MB { path: "test/exact2.jpg", sizeKB: 10240 }, // 10MB - Total exactly 20MB { path: "test/exact3.gif", sizeKB: 1024 }, // 1MB - This should be skipped ] - + // Mock file stats with simpler logic fsPromises.stat = vi.fn().mockImplementation((filePath) => { - if (filePath.includes('exact1')) { + if (filePath.includes("exact1")) { return Promise.resolve({ size: 10240 * 1024 }) // 10MB - } else if (filePath.includes('exact2')) { + } else if (filePath.includes("exact2")) { return Promise.resolve({ size: 10240 * 1024 }) // 10MB - } else if (filePath.includes('exact3')) { + } else if (filePath.includes("exact3")) { return Promise.resolve({ size: 1024 * 1024 }) // 1MB } return Promise.resolve({ size: 1024 * 1024 }) // Default 1MB }) - + // Mock path.resolve mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) - + // Execute - const result = await executeReadMultipleImagesTool(exactLimitImages.map(img => img.path)) - + const result = await executeReadMultipleImagesTool(exactLimitImages.map((img) => img.path)) + // Verify expect(Array.isArray(result)).toBe(true) const parts = result as any[] - - const textPart = parts.find(p => p.type === "text")?.text - const imageParts = parts.filter(p => p.type === "image") - + + const textPart = parts.find((p) => p.type === "text")?.text + const imageParts = parts.filter((p) => p.type === "image") + expect(imageParts).toHaveLength(2) // First 2 images should fit expect(textPart).toContain("Total image memory would exceed 20MB limit") expect(textPart).toContain("current: 20.0MB") // Should show exactly 20MB used }) - + it("should handle individual image size limit and total memory limit together", async () => { // Setup mocks (don't clear all mocks) - + // Setup required mocks mockedIsBinaryFile.mockResolvedValue(true) mockedCountFileLines.mockResolvedValue(0) - fsPromises.readFile.mockResolvedValue(Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", "base64")) - + fsPromises.readFile.mockResolvedValue( + Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", + "base64", + ), + ) + // Setup mockProvider - mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, maxTotalImageMemory: 20 }) // Allow up to 20MB per image and total memory - + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 20, + maxTotalImageMemory: 20, + }) // Allow up to 20MB per image and total memory + // Setup mockCline properties (complete setup) mockCline.cwd = "/" mockCline.task = "Test" @@ -909,60 +957,69 @@ describe("read_file tool XML output structure", () => { mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) mockCline.recordToolError = vi.fn().mockReturnValue(undefined) setImageSupport(mockCline, true) - + // Setup - mix of images with individual size violations and total memory issues const mixedImages = [ { path: "test/ok.png", sizeKB: 3072 }, // 3MB - OK { path: "test/too-big.jpg", sizeKB: 6144 }, // 6MB - Exceeds individual 5MB limit { path: "test/ok2.gif", sizeKB: 4096 }, // 4MB - OK individually but might exceed total ] - + // Mock file stats fsPromises.stat = vi.fn().mockImplementation((filePath) => { const fileName = path.basename(filePath) const baseName = path.parse(fileName).name - const image = mixedImages.find(img => img.path.includes(baseName)) + const image = mixedImages.find((img) => img.path.includes(baseName)) return Promise.resolve({ size: (image?.sizeKB || 1024) * 1024 }) }) - + // Mock provider state with 5MB individual limit mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 5, - maxTotalImageMemory: 20 + maxTotalImageMemory: 20, }) - + // Mock path.resolve mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) - + // Execute - const result = await executeReadMultipleImagesTool(mixedImages.map(img => img.path)) - + const result = await executeReadMultipleImagesTool(mixedImages.map((img) => img.path)) + // Verify expect(Array.isArray(result)).toBe(true) const parts = result as any[] - - const textPart = parts.find(p => p.type === "text")?.text - const imageParts = parts.filter(p => p.type === "image") - + + const textPart = parts.find((p) => p.type === "text")?.text + const imageParts = parts.filter((p) => p.type === "image") + // Should have 2 images (ok.png and ok2.gif) expect(imageParts).toHaveLength(2) - + // Should show individual size limit violation expect(textPart).toContain("Image file is too large (6.0 MB). The maximum allowed size is 5 MB.") }) - + it("should reset total memory tracking for each tool invocation", async () => { // Setup mocks (don't clear all mocks) - + // Setup required mocks for first batch mockedIsBinaryFile.mockResolvedValue(true) mockedCountFileLines.mockResolvedValue(0) - fsPromises.readFile.mockResolvedValue(Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", "base64")) - + fsPromises.readFile.mockResolvedValue( + Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", + "base64", + ), + ) + // Setup mockProvider - mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, maxTotalImageMemory: 20 }) - + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 20, + maxTotalImageMemory: 20, + }) + // Setup mockCline properties (complete setup) mockCline.cwd = "/" mockCline.task = "Test" @@ -982,25 +1039,34 @@ describe("read_file tool XML output structure", () => { mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) mockCline.recordToolError = vi.fn().mockReturnValue(undefined) setImageSupport(mockCline, true) - + // Setup - first call with images that use memory const firstBatch = [{ path: "test/first.png", sizeKB: 10240 }] // 10MB - + fsPromises.stat = vi.fn().mockResolvedValue({ size: 10240 * 1024 }) mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) - + // Execute first batch - await executeReadMultipleImagesTool(firstBatch.map(img => img.path)) - + await executeReadMultipleImagesTool(firstBatch.map((img) => img.path)) + // Setup second batch (don't clear all mocks) mockedIsBinaryFile.mockResolvedValue(true) mockedCountFileLines.mockResolvedValue(0) - fsPromises.readFile.mockResolvedValue(Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", "base64")) - mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, maxTotalImageMemory: 20 }) - + fsPromises.readFile.mockResolvedValue( + Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", + "base64", + ), + ) + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 20, + maxTotalImageMemory: 20, + }) + // Reset path resolving for second batch mockedPathResolve.mockClear() - + // Re-setup mockCline properties for second batch (complete setup) mockCline.cwd = "/" mockCline.task = "Test" @@ -1020,30 +1086,35 @@ describe("read_file tool XML output structure", () => { mockCline.recordToolUsage = vi.fn().mockReturnValue(undefined) mockCline.recordToolError = vi.fn().mockReturnValue(undefined) setImageSupport(mockCline, true) - + const secondBatch = [{ path: "test/second.png", sizeKB: 15360 }] // 15MB - + // Clear and reset file system mocks for second batch fsPromises.stat.mockClear() fsPromises.readFile.mockClear() mockedIsBinaryFile.mockClear() mockedCountFileLines.mockClear() - + // Reset mocks for second batch fsPromises.stat = vi.fn().mockResolvedValue({ size: 15360 * 1024 }) - fsPromises.readFile.mockResolvedValue(Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", "base64")) + fsPromises.readFile.mockResolvedValue( + Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", + "base64", + ), + ) mockedIsBinaryFile.mockResolvedValue(true) mockedCountFileLines.mockResolvedValue(0) mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) - + // Execute second batch - const result = await executeReadMultipleImagesTool(secondBatch.map(img => img.path)) - + const result = await executeReadMultipleImagesTool(secondBatch.map((img) => img.path)) + // Verify second batch is processed successfully (memory tracking was reset) expect(Array.isArray(result)).toBe(true) const parts = result as any[] - const imageParts = parts.filter(p => p.type === "image") - + const imageParts = parts.filter((p) => p.type === "image") + expect(imageParts).toHaveLength(1) // Second image should be processed }) }) diff --git a/src/core/tools/helpers/imageHelpers.ts b/src/core/tools/helpers/imageHelpers.ts new file mode 100644 index 0000000000..dfe93ae585 --- /dev/null +++ b/src/core/tools/helpers/imageHelpers.ts @@ -0,0 +1,65 @@ +import path from "path" +import * as fs from "fs/promises" + +/** + * Default maximum allowed image file size in bytes (5MB) + */ +export const DEFAULT_MAX_IMAGE_FILE_SIZE_MB = 5 + +/** + * Default maximum total memory usage for all images in a single read operation (20MB) + * This prevents memory issues when reading multiple large images simultaneously + */ +export const DEFAULT_MAX_TOTAL_IMAGE_MEMORY_MB = 20 + +/** + * Supported image formats that can be displayed + */ +export const SUPPORTED_IMAGE_FORMATS = [ + ".png", + ".jpg", + ".jpeg", + ".gif", + ".webp", + ".svg", + ".bmp", + ".ico", + ".tiff", + ".tif", + ".avif", +] as const + +export const IMAGE_MIME_TYPES: Record = { + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".gif": "image/gif", + ".webp": "image/webp", + ".svg": "image/svg+xml", + ".bmp": "image/bmp", + ".ico": "image/x-icon", + ".tiff": "image/tiff", + ".tif": "image/tiff", + ".avif": "image/avif", +} + +/** + * Reads an image file and returns both the data URL and buffer + */ +export async function readImageAsDataUrlWithBuffer(filePath: string): Promise<{ dataUrl: string; buffer: Buffer }> { + const fileBuffer = await fs.readFile(filePath) + const base64 = fileBuffer.toString("base64") + const ext = path.extname(filePath).toLowerCase() + + const mimeType = IMAGE_MIME_TYPES[ext] || "image/png" + const dataUrl = `data:${mimeType};base64,${base64}` + + return { dataUrl, buffer: fileBuffer } +} + +/** + * Checks if a file extension is a supported image format + */ +export function isSupportedImageFormat(extension: string): boolean { + return SUPPORTED_IMAGE_FORMATS.includes(extension.toLowerCase() as (typeof SUPPORTED_IMAGE_FORMATS)[number]) +} diff --git a/src/core/tools/readFileTool.ts b/src/core/tools/readFileTool.ts index 9ab2f9b26e..b4ed3338d3 100644 --- a/src/core/tools/readFileTool.ts +++ b/src/core/tools/readFileTool.ts @@ -15,62 +15,13 @@ import { extractTextFromFile, addLineNumbers, getSupportedBinaryFormats } from " import { parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter" import { parseXml } from "../../utils/xml" import * as fs from "fs/promises" - -/** - * Default maximum allowed image file size in bytes (5MB) - */ -const DEFAULT_MAX_IMAGE_FILE_SIZE_MB = 5 - -/** - * Default maximum total memory usage for all images in a single read operation (20MB) - * This prevents memory issues when reading multiple large images simultaneously - */ -const DEFAULT_MAX_TOTAL_IMAGE_MEMORY_MB = 20 - -/** - * Supported image formats that can be displayed - */ -const SUPPORTED_IMAGE_FORMATS = [ - ".png", - ".jpg", - ".jpeg", - ".gif", - ".webp", - ".svg", - ".bmp", - ".ico", - ".tiff", - ".tif", - ".avif", -] as const - -const IMAGE_MIME_TYPES: Record = { - ".png": "image/png", - ".jpg": "image/jpeg", - ".jpeg": "image/jpeg", - ".gif": "image/gif", - ".webp": "image/webp", - ".svg": "image/svg+xml", - ".bmp": "image/bmp", - ".ico": "image/x-icon", - ".tiff": "image/tiff", - ".tif": "image/tiff", - ".avif": "image/avif", -} - -/** - * Reads an image file and returns both the data URL and buffer - */ -async function readImageAsDataUrlWithBuffer(filePath: string): Promise<{ dataUrl: string; buffer: Buffer }> { - const fileBuffer = await fs.readFile(filePath) - const base64 = fileBuffer.toString("base64") - const ext = path.extname(filePath).toLowerCase() - - const mimeType = IMAGE_MIME_TYPES[ext] || "image/png" - const dataUrl = `data:${mimeType};base64,${base64}` - - return { dataUrl, buffer: fileBuffer } -} +import { + DEFAULT_MAX_IMAGE_FILE_SIZE_MB, + DEFAULT_MAX_TOTAL_IMAGE_MEMORY_MB, + SUPPORTED_IMAGE_FORMATS, + readImageAsDataUrlWithBuffer, + isSupportedImageFormat, +} from "./helpers/imageHelpers" export function getReadFileToolDescription(blockName: string, blockParams: any): string { // Handle both single path and multiple files via args @@ -485,7 +436,11 @@ export async function readFileTool( // Track total image memory usage across all files let totalImageMemoryUsed = 0 const state = await cline.providerRef.deref()?.getState() - const { maxReadFileLine = -1, maxImageFileSize = DEFAULT_MAX_IMAGE_FILE_SIZE_MB, maxTotalImageMemory = DEFAULT_MAX_TOTAL_IMAGE_MEMORY_MB } = state ?? {} + const { + maxReadFileLine = -1, + maxImageFileSize = DEFAULT_MAX_IMAGE_FILE_SIZE_MB, + maxTotalImageMemory = DEFAULT_MAX_TOTAL_IMAGE_MEMORY_MB, + } = state ?? {} // Then process only approved files for (const fileResult of fileResults) { @@ -507,11 +462,12 @@ export async function readFileTool( const supportedBinaryFormats = getSupportedBinaryFormats() // Check if it's a supported image format - if (SUPPORTED_IMAGE_FORMATS.includes(fileExtension as (typeof SUPPORTED_IMAGE_FORMATS)[number])) { + if (isSupportedImageFormat(fileExtension)) { // Skip image processing if model doesn't support images if (!supportsImages) { - const notice = "Image file detected but current model does not support images. Skipping image processing." - + const notice = + "Image file detected but current model does not support images. Skipping image processing." + // Track file read await cline.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource) @@ -527,7 +483,10 @@ export async function readFileTool( // Check if image file exceeds individual size limit if (imageStats.size > maxImageFileSize * 1024 * 1024) { const imageSizeInMB = (imageStats.size / (1024 * 1024)).toFixed(1) - const notice = t("tools:readFile.imageTooLarge", { size: imageSizeInMB, max: maxImageFileSize }) + const notice = t("tools:readFile.imageTooLarge", { + size: imageSizeInMB, + max: maxImageFileSize, + }) // Track file read await cline.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource) From d018c30d1c4586776bff4a4a7a8377d0d43cad12 Mon Sep 17 00:00:00 2001 From: Sam Hoang Van Date: Wed, 23 Jul 2025 23:30:45 +0700 Subject: [PATCH 14/21] Update src/core/tools/readFileTool.ts Co-authored-by: Daniel <57051444+daniel-lxs@users.noreply.github.com> --- src/core/tools/readFileTool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/tools/readFileTool.ts b/src/core/tools/readFileTool.ts index b4ed3338d3..19c3941b42 100644 --- a/src/core/tools/readFileTool.ts +++ b/src/core/tools/readFileTool.ts @@ -500,7 +500,7 @@ export async function readFileTool( // Check if adding this image would exceed total memory limit const imageSizeInMB = imageStats.size / (1024 * 1024) if (totalImageMemoryUsed + imageSizeInMB > maxTotalImageMemory) { - const notice = `Image skipped to prevent memory issues. Total image memory would exceed ${maxTotalImageMemory}MB limit (current: ${totalImageMemoryUsed.toFixed(1)}MB, this file: ${imageSizeInMB.toFixed(1)}MB). Consider reading fewer images at once or reducing image sizes.` + const notice = `Image skipped to avoid memory limit (${maxTotalImageMemory}MB). Current: ${totalImageMemoryUsed.toFixed(1)}MB + this file: ${imageSizeInMB.toFixed(1)}MB. Try fewer or smaller images.`; // Track file read await cline.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource) From 1dc79cecb8f2649eed7053d918e94dd561c95f6c Mon Sep 17 00:00:00 2001 From: Sam Hoang Date: Fri, 25 Jul 2025 23:32:36 +0700 Subject: [PATCH 15/21] test: enhance image memory limit checks and add new test for skipping images --- src/core/tools/__tests__/readFileTool.spec.ts | 72 +++++++++++++++---- 1 file changed, 60 insertions(+), 12 deletions(-) diff --git a/src/core/tools/__tests__/readFileTool.spec.ts b/src/core/tools/__tests__/readFileTool.spec.ts index 917abdd7c8..a324892ba4 100644 --- a/src/core/tools/__tests__/readFileTool.spec.ts +++ b/src/core/tools/__tests__/readFileTool.spec.ts @@ -489,6 +489,11 @@ describe("read_file tool XML output structure", () => { const mockedExtractTextFromFile = vi.mocked(extractTextFromFile) const mockedIsBinaryFile = vi.mocked(isBinaryFile) const mockedPathResolve = vi.mocked(path.resolve) + const mockedFsReadFile = vi.mocked(fsPromises.readFile) + const imageBuffer = Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", + "base64", + ) let mockCline: any let mockProvider: any @@ -679,6 +684,11 @@ describe("read_file tool XML output structure", () => { }, (_: ToolParamName, content?: string) => content ?? "", ) + // In multi-image scenarios, the result is pushed to pushToolResult, not returned directly. + // We need to check the mock's calls to get the result. + if (mockCline.pushToolResult.mock.calls.length > 0) { + return mockCline.pushToolResult.mock.calls[0][0] + } return localResult } @@ -822,8 +832,8 @@ describe("read_file tool XML output structure", () => { expect(Array.isArray(result)).toBe(true) const parts = result as any[] - const textPart = parts.find((p) => p.type === "text")?.text - const imageParts = parts.filter((p) => p.type === "image") + const textPart = Array.isArray(result) ? result.find((p) => p.type === "text")?.text : result + const imageParts = Array.isArray(result) ? result.filter((p) => p.type === "image") : [] expect(textPart).toBeDefined() @@ -836,9 +846,9 @@ describe("read_file tool XML output structure", () => { expect(imageParts).toHaveLength(4) // First 4 images should be included (~19.6MB total) // Verify memory limit notice for the fifth image - expect(textPart).toContain("Total image memory would exceed 20MB limit") - expect(textPart).toContain("current: 19.6MB") // 4 * 4.9MB - expect(textPart).toContain("this file: 4.9MB") + expect(textPart).toContain( + "Image skipped to avoid memory limit (20MB). Current: 19.6MB + this file: 4.9MB. Try fewer or smaller images.", + ) }) it("should track memory usage correctly across multiple images", async () => { @@ -907,15 +917,13 @@ describe("read_file tool XML output structure", () => { const result = await executeReadMultipleImagesTool(exactLimitImages.map((img) => img.path)) // Verify - expect(Array.isArray(result)).toBe(true) - const parts = result as any[] - - const textPart = parts.find((p) => p.type === "text")?.text - const imageParts = parts.filter((p) => p.type === "image") + const textPart = Array.isArray(result) ? result.find((p) => p.type === "text")?.text : result + const imageParts = Array.isArray(result) ? result.filter((p) => p.type === "image") : [] expect(imageParts).toHaveLength(2) // First 2 images should fit - expect(textPart).toContain("Total image memory would exceed 20MB limit") - expect(textPart).toContain("current: 20.0MB") // Should show exactly 20MB used + expect(textPart).toContain( + "Image skipped to avoid memory limit (20MB). Current: 20.0MB + this file: 1.0MB. Try fewer or smaller images.", + ) }) it("should handle individual image size limit and total memory limit together", async () => { @@ -1000,6 +1008,46 @@ describe("read_file tool XML output structure", () => { expect(textPart).toContain("Image file is too large (6.0 MB). The maximum allowed size is 5 MB.") }) + it("should correctly calculate total memory and skip the last image", async () => { + // Setup + const testImages = [ + { path: "test/image1.png", sizeMB: 8 }, + { path: "test/image2.png", sizeMB: 8 }, + { path: "test/image3.png", sizeMB: 8 }, // This one should be skipped + ] + + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 10, // 10MB per image + maxTotalImageMemory: 20, // 20MB total + }) + + mockedIsBinaryFile.mockResolvedValue(true) + mockedCountFileLines.mockResolvedValue(0) + mockedFsReadFile.mockResolvedValue(imageBuffer) + + fsPromises.stat.mockImplementation(async (filePath) => { + const file = testImages.find((f) => filePath.toString().includes(f.path)) + if (file) { + return { size: file.sizeMB * 1024 * 1024 } + } + return { size: 1024 * 1024 } // Default 1MB + }) + + const imagePaths = testImages.map((img) => img.path) + const result = await executeReadMultipleImagesTool(imagePaths) + + expect(Array.isArray(result)).toBe(true) + const parts = result as any[] + const textPart = parts.find((p) => p.type === "text")?.text + const imageParts = parts.filter((p) => p.type === "image") + + expect(imageParts).toHaveLength(2) // First two images should be processed + expect(textPart).toContain("Image skipped to avoid memory limit (20MB)") + expect(textPart).toContain("Current: 16.0MB") + expect(textPart).toContain("this file: 8.0MB") + }) + it("should reset total memory tracking for each tool invocation", async () => { // Setup mocks (don't clear all mocks) From 0fbdb9e2872940b6f5834e2fbf04e3311d8d2400 Mon Sep 17 00:00:00 2001 From: Sam Hoang Date: Fri, 25 Jul 2025 23:58:26 +0700 Subject: [PATCH 16/21] remove unuse i18n --- src/core/tools/__tests__/readFileTool.spec.ts | 1 - src/i18n/locales/ca/tools.json | 1 - src/i18n/locales/de/tools.json | 1 - src/i18n/locales/en/tools.json | 1 - src/i18n/locales/es/tools.json | 1 - src/i18n/locales/fr/tools.json | 1 - src/i18n/locales/hi/tools.json | 1 - src/i18n/locales/id/tools.json | 1 - src/i18n/locales/it/tools.json | 1 - src/i18n/locales/ja/tools.json | 1 - src/i18n/locales/ko/tools.json | 1 - src/i18n/locales/nl/tools.json | 1 - src/i18n/locales/pl/tools.json | 1 - src/i18n/locales/pt-BR/tools.json | 1 - src/i18n/locales/ru/tools.json | 1 - src/i18n/locales/tr/tools.json | 1 - src/i18n/locales/vi/tools.json | 1 - src/i18n/locales/zh-CN/tools.json | 1 - src/i18n/locales/zh-TW/tools.json | 1 - 19 files changed, 19 deletions(-) diff --git a/src/core/tools/__tests__/readFileTool.spec.ts b/src/core/tools/__tests__/readFileTool.spec.ts index a324892ba4..b57797dfee 100644 --- a/src/core/tools/__tests__/readFileTool.spec.ts +++ b/src/core/tools/__tests__/readFileTool.spec.ts @@ -154,7 +154,6 @@ vi.mock("../../../i18n", () => ({ t: vi.fn((key: string, params?: Record) => { // Map translation keys to English text const translations: Record = { - "tools:readFile.imageWithDimensions": "Image file ({{dimensions}}, {{size}} KB)", "tools:readFile.imageWithSize": "Image file ({{size}} KB)", "tools:readFile.imageTooLarge": "Image file is too large ({{size}} MB). The maximum allowed size is {{max}} MB.", diff --git a/src/i18n/locales/ca/tools.json b/src/i18n/locales/ca/tools.json index a3ec48ec42..0f10b6fc2a 100644 --- a/src/i18n/locales/ca/tools.json +++ b/src/i18n/locales/ca/tools.json @@ -4,7 +4,6 @@ "definitionsOnly": " (només definicions)", "maxLines": " (màxim {{max}} línies)", "imageTooLarge": "El fitxer d'imatge és massa gran ({{size}} MB). La mida màxima permesa és {{max}} MB.", - "imageWithDimensions": "Fitxer d'imatge ({{dimensions}}, {{size}} KB)", "imageWithSize": "Fitxer d'imatge ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo sembla estar atrapat en un bucle, intentant la mateixa acció ({{toolName}}) repetidament. Això podria indicar un problema amb la seva estratègia actual. Considera reformular la tasca, proporcionar instruccions més específiques o guiar-lo cap a un enfocament diferent.", diff --git a/src/i18n/locales/de/tools.json b/src/i18n/locales/de/tools.json index 9dd72ddf6d..ecf372a50b 100644 --- a/src/i18n/locales/de/tools.json +++ b/src/i18n/locales/de/tools.json @@ -4,7 +4,6 @@ "definitionsOnly": " (nur Definitionen)", "maxLines": " (maximal {{max}} Zeilen)", "imageTooLarge": "Die Bilddatei ist zu groß ({{size}} MB). Die maximal erlaubte Größe beträgt {{max}} MB.", - "imageWithDimensions": "Bilddatei ({{dimensions}}, {{size}} KB)", "imageWithSize": "Bilddatei ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo scheint in einer Schleife festzustecken und versucht wiederholt dieselbe Aktion ({{toolName}}). Dies könnte auf ein Problem mit der aktuellen Strategie hindeuten. Überlege dir, die Aufgabe umzuformulieren, genauere Anweisungen zu geben oder Roo zu einem anderen Ansatz zu führen.", diff --git a/src/i18n/locales/en/tools.json b/src/i18n/locales/en/tools.json index 188e4dbcf2..5b88affae6 100644 --- a/src/i18n/locales/en/tools.json +++ b/src/i18n/locales/en/tools.json @@ -4,7 +4,6 @@ "definitionsOnly": " (definitions only)", "maxLines": " (max {{max}} lines)", "imageTooLarge": "Image file is too large ({{size}} MB). The maximum allowed size is {{max}} MB.", - "imageWithDimensions": "Image file ({{dimensions}}, {{size}} KB)", "imageWithSize": "Image file ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo appears to be stuck in a loop, attempting the same action ({{toolName}}) repeatedly. This might indicate a problem with its current strategy. Consider rephrasing the task, providing more specific instructions, or guiding it towards a different approach.", diff --git a/src/i18n/locales/es/tools.json b/src/i18n/locales/es/tools.json index f1cb00f9a5..6fd1cc2122 100644 --- a/src/i18n/locales/es/tools.json +++ b/src/i18n/locales/es/tools.json @@ -4,7 +4,6 @@ "definitionsOnly": " (solo definiciones)", "maxLines": " (máximo {{max}} líneas)", "imageTooLarge": "El archivo de imagen es demasiado grande ({{size}} MB). El tamaño máximo permitido es {{max}} MB.", - "imageWithDimensions": "Archivo de imagen ({{dimensions}}, {{size}} KB)", "imageWithSize": "Archivo de imagen ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo parece estar atrapado en un bucle, intentando la misma acción ({{toolName}}) repetidamente. Esto podría indicar un problema con su estrategia actual. Considera reformular la tarea, proporcionar instrucciones más específicas o guiarlo hacia un enfoque diferente.", diff --git a/src/i18n/locales/fr/tools.json b/src/i18n/locales/fr/tools.json index c9ff4d14c3..b6d7accebb 100644 --- a/src/i18n/locales/fr/tools.json +++ b/src/i18n/locales/fr/tools.json @@ -4,7 +4,6 @@ "definitionsOnly": " (définitions uniquement)", "maxLines": " (max {{max}} lignes)", "imageTooLarge": "Le fichier image est trop volumineux ({{size}} MB). La taille maximale autorisée est {{max}} MB.", - "imageWithDimensions": "Fichier image ({{dimensions}}, {{size}} Ko)", "imageWithSize": "Fichier image ({{size}} Ko)" }, "toolRepetitionLimitReached": "Roo semble être bloqué dans une boucle, tentant la même action ({{toolName}}) de façon répétée. Cela pourrait indiquer un problème avec sa stratégie actuelle. Envisage de reformuler la tâche, de fournir des instructions plus spécifiques ou de le guider vers une approche différente.", diff --git a/src/i18n/locales/hi/tools.json b/src/i18n/locales/hi/tools.json index 0d262a2d08..cbfbd7aef7 100644 --- a/src/i18n/locales/hi/tools.json +++ b/src/i18n/locales/hi/tools.json @@ -4,7 +4,6 @@ "definitionsOnly": " (केवल परिभाषाएँ)", "maxLines": " (अधिकतम {{max}} पंक्तियाँ)", "imageTooLarge": "छवि फ़ाइल बहुत बड़ी है ({{size}} MB)। अधिकतम अनुमतित आकार {{max}} MB है।", - "imageWithDimensions": "छवि फ़ाइल ({{dimensions}}, {{size}} KB)", "imageWithSize": "छवि फ़ाइल ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo एक लूप में फंसा हुआ लगता है, बार-बार एक ही क्रिया ({{toolName}}) को दोहरा रहा है। यह उसकी वर्तमान रणनीति में किसी समस्या का संकेत हो सकता है। कार्य को पुनः परिभाषित करने, अधिक विशिष्ट निर्देश देने, या उसे एक अलग दृष्टिकोण की ओर मार्गदर्शित करने पर विचार करें।", diff --git a/src/i18n/locales/id/tools.json b/src/i18n/locales/id/tools.json index 692b2e1ac3..3eb8854eff 100644 --- a/src/i18n/locales/id/tools.json +++ b/src/i18n/locales/id/tools.json @@ -4,7 +4,6 @@ "definitionsOnly": " (hanya definisi)", "maxLines": " (maks {{max}} baris)", "imageTooLarge": "File gambar terlalu besar ({{size}} MB). Ukuran maksimum yang diizinkan adalah {{max}} MB.", - "imageWithDimensions": "File gambar ({{dimensions}}, {{size}} KB)", "imageWithSize": "File gambar ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo tampaknya terjebak dalam loop, mencoba aksi yang sama ({{toolName}}) berulang kali. Ini mungkin menunjukkan masalah dengan strategi saat ini. Pertimbangkan untuk mengubah frasa tugas, memberikan instruksi yang lebih spesifik, atau mengarahkannya ke pendekatan yang berbeda.", diff --git a/src/i18n/locales/it/tools.json b/src/i18n/locales/it/tools.json index 73cedd4520..35b114a719 100644 --- a/src/i18n/locales/it/tools.json +++ b/src/i18n/locales/it/tools.json @@ -4,7 +4,6 @@ "definitionsOnly": " (solo definizioni)", "maxLines": " (max {{max}} righe)", "imageTooLarge": "Il file immagine è troppo grande ({{size}} MB). La dimensione massima consentita è {{max}} MB.", - "imageWithDimensions": "File immagine ({{dimensions}}, {{size}} KB)", "imageWithSize": "File immagine ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo sembra essere bloccato in un ciclo, tentando ripetutamente la stessa azione ({{toolName}}). Questo potrebbe indicare un problema con la sua strategia attuale. Considera di riformulare l'attività, fornire istruzioni più specifiche o guidarlo verso un approccio diverso.", diff --git a/src/i18n/locales/ja/tools.json b/src/i18n/locales/ja/tools.json index d1af6c4ce7..257d5aa201 100644 --- a/src/i18n/locales/ja/tools.json +++ b/src/i18n/locales/ja/tools.json @@ -4,7 +4,6 @@ "definitionsOnly": " (定義のみ)", "maxLines": " (最大{{max}}行)", "imageTooLarge": "画像ファイルが大きすぎます({{size}} MB)。最大許可サイズは {{max}} MB です。", - "imageWithDimensions": "画像ファイル({{dimensions}}、{{size}} KB)", "imageWithSize": "画像ファイル({{size}} KB)" }, "toolRepetitionLimitReached": "Rooが同じ操作({{toolName}})を繰り返し試みるループに陥っているようです。これは現在の方法に問題がある可能性を示しています。タスクの言い換え、より具体的な指示の提供、または別のアプローチへの誘導を検討してください。", diff --git a/src/i18n/locales/ko/tools.json b/src/i18n/locales/ko/tools.json index f99a562bbc..94b6d8c377 100644 --- a/src/i18n/locales/ko/tools.json +++ b/src/i18n/locales/ko/tools.json @@ -4,7 +4,6 @@ "definitionsOnly": " (정의만)", "maxLines": " (최대 {{max}}행)", "imageTooLarge": "이미지 파일이 너무 큽니다 ({{size}} MB). 최대 허용 크기는 {{max}} MB입니다.", - "imageWithDimensions": "이미지 파일 ({{dimensions}}, {{size}} KB)", "imageWithSize": "이미지 파일 ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo가 같은 동작({{toolName}})을 반복적으로 시도하면서 루프에 갇힌 것 같습니다. 이는 현재 전략에 문제가 있을 수 있음을 나타냅니다. 작업을 다시 표현하거나, 더 구체적인 지침을 제공하거나, 다른 접근 방식으로 안내해 보세요.", diff --git a/src/i18n/locales/nl/tools.json b/src/i18n/locales/nl/tools.json index 18db1b6d9d..449cd54583 100644 --- a/src/i18n/locales/nl/tools.json +++ b/src/i18n/locales/nl/tools.json @@ -4,7 +4,6 @@ "definitionsOnly": " (alleen definities)", "maxLines": " (max {{max}} regels)", "imageTooLarge": "Afbeeldingsbestand is te groot ({{size}} MB). De maximaal toegestane grootte is {{max}} MB.", - "imageWithDimensions": "Afbeeldingsbestand ({{dimensions}}, {{size}} KB)", "imageWithSize": "Afbeeldingsbestand ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo lijkt vast te zitten in een lus, waarbij hij herhaaldelijk dezelfde actie ({{toolName}}) probeert. Dit kan duiden op een probleem met de huidige strategie. Overweeg de taak te herformuleren, specifiekere instructies te geven of Roo naar een andere aanpak te leiden.", diff --git a/src/i18n/locales/pl/tools.json b/src/i18n/locales/pl/tools.json index ab998b2d0d..979b2f54ae 100644 --- a/src/i18n/locales/pl/tools.json +++ b/src/i18n/locales/pl/tools.json @@ -4,7 +4,6 @@ "definitionsOnly": " (tylko definicje)", "maxLines": " (maks. {{max}} linii)", "imageTooLarge": "Plik obrazu jest zbyt duży ({{size}} MB). Maksymalny dozwolony rozmiar to {{max}} MB.", - "imageWithDimensions": "Plik obrazu ({{dimensions}}, {{size}} KB)", "imageWithSize": "Plik obrazu ({{size}} KB)" }, "toolRepetitionLimitReached": "Wygląda na to, że Roo utknął w pętli, wielokrotnie próbując wykonać tę samą akcję ({{toolName}}). Może to wskazywać na problem z jego obecną strategią. Rozważ przeformułowanie zadania, podanie bardziej szczegółowych instrukcji lub nakierowanie go na inne podejście.", diff --git a/src/i18n/locales/pt-BR/tools.json b/src/i18n/locales/pt-BR/tools.json index 6057723cca..4e3296fd4a 100644 --- a/src/i18n/locales/pt-BR/tools.json +++ b/src/i18n/locales/pt-BR/tools.json @@ -4,7 +4,6 @@ "definitionsOnly": " (apenas definições)", "maxLines": " (máx. {{max}} linhas)", "imageTooLarge": "Arquivo de imagem é muito grande ({{size}} MB). O tamanho máximo permitido é {{max}} MB.", - "imageWithDimensions": "Arquivo de imagem ({{dimensions}}, {{size}} KB)", "imageWithSize": "Arquivo de imagem ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo parece estar preso em um loop, tentando a mesma ação ({{toolName}}) repetidamente. Isso pode indicar um problema com sua estratégia atual. Considere reformular a tarefa, fornecer instruções mais específicas ou guiá-lo para uma abordagem diferente.", diff --git a/src/i18n/locales/ru/tools.json b/src/i18n/locales/ru/tools.json index c1dddffcae..d74918f058 100644 --- a/src/i18n/locales/ru/tools.json +++ b/src/i18n/locales/ru/tools.json @@ -4,7 +4,6 @@ "definitionsOnly": " (только определения)", "maxLines": " (макс. {{max}} строк)", "imageTooLarge": "Файл изображения слишком большой ({{size}} МБ). Максимально допустимый размер {{max}} МБ.", - "imageWithDimensions": "Файл изображения ({{dimensions}}, {{size}} КБ)", "imageWithSize": "Файл изображения ({{size}} КБ)" }, "toolRepetitionLimitReached": "Похоже, что Roo застрял в цикле, многократно пытаясь выполнить одно и то же действие ({{toolName}}). Это может указывать на проблему с его текущей стратегией. Попробуйте переформулировать задачу, предоставить более конкретные инструкции или направить его к другому подходу.", diff --git a/src/i18n/locales/tr/tools.json b/src/i18n/locales/tr/tools.json index 1462bc221c..5341a23cb1 100644 --- a/src/i18n/locales/tr/tools.json +++ b/src/i18n/locales/tr/tools.json @@ -4,7 +4,6 @@ "definitionsOnly": " (sadece tanımlar)", "maxLines": " (maks. {{max}} satır)", "imageTooLarge": "Görüntü dosyası çok büyük ({{size}} MB). İzin verilen maksimum boyut {{max}} MB.", - "imageWithDimensions": "Görüntü dosyası ({{dimensions}}, {{size}} KB)", "imageWithSize": "Görüntü dosyası ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo bir döngüye takılmış gibi görünüyor, aynı eylemi ({{toolName}}) tekrar tekrar deniyor. Bu, mevcut stratejisinde bir sorun olduğunu gösterebilir. Görevi yeniden ifade etmeyi, daha spesifik talimatlar vermeyi veya onu farklı bir yaklaşıma yönlendirmeyi düşünün.", diff --git a/src/i18n/locales/vi/tools.json b/src/i18n/locales/vi/tools.json index 62ba7187ce..4c5080a146 100644 --- a/src/i18n/locales/vi/tools.json +++ b/src/i18n/locales/vi/tools.json @@ -4,7 +4,6 @@ "definitionsOnly": " (chỉ định nghĩa)", "maxLines": " (tối đa {{max}} dòng)", "imageTooLarge": "Tệp hình ảnh quá lớn ({{size}} MB). Kích thước tối đa cho phép là {{max}} MB.", - "imageWithDimensions": "Tệp hình ảnh ({{dimensions}}, {{size}} KB)", "imageWithSize": "Tệp hình ảnh ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo dường như đang bị mắc kẹt trong một vòng lặp, liên tục cố gắng thực hiện cùng một hành động ({{toolName}}). Điều này có thể cho thấy vấn đề với chiến lược hiện tại. Hãy cân nhắc việc diễn đạt lại nhiệm vụ, cung cấp hướng dẫn cụ thể hơn, hoặc hướng Roo theo một cách tiếp cận khác.", diff --git a/src/i18n/locales/zh-CN/tools.json b/src/i18n/locales/zh-CN/tools.json index a7526cf8d1..c0c93d8436 100644 --- a/src/i18n/locales/zh-CN/tools.json +++ b/src/i18n/locales/zh-CN/tools.json @@ -4,7 +4,6 @@ "definitionsOnly": " (仅定义)", "maxLines": " (最多 {{max}} 行)", "imageTooLarge": "图片文件过大 ({{size}} MB)。允许的最大大小为 {{max}} MB。", - "imageWithDimensions": "图片文件 ({{dimensions}}, {{size}} KB)", "imageWithSize": "图片文件 ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo 似乎陷入循环,反复尝试同一操作 ({{toolName}})。这可能表明当前策略存在问题。请考虑重新描述任务、提供更具体的指示或引导其尝试不同的方法。", diff --git a/src/i18n/locales/zh-TW/tools.json b/src/i18n/locales/zh-TW/tools.json index 87c3fdee7f..b736448c20 100644 --- a/src/i18n/locales/zh-TW/tools.json +++ b/src/i18n/locales/zh-TW/tools.json @@ -4,7 +4,6 @@ "definitionsOnly": " (僅定義)", "maxLines": " (最多 {{max}} 行)", "imageTooLarge": "圖片檔案過大 ({{size}} MB)。允許的最大大小為 {{max}} MB。", - "imageWithDimensions": "圖片檔案 ({{dimensions}}, {{size}} KB)", "imageWithSize": "圖片檔案 ({{size}} KB)" }, "toolRepetitionLimitReached": "Roo 似乎陷入循環,反覆嘗試同一操作 ({{toolName}})。這可能表明目前策略存在問題。請考慮重新描述工作、提供更具體的指示或引導其嘗試不同的方法。", From 3038800d9b3b9b6a162459120ddad4f27874da35 Mon Sep 17 00:00:00 2001 From: Sam Hoang Date: Sat, 26 Jul 2025 15:39:55 +0700 Subject: [PATCH 17/21] fix test fail --- src/core/tools/__tests__/readFileTool.spec.ts | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/core/tools/__tests__/readFileTool.spec.ts b/src/core/tools/__tests__/readFileTool.spec.ts index b57797dfee..7db587a85d 100644 --- a/src/core/tools/__tests__/readFileTool.spec.ts +++ b/src/core/tools/__tests__/readFileTool.spec.ts @@ -739,8 +739,8 @@ describe("read_file tool XML output structure", () => { // Mock file stats for each image fsPromises.stat = vi.fn().mockImplementation((filePath) => { - const fileName = filePath.split("/").pop() - const image = smallImages.find((img) => img.path.includes(fileName.split(".")[0])) + const normalizedFilePath = path.normalize(filePath.toString()) + const image = smallImages.find((img) => normalizedFilePath.includes(path.normalize(img.path))) return Promise.resolve({ size: (image?.sizeKB || 1024) * 1024 }) }) @@ -815,9 +815,8 @@ describe("read_file tool XML output structure", () => { // Mock file stats for each image fsPromises.stat = vi.fn().mockImplementation((filePath) => { - const fileName = path.basename(filePath) - const baseName = path.parse(fileName).name - const image = largeImages.find((img) => img.path.includes(baseName)) + const normalizedFilePath = path.normalize(filePath.toString()) + const image = largeImages.find((img) => normalizedFilePath.includes(path.normalize(img.path))) return Promise.resolve({ size: (image?.sizeKB || 1024) * 1024 }) }) @@ -899,12 +898,10 @@ describe("read_file tool XML output structure", () => { // Mock file stats with simpler logic fsPromises.stat = vi.fn().mockImplementation((filePath) => { - if (filePath.includes("exact1")) { - return Promise.resolve({ size: 10240 * 1024 }) // 10MB - } else if (filePath.includes("exact2")) { - return Promise.resolve({ size: 10240 * 1024 }) // 10MB - } else if (filePath.includes("exact3")) { - return Promise.resolve({ size: 1024 * 1024 }) // 1MB + const normalizedFilePath = path.normalize(filePath.toString()) + const image = exactLimitImages.find((img) => normalizedFilePath.includes(path.normalize(img.path))) + if (image) { + return Promise.resolve({ size: image.sizeKB * 1024 }) } return Promise.resolve({ size: 1024 * 1024 }) // Default 1MB }) @@ -1026,7 +1023,8 @@ describe("read_file tool XML output structure", () => { mockedFsReadFile.mockResolvedValue(imageBuffer) fsPromises.stat.mockImplementation(async (filePath) => { - const file = testImages.find((f) => filePath.toString().includes(f.path)) + const normalizedFilePath = path.normalize(filePath.toString()) + const file = testImages.find((f) => normalizedFilePath.includes(path.normalize(f.path))) if (file) { return { size: file.sizeMB * 1024 * 1024 } } From 8b439625907ab9d813fbc3f3b755f589d46346b5 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Mon, 28 Jul 2025 11:30:33 -0500 Subject: [PATCH 18/21] feat: address PR review comments - Replace maxTotalImageMemory with maxTotalImageSize throughout codebase - Update all translation files with new terminology - Fix incorrect key names in pt-BR, zh-CN, and zh-TW translations - Improve clarity of size limit messages - Update tests to reflect new terminology --- packages/types/src/global-settings.ts | 2 +- src/core/tools/__tests__/readFileTool.spec.ts | 187 +++++++++++++++--- src/core/tools/helpers/imageHelpers.ts | 7 +- src/core/tools/readFileTool.ts | 16 +- src/core/webview/ClineProvider.ts | 6 +- src/core/webview/webviewMessageHandler.ts | 4 +- src/shared/ExtensionMessage.ts | 2 +- src/shared/WebviewMessage.ts | 2 +- .../settings/ContextManagementSettings.tsx | 18 +- .../src/components/settings/SettingsView.tsx | 6 +- .../src/context/ExtensionStateContext.tsx | 8 +- webview-ui/src/i18n/locales/ca/settings.json | 6 +- webview-ui/src/i18n/locales/de/settings.json | 6 +- webview-ui/src/i18n/locales/en/settings.json | 6 +- webview-ui/src/i18n/locales/es/settings.json | 6 +- webview-ui/src/i18n/locales/fr/settings.json | 6 +- webview-ui/src/i18n/locales/hi/settings.json | 6 +- webview-ui/src/i18n/locales/id/settings.json | 6 +- webview-ui/src/i18n/locales/it/settings.json | 6 +- webview-ui/src/i18n/locales/ja/settings.json | 6 +- webview-ui/src/i18n/locales/ko/settings.json | 6 +- webview-ui/src/i18n/locales/nl/settings.json | 6 +- webview-ui/src/i18n/locales/pl/settings.json | 6 +- .../src/i18n/locales/pt-BR/settings.json | 6 +- webview-ui/src/i18n/locales/ru/settings.json | 8 +- webview-ui/src/i18n/locales/tr/settings.json | 6 +- webview-ui/src/i18n/locales/vi/settings.json | 6 +- .../src/i18n/locales/zh-CN/settings.json | 6 +- .../src/i18n/locales/zh-TW/settings.json | 6 +- 29 files changed, 249 insertions(+), 119 deletions(-) diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index d49185c355..dc5a9e6744 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -102,7 +102,7 @@ export const globalSettingsSchema = z.object({ showRooIgnoredFiles: z.boolean().optional(), maxReadFileLine: z.number().optional(), maxImageFileSize: z.number().optional(), - maxTotalImageMemory: z.number().optional(), + maxTotalImageSize: z.number().optional(), terminalOutputLineLimit: z.number().optional(), terminalOutputCharacterLimit: z.number().optional(), diff --git a/src/core/tools/__tests__/readFileTool.spec.ts b/src/core/tools/__tests__/readFileTool.spec.ts index 7db587a85d..7ba822dce0 100644 --- a/src/core/tools/__tests__/readFileTool.spec.ts +++ b/src/core/tools/__tests__/readFileTool.spec.ts @@ -10,7 +10,7 @@ import { isBinaryFile } from "isbinaryfile" import { ReadFileToolUse, ToolParamName, ToolResponse } from "../../../shared/tools" import { readFileTool } from "../readFileTool" import { formatResponse } from "../../prompts/responses" -import { DEFAULT_MAX_IMAGE_FILE_SIZE_MB, DEFAULT_MAX_TOTAL_IMAGE_MEMORY_MB } from "../helpers/imageHelpers" +import { DEFAULT_MAX_IMAGE_FILE_SIZE_MB, DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB } from "../helpers/imageHelpers" vi.mock("path", async () => { const originalPath = await vi.importActual("path") @@ -156,7 +156,7 @@ vi.mock("../../../i18n", () => ({ const translations: Record = { "tools:readFile.imageWithSize": "Image file ({{size}} KB)", "tools:readFile.imageTooLarge": - "Image file is too large ({{size}} MB). The maximum allowed size is {{max}} MB.", + "Image file is too large ({{size}}). The maximum allowed size is {{max}} MB.", "tools:readFile.linesRange": " (lines {{start}}-{{end}})", "tools:readFile.definitionsOnly": " (definitions only)", "tools:readFile.maxLines": " (max {{max}} lines)", @@ -296,7 +296,7 @@ describe("read_file tool with maxReadFileLine setting", () => { const maxReadFileLine = options.maxReadFileLine ?? 500 const totalLines = options.totalLines ?? 5 - mockProvider.getState.mockResolvedValue({ maxReadFileLine, maxImageFileSize: 20, maxTotalImageMemory: 20 }) + mockProvider.getState.mockResolvedValue({ maxReadFileLine, maxImageFileSize: 20, maxTotalImageSize: 20 }) mockedCountFileLines.mockResolvedValue(totalLines) // Reset the spy before each test @@ -532,7 +532,7 @@ describe("read_file tool XML output structure", () => { mockInputContent = fileContent // Setup mock provider with default maxReadFileLine - mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, maxTotalImageMemory: 20 }) // Default to full file read + mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, maxTotalImageSize: 20 }) // Default to full file read // Add additional properties needed for XML tests mockCline.sayAndCreateMissingParamError = vi.fn().mockResolvedValue("Missing required parameter") @@ -557,7 +557,7 @@ describe("read_file tool XML output structure", () => { const isBinary = options.isBinary ?? false const validateAccess = options.validateAccess ?? true - mockProvider.getState.mockResolvedValue({ maxReadFileLine, maxImageFileSize: 20, maxTotalImageMemory: 20 }) + mockProvider.getState.mockResolvedValue({ maxReadFileLine, maxImageFileSize: 20, maxTotalImageSize: 20 }) mockedCountFileLines.mockResolvedValue(totalLines) mockedIsBinaryFile.mockResolvedValue(isBinary) mockCline.rooIgnoreController.validateAccess = vi.fn().mockReturnValue(validateAccess) @@ -599,8 +599,8 @@ describe("read_file tool XML output structure", () => { mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, - maxTotalImageMemory: 20, - }) // Allow up to 20MB per image and total memory + maxTotalImageSize: 20, + }) // Allow up to 20MB per image and total size // Execute const result = await executeReadFileTool() @@ -632,8 +632,8 @@ describe("read_file tool XML output structure", () => { mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, - maxTotalImageMemory: 20, - }) // Allow up to 20MB per image and total memory + maxTotalImageSize: 20, + }) // Allow up to 20MB per image and total size // Execute const result = await executeReadFileTool({}, { totalLines: 0 }) @@ -651,6 +651,12 @@ describe("read_file tool XML output structure", () => { { path: "test/image3.gif", sizeKB: 8192 }, // 8MB ] + // Define imageBuffer for this test suite + const imageBuffer = Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", + "base64", + ) + beforeEach(() => { // CRITICAL: Reset fsPromises mocks to prevent cross-test contamination within this suite fsPromises.stat.mockClear() @@ -707,8 +713,8 @@ describe("read_file tool XML output structure", () => { mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, - maxTotalImageMemory: 20, - }) // Allow up to 20MB per image and total memory + maxTotalImageSize: 20, + }) // Allow up to 20MB per image and total size // Setup mockCline properties (preserve existing API) mockCline.cwd = "/" @@ -780,8 +786,8 @@ describe("read_file tool XML output structure", () => { mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 15, - maxTotalImageMemory: 20, - }) // Allow up to 15MB per image and 20MB total memory + maxTotalImageSize: 20, + }) // Allow up to 15MB per image and 20MB total size // Setup mockCline properties mockCline.cwd = "/" @@ -844,9 +850,9 @@ describe("read_file tool XML output structure", () => { expect(imageParts).toHaveLength(4) // First 4 images should be included (~19.6MB total) // Verify memory limit notice for the fifth image - expect(textPart).toContain( - "Image skipped to avoid memory limit (20MB). Current: 19.6MB + this file: 4.9MB. Try fewer or smaller images.", - ) + expect(textPart).toContain("Image skipped to avoid size limit (20MB)") + expect(textPart).toMatch(/Current: \d+(\.\d+)? MB/) + expect(textPart).toMatch(/this file: \d+(\.\d+)? MB/) }) it("should track memory usage correctly across multiple images", async () => { @@ -866,8 +872,8 @@ describe("read_file tool XML output structure", () => { mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 15, - maxTotalImageMemory: 20, - }) // Allow up to 15MB per image and 20MB total memory + maxTotalImageSize: 20, + }) // Allow up to 15MB per image and 20MB total size // Setup mockCline properties mockCline.cwd = "/" @@ -917,9 +923,9 @@ describe("read_file tool XML output structure", () => { const imageParts = Array.isArray(result) ? result.filter((p) => p.type === "image") : [] expect(imageParts).toHaveLength(2) // First 2 images should fit - expect(textPart).toContain( - "Image skipped to avoid memory limit (20MB). Current: 20.0MB + this file: 1.0MB. Try fewer or smaller images.", - ) + expect(textPart).toContain("Image skipped to avoid size limit (20MB)") + expect(textPart).toMatch(/Current: \d+(\.\d+)? MB/) + expect(textPart).toMatch(/this file: \d+(\.\d+)? MB/) }) it("should handle individual image size limit and total memory limit together", async () => { @@ -939,8 +945,8 @@ describe("read_file tool XML output structure", () => { mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, - maxTotalImageMemory: 20, - }) // Allow up to 20MB per image and total memory + maxTotalImageSize: 20, + }) // Allow up to 20MB per image and total size // Setup mockCline properties (complete setup) mockCline.cwd = "/" @@ -981,7 +987,7 @@ describe("read_file tool XML output structure", () => { mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 5, - maxTotalImageMemory: 20, + maxTotalImageSize: 20, }) // Mock path.resolve @@ -1001,7 +1007,9 @@ describe("read_file tool XML output structure", () => { expect(imageParts).toHaveLength(2) // Should show individual size limit violation - expect(textPart).toContain("Image file is too large (6.0 MB). The maximum allowed size is 5 MB.") + expect(textPart).toMatch( + /Image file is too large \(\d+(\.\d+)? MB\)\. The maximum allowed size is 5 MB\./, + ) }) it("should correctly calculate total memory and skip the last image", async () => { @@ -1015,7 +1023,7 @@ describe("read_file tool XML output structure", () => { mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 10, // 10MB per image - maxTotalImageMemory: 20, // 20MB total + maxTotalImageSize: 20, // 20MB total }) mockedIsBinaryFile.mockResolvedValue(true) @@ -1040,9 +1048,9 @@ describe("read_file tool XML output structure", () => { const imageParts = parts.filter((p) => p.type === "image") expect(imageParts).toHaveLength(2) // First two images should be processed - expect(textPart).toContain("Image skipped to avoid memory limit (20MB)") - expect(textPart).toContain("Current: 16.0MB") - expect(textPart).toContain("this file: 8.0MB") + expect(textPart).toContain("Image skipped to avoid size limit (20MB)") + expect(textPart).toMatch(/Current: \d+(\.\d+)? MB/) + expect(textPart).toMatch(/this file: \d+(\.\d+)? MB/) }) it("should reset total memory tracking for each tool invocation", async () => { @@ -1062,7 +1070,7 @@ describe("read_file tool XML output structure", () => { mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, - maxTotalImageMemory: 20, + maxTotalImageSize: 20, }) // Setup mockCline properties (complete setup) @@ -1106,7 +1114,7 @@ describe("read_file tool XML output structure", () => { mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, - maxTotalImageMemory: 20, + maxTotalImageSize: 20, }) // Reset path resolving for second batch @@ -1162,6 +1170,123 @@ describe("read_file tool XML output structure", () => { expect(imageParts).toHaveLength(1) // Second image should be processed }) + + it("should handle a folder with many images just under the individual size limit", async () => { + // Setup - Create many images that are each just under the 5MB individual limit + // but together approach the 20MB total limit + const manyImages = [ + { path: "test/img1.png", sizeKB: 4900 }, // 4.78MB + { path: "test/img2.png", sizeKB: 4900 }, // 4.78MB + { path: "test/img3.png", sizeKB: 4900 }, // 4.78MB + { path: "test/img4.png", sizeKB: 4900 }, // 4.78MB + { path: "test/img5.png", sizeKB: 4900 }, // 4.78MB - This should be skipped (total would be ~23.9MB) + ] + + // Setup mocks + mockedIsBinaryFile.mockResolvedValue(true) + mockedCountFileLines.mockResolvedValue(0) + fsPromises.readFile.mockResolvedValue(imageBuffer) + + // Setup provider with 5MB individual limit and 20MB total limit + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 5, + maxTotalImageSize: 20, + }) + + // Mock file stats for each image + fsPromises.stat = vi.fn().mockImplementation((filePath) => { + const normalizedFilePath = path.normalize(filePath.toString()) + const image = manyImages.find((img) => normalizedFilePath.includes(path.normalize(img.path))) + return Promise.resolve({ size: (image?.sizeKB || 1024) * 1024 }) + }) + + // Mock path.resolve + mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) + + // Execute + const result = await executeReadMultipleImagesTool(manyImages.map((img) => img.path)) + + // Verify + expect(Array.isArray(result)).toBe(true) + const parts = result as any[] + const textPart = parts.find((p) => p.type === "text")?.text + const imageParts = parts.filter((p) => p.type === "image") + + // Should process first 4 images (total ~19.12MB, under 20MB limit) + expect(imageParts).toHaveLength(4) + + // Should show memory limit notice for the 5th image + expect(textPart).toContain("Image skipped to avoid size limit (20MB)") + expect(textPart).toContain("test/img5.png") + + // Verify memory tracking worked correctly + // The notice should show current memory usage around 20MB (4 * 4900KB ≈ 19.14MB, displayed as 20.1MB) + expect(textPart).toMatch(/Current: \d+(\.\d+)? MB/) + }) + + it("should reset memory tracking between separate tool invocations more explicitly", async () => { + // This test verifies that totalImageMemoryUsed is reset between calls + // by making two separate tool invocations and ensuring the second one + // starts with fresh memory tracking + + // Setup mocks + mockedIsBinaryFile.mockResolvedValue(true) + mockedCountFileLines.mockResolvedValue(0) + fsPromises.readFile.mockResolvedValue(imageBuffer) + + // Setup provider + mockProvider.getState.mockResolvedValue({ + maxReadFileLine: -1, + maxImageFileSize: 20, + maxTotalImageSize: 20, + }) + + // First invocation - use 15MB of memory + const firstBatch = [{ path: "test/large1.png", sizeKB: 15360 }] // 15MB + + fsPromises.stat = vi.fn().mockResolvedValue({ size: 15360 * 1024 }) + mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) + + // Execute first batch + const result1 = await executeReadMultipleImagesTool(firstBatch.map((img) => img.path)) + + // Verify first batch processed successfully + expect(Array.isArray(result1)).toBe(true) + const parts1 = result1 as any[] + const imageParts1 = parts1.filter((p) => p.type === "image") + expect(imageParts1).toHaveLength(1) + + // Second invocation - should start with 0 memory used, not 15MB + // If memory tracking wasn't reset, this 18MB image would be rejected + const secondBatch = [{ path: "test/large2.png", sizeKB: 18432 }] // 18MB + + // Reset mocks for second invocation + fsPromises.stat.mockClear() + fsPromises.readFile.mockClear() + mockedPathResolve.mockClear() + + fsPromises.stat = vi.fn().mockResolvedValue({ size: 18432 * 1024 }) + fsPromises.readFile.mockResolvedValue(imageBuffer) + mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`) + + // Execute second batch + const result2 = await executeReadMultipleImagesTool(secondBatch.map((img) => img.path)) + + // Verify second batch processed successfully + expect(Array.isArray(result2)).toBe(true) + const parts2 = result2 as any[] + const imageParts2 = parts2.filter((p) => p.type === "image") + const textPart2 = parts2.find((p) => p.type === "text")?.text + + // The 18MB image should be processed successfully because memory was reset + expect(imageParts2).toHaveLength(1) + + // Should NOT contain any memory limit notices + expect(textPart2).not.toContain("Image skipped to avoid memory limit") + + // This proves memory tracking was reset between invocations + }) }) }) diff --git a/src/core/tools/helpers/imageHelpers.ts b/src/core/tools/helpers/imageHelpers.ts index dfe93ae585..d238e98f42 100644 --- a/src/core/tools/helpers/imageHelpers.ts +++ b/src/core/tools/helpers/imageHelpers.ts @@ -8,9 +8,12 @@ export const DEFAULT_MAX_IMAGE_FILE_SIZE_MB = 5 /** * Default maximum total memory usage for all images in a single read operation (20MB) - * This prevents memory issues when reading multiple large images simultaneously + * This is a cumulative limit - as each image is processed, its size is added to the total. + * If including another image would exceed this limit, it will be skipped with a notice. + * Example: With a 20MB limit, reading 3 images of 8MB, 7MB, and 10MB would process + * the first two (15MB total) but skip the third to stay under the limit. */ -export const DEFAULT_MAX_TOTAL_IMAGE_MEMORY_MB = 20 +export const DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB = 20 /** * Supported image formats that can be displayed diff --git a/src/core/tools/readFileTool.ts b/src/core/tools/readFileTool.ts index 19c3941b42..e21f4f9530 100644 --- a/src/core/tools/readFileTool.ts +++ b/src/core/tools/readFileTool.ts @@ -1,5 +1,6 @@ import path from "path" import { isBinaryFile } from "isbinaryfile" +import prettyBytes from "pretty-bytes" import { Task } from "../task/Task" import { ClineSayTool } from "../../shared/ExtensionMessage" @@ -17,8 +18,7 @@ import { parseXml } from "../../utils/xml" import * as fs from "fs/promises" import { DEFAULT_MAX_IMAGE_FILE_SIZE_MB, - DEFAULT_MAX_TOTAL_IMAGE_MEMORY_MB, - SUPPORTED_IMAGE_FORMATS, + DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB, readImageAsDataUrlWithBuffer, isSupportedImageFormat, } from "./helpers/imageHelpers" @@ -439,7 +439,7 @@ export async function readFileTool( const { maxReadFileLine = -1, maxImageFileSize = DEFAULT_MAX_IMAGE_FILE_SIZE_MB, - maxTotalImageMemory = DEFAULT_MAX_TOTAL_IMAGE_MEMORY_MB, + maxTotalImageSize = DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB, } = state ?? {} // Then process only approved files @@ -482,9 +482,9 @@ export async function readFileTool( // Check if image file exceeds individual size limit if (imageStats.size > maxImageFileSize * 1024 * 1024) { - const imageSizeInMB = (imageStats.size / (1024 * 1024)).toFixed(1) + const imageSizeFormatted = prettyBytes(imageStats.size) const notice = t("tools:readFile.imageTooLarge", { - size: imageSizeInMB, + size: imageSizeFormatted, max: maxImageFileSize, }) @@ -499,8 +499,10 @@ export async function readFileTool( // Check if adding this image would exceed total memory limit const imageSizeInMB = imageStats.size / (1024 * 1024) - if (totalImageMemoryUsed + imageSizeInMB > maxTotalImageMemory) { - const notice = `Image skipped to avoid memory limit (${maxTotalImageMemory}MB). Current: ${totalImageMemoryUsed.toFixed(1)}MB + this file: ${imageSizeInMB.toFixed(1)}MB. Try fewer or smaller images.`; + if (totalImageMemoryUsed + imageSizeInMB > maxTotalImageSize) { + const currentMemoryFormatted = prettyBytes(totalImageMemoryUsed * 1024 * 1024) + const fileMemoryFormatted = prettyBytes(imageStats.size) + const notice = `Image skipped to avoid size limit (${maxTotalImageSize}MB). Current: ${currentMemoryFormatted} + this file: ${fileMemoryFormatted}. Try fewer or smaller images.` // Track file read await cline.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index c55f68b1fa..60d198345b 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1426,7 +1426,7 @@ export class ClineProvider language, maxReadFileLine, maxImageFileSize, - maxTotalImageMemory, + maxTotalImageSize, terminalCompressProgressBar, historyPreviewCollapsed, cloudUserInfo, @@ -1535,7 +1535,7 @@ export class ClineProvider renderContext: this.renderContext, maxReadFileLine: maxReadFileLine ?? -1, maxImageFileSize: maxImageFileSize ?? 5, - maxTotalImageMemory: maxTotalImageMemory ?? 20, + maxTotalImageSize: maxTotalImageSize ?? 20, maxConcurrentFileReads: maxConcurrentFileReads ?? 5, settingsImportedAt: this.settingsImportedAt, terminalCompressProgressBar: terminalCompressProgressBar ?? true, @@ -1707,7 +1707,7 @@ export class ClineProvider showRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? true, maxReadFileLine: stateValues.maxReadFileLine ?? -1, maxImageFileSize: stateValues.maxImageFileSize ?? 5, - maxTotalImageMemory: stateValues.maxTotalImageMemory ?? 20, + maxTotalImageSize: stateValues.maxTotalImageSize ?? 20, maxConcurrentFileReads: stateValues.maxConcurrentFileReads ?? 5, historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false, cloudUserInfo, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index cd309f363b..93608e7131 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1269,8 +1269,8 @@ export const webviewMessageHandler = async ( await updateGlobalState("maxImageFileSize", message.value) await provider.postStateToWebview() break - case "maxTotalImageMemory": - await updateGlobalState("maxTotalImageMemory", message.value) + case "maxTotalImageSize": + await updateGlobalState("maxTotalImageSize", message.value) await provider.postStateToWebview() break case "maxConcurrentFileReads": diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 46dd8d66ad..07d57d679f 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -279,7 +279,7 @@ export type ExtensionState = Pick< showRooIgnoredFiles: boolean // Whether to show .rooignore'd files in listings maxReadFileLine: number // Maximum number of lines to read from a file before truncating maxImageFileSize: number // Maximum size of image files to process in MB - maxTotalImageMemory: number // Maximum total memory for all images in a single read operation in MB + maxTotalImageSize: number // Maximum total size for all images in a single read operation in MB experiments: Experiments // Map of experiment IDs to their enabled state diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 76add0a43f..e8e8721b7e 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -163,7 +163,7 @@ export interface WebviewMessage { | "language" | "maxReadFileLine" | "maxImageFileSize" - | "maxTotalImageMemory" + | "maxTotalImageSize" | "maxConcurrentFileReads" | "includeDiagnosticMessages" | "maxDiagnosticMessages" diff --git a/webview-ui/src/components/settings/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx index 7a207b6895..88484e1d63 100644 --- a/webview-ui/src/components/settings/ContextManagementSettings.tsx +++ b/webview-ui/src/components/settings/ContextManagementSettings.tsx @@ -21,7 +21,7 @@ type ContextManagementSettingsProps = HTMLAttributes & { showRooIgnoredFiles?: boolean maxReadFileLine?: number maxImageFileSize?: number - maxTotalImageMemory?: number + maxTotalImageSize?: number maxConcurrentFileReads?: number profileThresholds?: Record includeDiagnosticMessages?: boolean @@ -35,7 +35,7 @@ type ContextManagementSettingsProps = HTMLAttributes & { | "showRooIgnoredFiles" | "maxReadFileLine" | "maxImageFileSize" - | "maxTotalImageMemory" + | "maxTotalImageSize" | "maxConcurrentFileReads" | "profileThresholds" | "includeDiagnosticMessages" @@ -54,7 +54,7 @@ export const ContextManagementSettings = ({ setCachedStateField, maxReadFileLine, maxImageFileSize, - maxTotalImageMemory, + maxTotalImageSize, maxConcurrentFileReads, profileThresholds = {}, includeDiagnosticMessages, @@ -242,29 +242,29 @@ export const ContextManagementSettings = ({
- {t("settings:contextManagement.maxTotalImageMemory.label")} + {t("settings:contextManagement.maxTotalImageSize.label")}
{ const newValue = parseInt(e.target.value, 10) if (!isNaN(newValue) && newValue >= 1 && newValue <= 500) { - setCachedStateField("maxTotalImageMemory", newValue) + setCachedStateField("maxTotalImageSize", newValue) } }} onClick={(e) => e.currentTarget.select()} - data-testid="max-total-image-memory-input" + data-testid="max-total-image-size-input" /> - {t("settings:contextManagement.maxTotalImageMemory.mb")} + {t("settings:contextManagement.maxTotalImageSize.mb")}
- {t("settings:contextManagement.maxTotalImageMemory.description")} + {t("settings:contextManagement.maxTotalImageSize.description")}
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 15f73a709d..1854585377 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -169,7 +169,7 @@ const SettingsView = forwardRef(({ onDone, t remoteBrowserEnabled, maxReadFileLine, maxImageFileSize, - maxTotalImageMemory, + maxTotalImageSize, terminalCompressProgressBar, maxConcurrentFileReads, condensingApiConfigId, @@ -324,7 +324,7 @@ const SettingsView = forwardRef(({ onDone, t vscode.postMessage({ type: "showRooIgnoredFiles", bool: showRooIgnoredFiles }) vscode.postMessage({ type: "maxReadFileLine", value: maxReadFileLine ?? -1 }) vscode.postMessage({ type: "maxImageFileSize", value: maxImageFileSize ?? 5 }) - vscode.postMessage({ type: "maxTotalImageMemory", value: maxTotalImageMemory ?? 20 }) + vscode.postMessage({ type: "maxTotalImageSize", value: maxTotalImageSize ?? 20 }) vscode.postMessage({ type: "maxConcurrentFileReads", value: cachedState.maxConcurrentFileReads ?? 5 }) vscode.postMessage({ type: "includeDiagnosticMessages", bool: includeDiagnosticMessages }) vscode.postMessage({ type: "maxDiagnosticMessages", value: maxDiagnosticMessages ?? 50 }) @@ -672,7 +672,7 @@ const SettingsView = forwardRef(({ onDone, t showRooIgnoredFiles={showRooIgnoredFiles} maxReadFileLine={maxReadFileLine} maxImageFileSize={maxImageFileSize} - maxTotalImageMemory={maxTotalImageMemory} + maxTotalImageSize={maxTotalImageSize} maxConcurrentFileReads={maxConcurrentFileReads} profileThresholds={profileThresholds} includeDiagnosticMessages={includeDiagnosticMessages} diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 16434c35d8..3d39613eda 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -122,8 +122,8 @@ export interface ExtensionStateContextType extends ExtensionState { setMaxReadFileLine: (value: number) => void maxImageFileSize: number setMaxImageFileSize: (value: number) => void - maxTotalImageMemory: number - setMaxTotalImageMemory: (value: number) => void + maxTotalImageSize: number + setMaxTotalImageSize: (value: number) => void machineId?: string pinnedApiConfigs?: Record setPinnedApiConfigs: (value: Record) => void @@ -213,7 +213,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode renderContext: "sidebar", maxReadFileLine: -1, // Default max read file line limit maxImageFileSize: 5, // Default max image file size in MB - maxTotalImageMemory: 20, // Default max total image memory in MB + maxTotalImageSize: 20, // Default max total image size in MB pinnedApiConfigs: {}, // Empty object for pinned API configs terminalZshOhMy: false, // Default Oh My Zsh integration setting maxConcurrentFileReads: 5, // Default concurrent file reads @@ -455,7 +455,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setAwsUsePromptCache: (value) => setState((prevState) => ({ ...prevState, awsUsePromptCache: value })), setMaxReadFileLine: (value) => setState((prevState) => ({ ...prevState, maxReadFileLine: value })), setMaxImageFileSize: (value) => setState((prevState) => ({ ...prevState, maxImageFileSize: value })), - setMaxTotalImageMemory: (value) => setState((prevState) => ({ ...prevState, maxTotalImageMemory: value })), + setMaxTotalImageSize: (value) => setState((prevState) => ({ ...prevState, maxTotalImageSize: value })), setPinnedApiConfigs: (value) => setState((prevState) => ({ ...prevState, pinnedApiConfigs: value })), setTerminalCompressProgressBar: (value) => setState((prevState) => ({ ...prevState, terminalCompressProgressBar: value })), diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 5ae9f49720..7264c85791 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -532,10 +532,10 @@ "mb": "MB", "description": "Mida màxima (en MB) per a arxius d'imatge que poden ser processats per l'eina de lectura d'arxius." }, - "maxTotalImageMemory": { - "label": "Memòria total màxima per a imatges", + "maxTotalImageSize": { + "label": "Mida total màxima d'imatges", "mb": "MB", - "description": "Memòria total màxima (en MB) per a totes les imatges en una sola operació de lectura. Prevé problemes de memòria en llegir múltiples imatges grans simultàniament." + "description": "Límit de mida acumulativa màxima (en MB) per a totes les imatges processades en una sola operació read_file. Quan es llegeixen múltiples imatges, la mida de cada imatge s'afegeix al total. Si incloure una altra imatge excediria aquest límit, serà omesa." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 36cd9a339d..1ba4286772 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -532,10 +532,10 @@ "mb": "MB", "description": "Maximale Größe (in MB) für Bilddateien, die vom read file Tool verarbeitet werden können." }, - "maxTotalImageMemory": { - "label": "Maximaler Gesamtspeicher für Bilder", + "maxTotalImageSize": { + "label": "Maximale Gesamtbildgröße", "mb": "MB", - "description": "Maximaler Gesamtspeicher (in MB) für alle Bilder in einem einzelnen Lesevorgang. Verhindert Speicherprobleme beim gleichzeitigen Lesen mehrerer großer Bilder." + "description": "Maximales kumulatives Größenlimit (in MB) für alle Bilder, die in einer einzelnen read_file-Operation verarbeitet werden. Beim Lesen mehrerer Bilder wird die Größe jedes Bildes zur Gesamtsumme addiert. Wenn das Einbeziehen eines weiteren Bildes dieses Limit überschreiten würde, wird es übersprungen." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 028eecacb6..8f40db0bff 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -507,10 +507,10 @@ "mb": "MB", "description": "Maximum size (in MB) for image files that can be processed by the read file tool." }, - "maxTotalImageMemory": { - "label": "Max total image memory", + "maxTotalImageSize": { + "label": "Max total image size", "mb": "MB", - "description": "Maximum total memory (in MB) for all images in a single read operation. Prevents memory issues when reading multiple large images simultaneously." + "description": "Maximum cumulative size limit (in MB) for all images processed in a single read_file operation. When reading multiple images, each image's size is added to the total. If including another image would exceed this limit, it will be skipped." }, "diagnostics": { "includeMessages": { diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index cccfbaa73c..e6c101285d 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -507,10 +507,10 @@ "mb": "MB", "description": "Tamaño máximo (en MB) para archivos de imagen que pueden ser procesados por la herramienta de lectura de archivos." }, - "maxTotalImageMemory": { - "label": "Memoria total máxima para imágenes", + "maxTotalImageSize": { + "label": "Tamaño total máximo de imágenes", "mb": "MB", - "description": "Memoria total máxima (en MB) para todas las imágenes en una sola operación de lectura. Previene problemas de memoria al leer múltiples imágenes grandes simultáneamente." + "description": "Límite de tamaño acumulativo máximo (en MB) para todas las imágenes procesadas en una sola operación read_file. Al leer múltiples imágenes, el tamaño de cada imagen se suma al total. Si incluir otra imagen excedería este límite, será omitida." }, "diagnostics": { "includeMessages": { diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 3f5b0e9530..bbe657082a 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -507,10 +507,10 @@ "mb": "MB", "description": "Taille maximale (en MB) pour les fichiers d'image qui peuvent être traités par l'outil de lecture de fichier." }, - "maxTotalImageMemory": { - "label": "Mémoire totale maximale pour les images", + "maxTotalImageSize": { + "label": "Taille totale maximale des images", "mb": "MB", - "description": "Mémoire totale maximale (en MB) pour toutes les images dans une seule opération de lecture. Empêche les problèmes de mémoire lors de la lecture simultanée de plusieurs grandes images." + "description": "Limite de taille cumulée maximale (en MB) pour toutes les images traitées dans une seule opération read_file. Lors de la lecture de plusieurs images, la taille de chaque image est ajoutée au total. Si l'inclusion d'une autre image dépasserait cette limite, elle sera ignorée." }, "diagnostics": { "includeMessages": { diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 69fc029eaa..33a23b2d11 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -533,10 +533,10 @@ "mb": "MB", "description": "छवि फ़ाइलों के लिए अधिकतम आकार (MB में) जो read file tool द्वारा प्रसंस्कृत किया जा सकता है।" }, - "maxTotalImageMemory": { - "label": "छवियों के लिए अधिकतम कुल मेमोरी", + "maxTotalImageSize": { + "label": "अधिकतम कुल छवि आकार", "mb": "MB", - "description": "एक ही पठन ऑपरेशन में सभी छवियों के लिए अधिकतम कुल मेमोरी (MB में)। एक साथ कई बड़ी छवियों को पढ़ते समय मेमोरी समस्याओं को रोकता है।" + "description": "एकल read_file ऑपरेशन में संसाधित सभी छवियों के लिए अधिकतम संचयी आकार सीमा (MB में)। कई छवियों को पढ़ते समय, प्रत्येक छवि का आकार कुल में जोड़ा जाता है। यदि किसी अन्य छवि को शामिल करने से यह सीमा पार हो जाएगी, तो उसे छोड़ दिया जाएगा।" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 252a20c1cd..12352cdeb8 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -537,10 +537,10 @@ "mb": "MB", "description": "Ukuran maksimum (dalam MB) untuk file gambar yang dapat diproses oleh alat baca file." }, - "maxTotalImageMemory": { - "label": "Total memori maksimum untuk gambar", + "maxTotalImageSize": { + "label": "Ukuran total gambar maksimum", "mb": "MB", - "description": "Total memori maksimum (dalam MB) untuk semua gambar dalam satu operasi baca. Mencegah masalah memori saat membaca beberapa gambar besar secara bersamaan." + "description": "Batas ukuran kumulatif maksimum (dalam MB) untuk semua gambar yang diproses dalam satu operasi read_file. Saat membaca beberapa gambar, ukuran setiap gambar ditambahkan ke total. Jika menyertakan gambar lain akan melebihi batas ini, gambar tersebut akan dilewati." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index eb6a6dfd5f..5bb2e422a6 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -533,10 +533,10 @@ "mb": "MB", "description": "Dimensione massima (in MB) per i file immagine che possono essere elaborati dallo strumento di lettura file." }, - "maxTotalImageMemory": { - "label": "Memoria totale massima per le immagini", + "maxTotalImageSize": { + "label": "Dimensione totale massima immagini", "mb": "MB", - "description": "Memoria totale massima (in MB) per tutte le immagini in una singola operazione di lettura. Previene problemi di memoria durante la lettura simultanea di più immagini grandi." + "description": "Limite di dimensione cumulativa massima (in MB) per tutte le immagini elaborate in una singola operazione read_file. Durante la lettura di più immagini, la dimensione di ogni immagine viene aggiunta al totale. Se l'inclusione di un'altra immagine supererebbe questo limite, verrà saltata." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 8e8d7dad4c..eb6c30bfe1 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -533,10 +533,10 @@ "mb": "MB", "description": "read fileツールで処理できる画像ファイルの最大サイズ(MB単位)。" }, - "maxTotalImageMemory": { - "label": "画像の最大合計メモリ", + "maxTotalImageSize": { + "label": "最大合計画像サイズ", "mb": "MB", - "description": "単一の読み取り操作ですべての画像に使用できる最大合計メモリ(MB単位)。複数の大きな画像を同時に読み取る際のメモリ問題を防ぎます。" + "description": "単一のread_file操作で処理されるすべての画像の累積サイズ制限(MB単位)。複数の画像を読み取る際、各画像のサイズが合計に加算されます。別の画像を含めるとこの制限を超える場合、その画像はスキップされます。" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 550e35320e..9bbaa6ca0c 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -533,10 +533,10 @@ "mb": "MB", "description": "read file 도구로 처리할 수 있는 이미지 파일의 최대 크기(MB 단위)입니다." }, - "maxTotalImageMemory": { - "label": "이미지 최대 총 메모리", + "maxTotalImageSize": { + "label": "최대 총 이미지 크기", "mb": "MB", - "description": "단일 읽기 작업에서 모든 이미지에 대한 최대 총 메모리(MB 단위). 여러 대용량 이미지를 동시에 읽을 때 메모리 문제를 방지합니다." + "description": "단일 read_file 작업에서 처리되는 모든 이미지의 최대 누적 크기 제한(MB 단위)입니다. 여러 이미지를 읽을 때 각 이미지의 크기가 총계에 추가됩니다. 다른 이미지를 포함하면 이 제한을 초과하는 경우 해당 이미지는 건너뜁니다." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 9cf0434274..228da40231 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -507,10 +507,10 @@ "mb": "MB", "description": "Maximale grootte (in MB) voor afbeeldingsbestanden die kunnen worden verwerkt door de read file tool." }, - "maxTotalImageMemory": { - "label": "Maximaal totaal afbeeldingsgeheugen", + "maxTotalImageSize": { + "label": "Maximale totale afbeeldingsgrootte", "mb": "MB", - "description": "Maximaal totaal geheugen (in MB) voor alle afbeeldingen in één leesbewerking. Voorkomt geheugenproblemen bij het gelijktijdig lezen van meerdere grote afbeeldingen." + "description": "Maximale cumulatieve groottelimiet (in MB) voor alle afbeeldingen die in één read_file-bewerking worden verwerkt. Bij het lezen van meerdere afbeeldingen wordt de grootte van elke afbeelding bij het totaal opgeteld. Als het toevoegen van een andere afbeelding deze limiet zou overschrijden, wordt deze overgeslagen." }, "diagnostics": { "includeMessages": { diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index bfacaea3b3..615c0d68d7 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -533,10 +533,10 @@ "mb": "MB", "description": "Maksymalny rozmiar (w MB) plików obrazów, które mogą być przetwarzane przez narzędzie do czytania plików." }, - "maxTotalImageMemory": { - "label": "Maksymalna całkowita pamięć dla obrazów", + "maxTotalImageSize": { + "label": "Maksymalny całkowity rozmiar obrazów", "mb": "MB", - "description": "Maksymalna całkowita pamięć (w MB) dla wszystkich obrazów w jednej operacji odczytu. Zapobiega problemom z pamięcią podczas jednoczesnego odczytu wielu dużych obrazów." + "description": "Maksymalny skumulowany limit rozmiaru (w MB) dla wszystkich obrazów przetwarzanych w jednej operacji read_file. Podczas odczytu wielu obrazów rozmiar każdego obrazu jest dodawany do sumy. Jeśli dołączenie kolejnego obrazu przekroczyłoby ten limit, zostanie on pominięty." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 52c6720373..96ab854bfb 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -533,10 +533,10 @@ "mb": "MB", "description": "Tamanho máximo (em MB) para arquivos de imagem que podem ser processados pela ferramenta de leitura de arquivos." }, - "maxTotalImageMemory": { - "label": "Memória total máxima para imagens", + "maxTotalImageSize": { + "label": "Tamanho total máximo da imagem", "mb": "MB", - "description": "Memória total máxima (em MB) para todas as imagens em uma única operação de leitura. Evita problemas de memória ao ler múltiplas imagens grandes simultaneamente." + "description": "Limite máximo de tamanho cumulativo (em MB) para todas as imagens processadas em uma única operação read_file. Ao ler várias imagens, o tamanho de cada imagem é adicionado ao total. Se incluir outra imagem exceder esse limite, ela será ignorada." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 2927c30f94..bda495d04a 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -533,10 +533,10 @@ "mb": "MB", "description": "Максимальный размер (в МБ) для файлов изображений, которые могут быть обработаны инструментом чтения файлов." }, - "maxTotalImageMemory": { - "label": "Максимальная общая память для изображений", - "mb": "MB", - "description": "Максимальная общая память (в МБ) для всех изображений в одной операции чтения. Предотвращает проблемы с памятью при одновременном чтении нескольких больших изображений." + "maxTotalImageSize": { + "label": "Максимальный общий размер изображений", + "mb": "МБ", + "description": "Максимальный совокупный лимит размера (в МБ) для всех изображений, обрабатываемых в одной операции read_file. При чтении нескольких изображений размер каждого изображения добавляется к общему. Если включение другого изображения превысит этот лимит, оно будет пропущено." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index d3c225c646..e3e71633f3 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -533,10 +533,10 @@ "mb": "MB", "description": "Dosya okuma aracı tarafından işlenebilecek görüntü dosyaları için maksimum boyut (MB cinsinden)." }, - "maxTotalImageMemory": { - "label": "Görüntüler için maksimum toplam bellek", + "maxTotalImageSize": { + "label": "Maksimum toplam görüntü boyutu", "mb": "MB", - "description": "Tek bir okuma işlemindeki tüm görüntüler için maksimum toplam bellek (MB cinsinden). Birden çok büyük görüntüyü eş zamanlı okurken bellek sorunlarını önler." + "description": "Tek bir read_file işleminde işlenen tüm görüntüler için maksimum kümülatif boyut sınırı (MB cinsinden). Birden çok görüntü okurken, her görüntünün boyutu toplama eklenir. Başka bir görüntü eklemek bu sınırı aşacaksa, atlanacaktır." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 56b0c98859..9bcbb1263a 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -533,10 +533,10 @@ "mb": "MB", "description": "Kích thước tối đa (tính bằng MB) cho các tệp hình ảnh có thể được xử lý bởi công cụ đọc tệp." }, - "maxTotalImageMemory": { - "label": "Bộ nhớ tổng tối đa cho hình ảnh", + "maxTotalImageSize": { + "label": "Kích thước tổng tối đa của hình ảnh", "mb": "MB", - "description": "Bộ nhớ tổng tối đa (tính bằng MB) cho tất cả hình ảnh trong một hoạt động đọc. Ngăn ngừa vấn đề bộ nhớ khi đọc đồng thời nhiều hình ảnh lớn." + "description": "Giới hạn kích thước tích lũy tối đa (tính bằng MB) cho tất cả hình ảnh được xử lý trong một thao tác read_file duy nhất. Khi đọc nhiều hình ảnh, kích thước của mỗi hình ảnh được cộng vào tổng. Nếu việc thêm một hình ảnh khác sẽ vượt quá giới hạn này, nó sẽ bị bỏ qua." } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 264d2d4933..5cbd80aed6 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -533,10 +533,10 @@ "mb": "MB", "description": "read file工具可以处理的图像文件的最大大小(以MB为单位)。" }, - "maxTotalImageMemory": { - "label": "图像最大总内存", + "maxTotalImageSize": { + "label": "图片总大小上限", "mb": "MB", - "description": "单次读取操作中所有图像的最大总内存(以MB为单位)。防止同时读取多个大图像时出现内存问题。" + "description": "单次 read_file 操作中处理的所有图片的最大累计大小限制(MB)。读取多张图片时,每张图片的大小会累加到总大小中。如果包含另一张图片会超过此限制,则会跳过该图片。" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index be55a5de41..b9e9f1ef06 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -533,10 +533,10 @@ "mb": "MB", "description": "read file工具可以處理的圖像檔案的最大大小(以MB為單位)。" }, - "maxTotalImageMemory": { - "label": "圖像最大總記憶體", + "maxTotalImageSize": { + "label": "圖片總大小上限", "mb": "MB", - "description": "單次讀取操作中所有圖像的最大總記憶體(以MB為單位)。防止同時讀取多個大圖像時出現記憶體問題。" + "description": "單次 read_file 操作中處理的所有圖片的最大累計大小限制(MB)。讀取多張圖片時,每張圖片的大小會累加到總大小中。如果包含另一張圖片會超過此限制,則會跳過該圖片。" } }, "terminal": { From 1ead3953a2a6308ca58caad90b165ffb16ddd511 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Mon, 28 Jul 2025 11:32:03 -0500 Subject: [PATCH 19/21] fix: update test to use maxTotalImageSize instead of maxTotalImageMemory --- src/core/webview/__tests__/ClineProvider.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 21232f6649..98de24a1cd 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -534,7 +534,7 @@ describe("ClineProvider", () => { renderContext: "sidebar", maxReadFileLine: 500, maxImageFileSize: 5, - maxTotalImageMemory: 20, + maxTotalImageSize: 20, cloudUserInfo: null, organizationAllowList: ORGANIZATION_ALLOW_ALL, autoCondenseContext: true, From 701d5fc0db88604bdff58a8c32518a3058fe2968 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Mon, 28 Jul 2025 11:32:50 -0500 Subject: [PATCH 20/21] fix: update webview test to use maxTotalImageSize --- webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx index 5b4874a6ff..d9f8101113 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx @@ -210,7 +210,7 @@ describe("mergeExtensionState", () => { profileThresholds: {}, hasOpenedModeSelector: false, // Add the new required property maxImageFileSize: 5, - maxTotalImageMemory: 20 + maxTotalImageSize: 20, } const prevState: ExtensionState = { From 32a68d2baf4a711460340c4e4e69720c5c0d70a7 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Mon, 28 Jul 2025 11:45:54 -0500 Subject: [PATCH 21/21] refactor: move image processing logic to imageHelpers - Extract image validation logic into validateImageForProcessing function - Extract image processing logic into processImageFile function - Add ImageMemoryTracker class to encapsulate memory tracking - Add proper TypeScript interfaces for validation and processing results - Reduce code duplication and improve separation of concerns --- src/core/tools/helpers/imageHelpers.ts | 124 +++++++++++++++++++++++++ src/core/tools/readFileTool.ts | 70 ++++---------- 2 files changed, 143 insertions(+), 51 deletions(-) diff --git a/src/core/tools/helpers/imageHelpers.ts b/src/core/tools/helpers/imageHelpers.ts index d238e98f42..a1adb078e6 100644 --- a/src/core/tools/helpers/imageHelpers.ts +++ b/src/core/tools/helpers/imageHelpers.ts @@ -1,5 +1,7 @@ import path from "path" import * as fs from "fs/promises" +import { t } from "../../../i18n" +import prettyBytes from "pretty-bytes" /** * Default maximum allowed image file size in bytes (5MB) @@ -46,6 +48,27 @@ export const IMAGE_MIME_TYPES: Record = { ".avif": "image/avif", } +/** + * Result of image validation + */ +export interface ImageValidationResult { + isValid: boolean + reason?: "size_limit" | "memory_limit" | "unsupported_model" + notice?: string + sizeInMB?: number +} + +/** + * Result of image processing + */ +export interface ImageProcessingResult { + dataUrl: string + buffer: Buffer + sizeInKB: number + sizeInMB: number + notice: string +} + /** * Reads an image file and returns both the data URL and buffer */ @@ -66,3 +89,104 @@ export async function readImageAsDataUrlWithBuffer(filePath: string): Promise<{ export function isSupportedImageFormat(extension: string): boolean { return SUPPORTED_IMAGE_FORMATS.includes(extension.toLowerCase() as (typeof SUPPORTED_IMAGE_FORMATS)[number]) } + +/** + * Validates if an image can be processed based on size limits and model support + */ +export async function validateImageForProcessing( + fullPath: string, + supportsImages: boolean, + maxImageFileSize: number, + maxTotalImageSize: number, + currentTotalMemoryUsed: number, +): Promise { + // Check if model supports images + if (!supportsImages) { + return { + isValid: false, + reason: "unsupported_model", + notice: "Image file detected but current model does not support images. Skipping image processing.", + } + } + + const imageStats = await fs.stat(fullPath) + const imageSizeInMB = imageStats.size / (1024 * 1024) + + // Check individual file size limit + if (imageStats.size > maxImageFileSize * 1024 * 1024) { + const imageSizeFormatted = prettyBytes(imageStats.size) + return { + isValid: false, + reason: "size_limit", + notice: t("tools:readFile.imageTooLarge", { + size: imageSizeFormatted, + max: maxImageFileSize, + }), + sizeInMB: imageSizeInMB, + } + } + + // Check total memory limit + if (currentTotalMemoryUsed + imageSizeInMB > maxTotalImageSize) { + const currentMemoryFormatted = prettyBytes(currentTotalMemoryUsed * 1024 * 1024) + const fileMemoryFormatted = prettyBytes(imageStats.size) + return { + isValid: false, + reason: "memory_limit", + notice: `Image skipped to avoid size limit (${maxTotalImageSize}MB). Current: ${currentMemoryFormatted} + this file: ${fileMemoryFormatted}. Try fewer or smaller images.`, + sizeInMB: imageSizeInMB, + } + } + + return { + isValid: true, + sizeInMB: imageSizeInMB, + } +} + +/** + * Processes an image file and returns the result + */ +export async function processImageFile(fullPath: string): Promise { + const imageStats = await fs.stat(fullPath) + const { dataUrl, buffer } = await readImageAsDataUrlWithBuffer(fullPath) + const imageSizeInKB = Math.round(imageStats.size / 1024) + const imageSizeInMB = imageStats.size / (1024 * 1024) + const noticeText = t("tools:readFile.imageWithSize", { size: imageSizeInKB }) + + return { + dataUrl, + buffer, + sizeInKB: imageSizeInKB, + sizeInMB: imageSizeInMB, + notice: noticeText, + } +} + +/** + * Memory tracker for image processing + */ +export class ImageMemoryTracker { + private totalMemoryUsed: number = 0 + + /** + * Gets the current total memory used in MB + */ + getTotalMemoryUsed(): number { + return this.totalMemoryUsed + } + + /** + * Adds to the total memory used + */ + addMemoryUsage(sizeInMB: number): void { + this.totalMemoryUsed += sizeInMB + } + + /** + * Resets the memory tracker + */ + reset(): void { + this.totalMemoryUsed = 0 + } +} diff --git a/src/core/tools/readFileTool.ts b/src/core/tools/readFileTool.ts index e21f4f9530..01427f4d9d 100644 --- a/src/core/tools/readFileTool.ts +++ b/src/core/tools/readFileTool.ts @@ -1,6 +1,5 @@ import path from "path" import { isBinaryFile } from "isbinaryfile" -import prettyBytes from "pretty-bytes" import { Task } from "../task/Task" import { ClineSayTool } from "../../shared/ExtensionMessage" @@ -15,12 +14,13 @@ import { readLines } from "../../integrations/misc/read-lines" import { extractTextFromFile, addLineNumbers, getSupportedBinaryFormats } from "../../integrations/misc/extract-text" import { parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter" import { parseXml } from "../../utils/xml" -import * as fs from "fs/promises" import { DEFAULT_MAX_IMAGE_FILE_SIZE_MB, DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB, - readImageAsDataUrlWithBuffer, isSupportedImageFormat, + validateImageForProcessing, + processImageFile, + ImageMemoryTracker, } from "./helpers/imageHelpers" export function getReadFileToolDescription(blockName: string, blockParams: any): string { @@ -434,7 +434,7 @@ export async function readFileTool( } // Track total image memory usage across all files - let totalImageMemoryUsed = 0 + const imageMemoryTracker = new ImageMemoryTracker() const state = await cline.providerRef.deref()?.getState() const { maxReadFileLine = -1, @@ -463,71 +463,39 @@ export async function readFileTool( // Check if it's a supported image format if (isSupportedImageFormat(fileExtension)) { - // Skip image processing if model doesn't support images - if (!supportsImages) { - const notice = - "Image file detected but current model does not support images. Skipping image processing." - - // Track file read - await cline.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource) - - updateFileResult(relPath, { - xmlContent: `${relPath}\n${notice}\n`, - }) - continue - } - try { - const imageStats = await fs.stat(fullPath) - - // Check if image file exceeds individual size limit - if (imageStats.size > maxImageFileSize * 1024 * 1024) { - const imageSizeFormatted = prettyBytes(imageStats.size) - const notice = t("tools:readFile.imageTooLarge", { - size: imageSizeFormatted, - max: maxImageFileSize, - }) + // Validate image for processing + const validationResult = await validateImageForProcessing( + fullPath, + supportsImages, + maxImageFileSize, + maxTotalImageSize, + imageMemoryTracker.getTotalMemoryUsed(), + ) + if (!validationResult.isValid) { // Track file read await cline.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource) updateFileResult(relPath, { - xmlContent: `${relPath}\n${notice}\n`, + xmlContent: `${relPath}\n${validationResult.notice}\n`, }) continue } - // Check if adding this image would exceed total memory limit - const imageSizeInMB = imageStats.size / (1024 * 1024) - if (totalImageMemoryUsed + imageSizeInMB > maxTotalImageSize) { - const currentMemoryFormatted = prettyBytes(totalImageMemoryUsed * 1024 * 1024) - const fileMemoryFormatted = prettyBytes(imageStats.size) - const notice = `Image skipped to avoid size limit (${maxTotalImageSize}MB). Current: ${currentMemoryFormatted} + this file: ${fileMemoryFormatted}. Try fewer or smaller images.` - - // Track file read - await cline.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource) - - updateFileResult(relPath, { - xmlContent: `${relPath}\n${notice}\n`, - }) - continue - } + // Process the image + const imageResult = await processImageFile(fullPath) // Track memory usage for this image - totalImageMemoryUsed += imageSizeInMB - - const { dataUrl: imageDataUrl, buffer } = await readImageAsDataUrlWithBuffer(fullPath) - const imageSizeInKB = Math.round(imageStats.size / 1024) + imageMemoryTracker.addMemoryUsage(imageResult.sizeInMB) // Track file read await cline.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource) // Store image data URL separately - NOT in XML - const noticeText = t("tools:readFile.imageWithSize", { size: imageSizeInKB }) - updateFileResult(relPath, { - xmlContent: `${relPath}\n${noticeText}\n`, - imageDataUrl: imageDataUrl, + xmlContent: `${relPath}\n${imageResult.notice}\n`, + imageDataUrl: imageResult.dataUrl, }) continue } catch (error) {