Skip to content

Commit 8b43962

Browse files
committed
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
1 parent 3038800 commit 8b43962

29 files changed

+249
-119
lines changed

packages/types/src/global-settings.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export const globalSettingsSchema = z.object({
102102
showRooIgnoredFiles: z.boolean().optional(),
103103
maxReadFileLine: z.number().optional(),
104104
maxImageFileSize: z.number().optional(),
105-
maxTotalImageMemory: z.number().optional(),
105+
maxTotalImageSize: z.number().optional(),
106106

107107
terminalOutputLineLimit: z.number().optional(),
108108
terminalOutputCharacterLimit: z.number().optional(),

src/core/tools/__tests__/readFileTool.spec.ts

Lines changed: 156 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { isBinaryFile } from "isbinaryfile"
1010
import { ReadFileToolUse, ToolParamName, ToolResponse } from "../../../shared/tools"
1111
import { readFileTool } from "../readFileTool"
1212
import { formatResponse } from "../../prompts/responses"
13-
import { DEFAULT_MAX_IMAGE_FILE_SIZE_MB, DEFAULT_MAX_TOTAL_IMAGE_MEMORY_MB } from "../helpers/imageHelpers"
13+
import { DEFAULT_MAX_IMAGE_FILE_SIZE_MB, DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB } from "../helpers/imageHelpers"
1414

1515
vi.mock("path", async () => {
1616
const originalPath = await vi.importActual("path")
@@ -156,7 +156,7 @@ vi.mock("../../../i18n", () => ({
156156
const translations: Record<string, string> = {
157157
"tools:readFile.imageWithSize": "Image file ({{size}} KB)",
158158
"tools:readFile.imageTooLarge":
159-
"Image file is too large ({{size}} MB). The maximum allowed size is {{max}} MB.",
159+
"Image file is too large ({{size}}). The maximum allowed size is {{max}} MB.",
160160
"tools:readFile.linesRange": " (lines {{start}}-{{end}})",
161161
"tools:readFile.definitionsOnly": " (definitions only)",
162162
"tools:readFile.maxLines": " (max {{max}} lines)",
@@ -296,7 +296,7 @@ describe("read_file tool with maxReadFileLine setting", () => {
296296
const maxReadFileLine = options.maxReadFileLine ?? 500
297297
const totalLines = options.totalLines ?? 5
298298

299-
mockProvider.getState.mockResolvedValue({ maxReadFileLine, maxImageFileSize: 20, maxTotalImageMemory: 20 })
299+
mockProvider.getState.mockResolvedValue({ maxReadFileLine, maxImageFileSize: 20, maxTotalImageSize: 20 })
300300
mockedCountFileLines.mockResolvedValue(totalLines)
301301

302302
// Reset the spy before each test
@@ -532,7 +532,7 @@ describe("read_file tool XML output structure", () => {
532532
mockInputContent = fileContent
533533

534534
// Setup mock provider with default maxReadFileLine
535-
mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, maxTotalImageMemory: 20 }) // Default to full file read
535+
mockProvider.getState.mockResolvedValue({ maxReadFileLine: -1, maxImageFileSize: 20, maxTotalImageSize: 20 }) // Default to full file read
536536

537537
// Add additional properties needed for XML tests
538538
mockCline.sayAndCreateMissingParamError = vi.fn().mockResolvedValue("Missing required parameter")
@@ -557,7 +557,7 @@ describe("read_file tool XML output structure", () => {
557557
const isBinary = options.isBinary ?? false
558558
const validateAccess = options.validateAccess ?? true
559559

560-
mockProvider.getState.mockResolvedValue({ maxReadFileLine, maxImageFileSize: 20, maxTotalImageMemory: 20 })
560+
mockProvider.getState.mockResolvedValue({ maxReadFileLine, maxImageFileSize: 20, maxTotalImageSize: 20 })
561561
mockedCountFileLines.mockResolvedValue(totalLines)
562562
mockedIsBinaryFile.mockResolvedValue(isBinary)
563563
mockCline.rooIgnoreController.validateAccess = vi.fn().mockReturnValue(validateAccess)
@@ -599,8 +599,8 @@ describe("read_file tool XML output structure", () => {
599599
mockProvider.getState.mockResolvedValue({
600600
maxReadFileLine: -1,
601601
maxImageFileSize: 20,
602-
maxTotalImageMemory: 20,
603-
}) // Allow up to 20MB per image and total memory
602+
maxTotalImageSize: 20,
603+
}) // Allow up to 20MB per image and total size
604604

605605
// Execute
606606
const result = await executeReadFileTool()
@@ -632,8 +632,8 @@ describe("read_file tool XML output structure", () => {
632632
mockProvider.getState.mockResolvedValue({
633633
maxReadFileLine: -1,
634634
maxImageFileSize: 20,
635-
maxTotalImageMemory: 20,
636-
}) // Allow up to 20MB per image and total memory
635+
maxTotalImageSize: 20,
636+
}) // Allow up to 20MB per image and total size
637637

638638
// Execute
639639
const result = await executeReadFileTool({}, { totalLines: 0 })
@@ -651,6 +651,12 @@ describe("read_file tool XML output structure", () => {
651651
{ path: "test/image3.gif", sizeKB: 8192 }, // 8MB
652652
]
653653

654+
// Define imageBuffer for this test suite
655+
const imageBuffer = Buffer.from(
656+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==",
657+
"base64",
658+
)
659+
654660
beforeEach(() => {
655661
// CRITICAL: Reset fsPromises mocks to prevent cross-test contamination within this suite
656662
fsPromises.stat.mockClear()
@@ -707,8 +713,8 @@ describe("read_file tool XML output structure", () => {
707713
mockProvider.getState.mockResolvedValue({
708714
maxReadFileLine: -1,
709715
maxImageFileSize: 20,
710-
maxTotalImageMemory: 20,
711-
}) // Allow up to 20MB per image and total memory
716+
maxTotalImageSize: 20,
717+
}) // Allow up to 20MB per image and total size
712718

713719
// Setup mockCline properties (preserve existing API)
714720
mockCline.cwd = "/"
@@ -780,8 +786,8 @@ describe("read_file tool XML output structure", () => {
780786
mockProvider.getState.mockResolvedValue({
781787
maxReadFileLine: -1,
782788
maxImageFileSize: 15,
783-
maxTotalImageMemory: 20,
784-
}) // Allow up to 15MB per image and 20MB total memory
789+
maxTotalImageSize: 20,
790+
}) // Allow up to 15MB per image and 20MB total size
785791

786792
// Setup mockCline properties
787793
mockCline.cwd = "/"
@@ -844,9 +850,9 @@ describe("read_file tool XML output structure", () => {
844850
expect(imageParts).toHaveLength(4) // First 4 images should be included (~19.6MB total)
845851

846852
// Verify memory limit notice for the fifth image
847-
expect(textPart).toContain(
848-
"Image skipped to avoid memory limit (20MB). Current: 19.6MB + this file: 4.9MB. Try fewer or smaller images.",
849-
)
853+
expect(textPart).toContain("Image skipped to avoid size limit (20MB)")
854+
expect(textPart).toMatch(/Current: \d+(\.\d+)? MB/)
855+
expect(textPart).toMatch(/this file: \d+(\.\d+)? MB/)
850856
})
851857

852858
it("should track memory usage correctly across multiple images", async () => {
@@ -866,8 +872,8 @@ describe("read_file tool XML output structure", () => {
866872
mockProvider.getState.mockResolvedValue({
867873
maxReadFileLine: -1,
868874
maxImageFileSize: 15,
869-
maxTotalImageMemory: 20,
870-
}) // Allow up to 15MB per image and 20MB total memory
875+
maxTotalImageSize: 20,
876+
}) // Allow up to 15MB per image and 20MB total size
871877

872878
// Setup mockCline properties
873879
mockCline.cwd = "/"
@@ -917,9 +923,9 @@ describe("read_file tool XML output structure", () => {
917923
const imageParts = Array.isArray(result) ? result.filter((p) => p.type === "image") : []
918924

919925
expect(imageParts).toHaveLength(2) // First 2 images should fit
920-
expect(textPart).toContain(
921-
"Image skipped to avoid memory limit (20MB). Current: 20.0MB + this file: 1.0MB. Try fewer or smaller images.",
922-
)
926+
expect(textPart).toContain("Image skipped to avoid size limit (20MB)")
927+
expect(textPart).toMatch(/Current: \d+(\.\d+)? MB/)
928+
expect(textPart).toMatch(/this file: \d+(\.\d+)? MB/)
923929
})
924930

925931
it("should handle individual image size limit and total memory limit together", async () => {
@@ -939,8 +945,8 @@ describe("read_file tool XML output structure", () => {
939945
mockProvider.getState.mockResolvedValue({
940946
maxReadFileLine: -1,
941947
maxImageFileSize: 20,
942-
maxTotalImageMemory: 20,
943-
}) // Allow up to 20MB per image and total memory
948+
maxTotalImageSize: 20,
949+
}) // Allow up to 20MB per image and total size
944950

945951
// Setup mockCline properties (complete setup)
946952
mockCline.cwd = "/"
@@ -981,7 +987,7 @@ describe("read_file tool XML output structure", () => {
981987
mockProvider.getState.mockResolvedValue({
982988
maxReadFileLine: -1,
983989
maxImageFileSize: 5,
984-
maxTotalImageMemory: 20,
990+
maxTotalImageSize: 20,
985991
})
986992

987993
// Mock path.resolve
@@ -1001,7 +1007,9 @@ describe("read_file tool XML output structure", () => {
10011007
expect(imageParts).toHaveLength(2)
10021008

10031009
// Should show individual size limit violation
1004-
expect(textPart).toContain("Image file is too large (6.0 MB). The maximum allowed size is 5 MB.")
1010+
expect(textPart).toMatch(
1011+
/Image file is too large \(\d+(\.\d+)? MB\)\. The maximum allowed size is 5 MB\./,
1012+
)
10051013
})
10061014

10071015
it("should correctly calculate total memory and skip the last image", async () => {
@@ -1015,7 +1023,7 @@ describe("read_file tool XML output structure", () => {
10151023
mockProvider.getState.mockResolvedValue({
10161024
maxReadFileLine: -1,
10171025
maxImageFileSize: 10, // 10MB per image
1018-
maxTotalImageMemory: 20, // 20MB total
1026+
maxTotalImageSize: 20, // 20MB total
10191027
})
10201028

10211029
mockedIsBinaryFile.mockResolvedValue(true)
@@ -1040,9 +1048,9 @@ describe("read_file tool XML output structure", () => {
10401048
const imageParts = parts.filter((p) => p.type === "image")
10411049

10421050
expect(imageParts).toHaveLength(2) // First two images should be processed
1043-
expect(textPart).toContain("Image skipped to avoid memory limit (20MB)")
1044-
expect(textPart).toContain("Current: 16.0MB")
1045-
expect(textPart).toContain("this file: 8.0MB")
1051+
expect(textPart).toContain("Image skipped to avoid size limit (20MB)")
1052+
expect(textPart).toMatch(/Current: \d+(\.\d+)? MB/)
1053+
expect(textPart).toMatch(/this file: \d+(\.\d+)? MB/)
10461054
})
10471055

10481056
it("should reset total memory tracking for each tool invocation", async () => {
@@ -1062,7 +1070,7 @@ describe("read_file tool XML output structure", () => {
10621070
mockProvider.getState.mockResolvedValue({
10631071
maxReadFileLine: -1,
10641072
maxImageFileSize: 20,
1065-
maxTotalImageMemory: 20,
1073+
maxTotalImageSize: 20,
10661074
})
10671075

10681076
// Setup mockCline properties (complete setup)
@@ -1106,7 +1114,7 @@ describe("read_file tool XML output structure", () => {
11061114
mockProvider.getState.mockResolvedValue({
11071115
maxReadFileLine: -1,
11081116
maxImageFileSize: 20,
1109-
maxTotalImageMemory: 20,
1117+
maxTotalImageSize: 20,
11101118
})
11111119

11121120
// Reset path resolving for second batch
@@ -1162,6 +1170,123 @@ describe("read_file tool XML output structure", () => {
11621170

11631171
expect(imageParts).toHaveLength(1) // Second image should be processed
11641172
})
1173+
1174+
it("should handle a folder with many images just under the individual size limit", async () => {
1175+
// Setup - Create many images that are each just under the 5MB individual limit
1176+
// but together approach the 20MB total limit
1177+
const manyImages = [
1178+
{ path: "test/img1.png", sizeKB: 4900 }, // 4.78MB
1179+
{ path: "test/img2.png", sizeKB: 4900 }, // 4.78MB
1180+
{ path: "test/img3.png", sizeKB: 4900 }, // 4.78MB
1181+
{ path: "test/img4.png", sizeKB: 4900 }, // 4.78MB
1182+
{ path: "test/img5.png", sizeKB: 4900 }, // 4.78MB - This should be skipped (total would be ~23.9MB)
1183+
]
1184+
1185+
// Setup mocks
1186+
mockedIsBinaryFile.mockResolvedValue(true)
1187+
mockedCountFileLines.mockResolvedValue(0)
1188+
fsPromises.readFile.mockResolvedValue(imageBuffer)
1189+
1190+
// Setup provider with 5MB individual limit and 20MB total limit
1191+
mockProvider.getState.mockResolvedValue({
1192+
maxReadFileLine: -1,
1193+
maxImageFileSize: 5,
1194+
maxTotalImageSize: 20,
1195+
})
1196+
1197+
// Mock file stats for each image
1198+
fsPromises.stat = vi.fn().mockImplementation((filePath) => {
1199+
const normalizedFilePath = path.normalize(filePath.toString())
1200+
const image = manyImages.find((img) => normalizedFilePath.includes(path.normalize(img.path)))
1201+
return Promise.resolve({ size: (image?.sizeKB || 1024) * 1024 })
1202+
})
1203+
1204+
// Mock path.resolve
1205+
mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`)
1206+
1207+
// Execute
1208+
const result = await executeReadMultipleImagesTool(manyImages.map((img) => img.path))
1209+
1210+
// Verify
1211+
expect(Array.isArray(result)).toBe(true)
1212+
const parts = result as any[]
1213+
const textPart = parts.find((p) => p.type === "text")?.text
1214+
const imageParts = parts.filter((p) => p.type === "image")
1215+
1216+
// Should process first 4 images (total ~19.12MB, under 20MB limit)
1217+
expect(imageParts).toHaveLength(4)
1218+
1219+
// Should show memory limit notice for the 5th image
1220+
expect(textPart).toContain("Image skipped to avoid size limit (20MB)")
1221+
expect(textPart).toContain("test/img5.png")
1222+
1223+
// Verify memory tracking worked correctly
1224+
// The notice should show current memory usage around 20MB (4 * 4900KB ≈ 19.14MB, displayed as 20.1MB)
1225+
expect(textPart).toMatch(/Current: \d+(\.\d+)? MB/)
1226+
})
1227+
1228+
it("should reset memory tracking between separate tool invocations more explicitly", async () => {
1229+
// This test verifies that totalImageMemoryUsed is reset between calls
1230+
// by making two separate tool invocations and ensuring the second one
1231+
// starts with fresh memory tracking
1232+
1233+
// Setup mocks
1234+
mockedIsBinaryFile.mockResolvedValue(true)
1235+
mockedCountFileLines.mockResolvedValue(0)
1236+
fsPromises.readFile.mockResolvedValue(imageBuffer)
1237+
1238+
// Setup provider
1239+
mockProvider.getState.mockResolvedValue({
1240+
maxReadFileLine: -1,
1241+
maxImageFileSize: 20,
1242+
maxTotalImageSize: 20,
1243+
})
1244+
1245+
// First invocation - use 15MB of memory
1246+
const firstBatch = [{ path: "test/large1.png", sizeKB: 15360 }] // 15MB
1247+
1248+
fsPromises.stat = vi.fn().mockResolvedValue({ size: 15360 * 1024 })
1249+
mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`)
1250+
1251+
// Execute first batch
1252+
const result1 = await executeReadMultipleImagesTool(firstBatch.map((img) => img.path))
1253+
1254+
// Verify first batch processed successfully
1255+
expect(Array.isArray(result1)).toBe(true)
1256+
const parts1 = result1 as any[]
1257+
const imageParts1 = parts1.filter((p) => p.type === "image")
1258+
expect(imageParts1).toHaveLength(1)
1259+
1260+
// Second invocation - should start with 0 memory used, not 15MB
1261+
// If memory tracking wasn't reset, this 18MB image would be rejected
1262+
const secondBatch = [{ path: "test/large2.png", sizeKB: 18432 }] // 18MB
1263+
1264+
// Reset mocks for second invocation
1265+
fsPromises.stat.mockClear()
1266+
fsPromises.readFile.mockClear()
1267+
mockedPathResolve.mockClear()
1268+
1269+
fsPromises.stat = vi.fn().mockResolvedValue({ size: 18432 * 1024 })
1270+
fsPromises.readFile.mockResolvedValue(imageBuffer)
1271+
mockedPathResolve.mockImplementation((cwd, relPath) => `/${relPath}`)
1272+
1273+
// Execute second batch
1274+
const result2 = await executeReadMultipleImagesTool(secondBatch.map((img) => img.path))
1275+
1276+
// Verify second batch processed successfully
1277+
expect(Array.isArray(result2)).toBe(true)
1278+
const parts2 = result2 as any[]
1279+
const imageParts2 = parts2.filter((p) => p.type === "image")
1280+
const textPart2 = parts2.find((p) => p.type === "text")?.text
1281+
1282+
// The 18MB image should be processed successfully because memory was reset
1283+
expect(imageParts2).toHaveLength(1)
1284+
1285+
// Should NOT contain any memory limit notices
1286+
expect(textPart2).not.toContain("Image skipped to avoid memory limit")
1287+
1288+
// This proves memory tracking was reset between invocations
1289+
})
11651290
})
11661291
})
11671292

src/core/tools/helpers/imageHelpers.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@ export const DEFAULT_MAX_IMAGE_FILE_SIZE_MB = 5
88

99
/**
1010
* Default maximum total memory usage for all images in a single read operation (20MB)
11-
* This prevents memory issues when reading multiple large images simultaneously
11+
* This is a cumulative limit - as each image is processed, its size is added to the total.
12+
* If including another image would exceed this limit, it will be skipped with a notice.
13+
* Example: With a 20MB limit, reading 3 images of 8MB, 7MB, and 10MB would process
14+
* the first two (15MB total) but skip the third to stay under the limit.
1215
*/
13-
export const DEFAULT_MAX_TOTAL_IMAGE_MEMORY_MB = 20
16+
export const DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB = 20
1417

1518
/**
1619
* Supported image formats that can be displayed

0 commit comments

Comments
 (0)