diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 288f6c2118c..961667c7e0f 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -167,6 +167,20 @@ export const globalSettingsSchema = z.object({ maxImageFileSize: z.number().optional(), maxTotalImageSize: z.number().optional(), + /** + * Maximum dimension (width or height) in pixels for image downscaling before LLM upload. + * Images exceeding this dimension are proportionally resized, preserving aspect ratio. + * Set to 0 (default) to disable downscaling. + */ + maxImageDimension: z.number().min(0).optional(), + + /** + * JPEG/WebP quality (1-100) used when re-encoding resized images. + * Only applies when maxImageDimension is set and the image is downscaled. + * @default 85 + */ + imageDownscaleQuality: z.number().min(1).max(100).optional(), + terminalOutputPreviewSize: z.enum(["small", "medium", "large"]).optional(), terminalShellIntegrationTimeout: z.number().optional(), terminalShellIntegrationDisabled: z.boolean().optional(), diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index b20539afe49..963c2c14e72 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -330,6 +330,8 @@ export type ExtensionState = Pick< maxReadFileLine?: number // Maximum line limit for read_file tool (-1 for default) maxImageFileSize: number // Maximum size of image files to process in MB maxTotalImageSize: number // Maximum total size for all images in a single read operation in MB + maxImageDimension?: number // Maximum dimension (width or height) in pixels for image downscaling (0 = disabled) + imageDownscaleQuality?: number // JPEG/WebP quality (1-100) for re-encoding resized images (default 85) experiments: Experiments // Map of experiment IDs to their enabled state diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d95c2f02346..7fa78f75455 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -962,6 +962,9 @@ importers: serialize-error: specifier: ^12.0.0 version: 12.0.0 + sharp: + specifier: ^0.34.5 + version: 0.34.5 shell-quote: specifier: ^1.8.2 version: 1.8.3 @@ -1074,6 +1077,9 @@ importers: '@types/semver-compare': specifier: ^1.0.3 version: 1.0.3 + '@types/sharp': + specifier: ^0.32.0 + version: 0.32.0 '@types/shell-quote': specifier: ^1.7.5 version: 1.7.5 @@ -2027,9 +2033,6 @@ packages: '@emnapi/core@1.4.3': resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==} - '@emnapi/runtime@1.4.3': - resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} - '@emnapi/runtime@1.8.1': resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} @@ -4585,6 +4588,10 @@ packages: '@types/semver-compare@1.0.3': resolution: {integrity: sha512-mVZkB2QjXmZhh+MrtwMlJ8BqUnmbiSkpd88uOWskfwB8yitBT0tBRAKt+41VRgZD9zr9Sc+Xs02qGgvzd1Rq/Q==} + '@types/sharp@0.32.0': + resolution: {integrity: sha512-OOi3kL+FZDnPhVzsfD37J88FNeZh6gQsGcLc95NbeURRGvmSjeXiDcyWzF2o3yh/gQAUn2uhh/e+CPCa5nwAxw==} + deprecated: This is a stub types definition. sharp provides its own type definitions, so you do not need this installed. + '@types/shell-quote@1.7.5': resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==} @@ -5130,6 +5137,7 @@ packages: basic-ftp@5.0.5: resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} engines: {node: '>=10.0.0'} + deprecated: Security vulnerability fixed in 5.2.0, please upgrade better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} @@ -5957,10 +5965,6 @@ packages: resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} engines: {node: '>=8'} - detect-libc@2.0.4: - resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} - engines: {node: '>=8'} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -8976,6 +8980,7 @@ packages: prebuild-install@7.1.3: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true prelude-ls@1.2.1: @@ -12149,11 +12154,6 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/runtime@1.4.3': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.8.1': dependencies: tslib: 2.8.1 @@ -12391,8 +12391,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@img/colour@1.0.0': - optional: true + '@img/colour@1.0.0': {} '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: @@ -12733,14 +12732,14 @@ snapshots: '@napi-rs/wasm-runtime@0.2.10': dependencies: '@emnapi/core': 1.4.3 - '@emnapi/runtime': 1.4.3 + '@emnapi/runtime': 1.8.1 '@tybys/wasm-util': 0.9.0 optional: true '@napi-rs/wasm-runtime@0.2.11': dependencies: '@emnapi/core': 1.4.3 - '@emnapi/runtime': 1.4.3 + '@emnapi/runtime': 1.8.1 '@tybys/wasm-util': 0.9.0 optional: true @@ -14319,7 +14318,7 @@ snapshots: '@tailwindcss/oxide@4.1.6': dependencies: - detect-libc: 2.0.4 + detect-libc: 2.1.2 tar: 7.4.3 optionalDependencies: '@tailwindcss/oxide-android-arm64': 4.1.6 @@ -14337,7 +14336,7 @@ snapshots: '@tailwindcss/oxide@4.1.8': dependencies: - detect-libc: 2.0.4 + detect-libc: 2.1.2 tar: 7.4.3 optionalDependencies: '@tailwindcss/oxide-android-arm64': 4.1.8 @@ -14720,6 +14719,10 @@ snapshots: '@types/semver-compare@1.0.3': {} + '@types/sharp@0.32.0': + dependencies: + sharp: 0.34.5 + '@types/shell-quote@1.7.5': {} '@types/stack-utils@2.0.3': {} @@ -14974,7 +14977,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.50)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) '@vitest/utils@3.2.4': dependencies: @@ -16253,10 +16256,7 @@ snapshots: detect-libc@2.0.2: optional: true - detect-libc@2.0.4: {} - - detect-libc@2.1.2: - optional: true + detect-libc@2.1.2: {} detect-node-es@1.1.0: {} @@ -18333,7 +18333,7 @@ snapshots: lightningcss@1.29.2: dependencies: - detect-libc: 2.0.4 + detect-libc: 2.1.2 optionalDependencies: lightningcss-darwin-arm64: 1.29.2 lightningcss-darwin-x64: 1.29.2 @@ -18348,7 +18348,7 @@ snapshots: lightningcss@1.30.1: dependencies: - detect-libc: 2.0.4 + detect-libc: 2.1.2 optionalDependencies: lightningcss-darwin-arm64: 1.30.1 lightningcss-darwin-x64: 1.30.1 @@ -19761,7 +19761,7 @@ snapshots: prebuild-install@7.1.3: dependencies: - detect-libc: 2.0.4 + detect-libc: 2.1.2 expand-template: 2.0.3 github-from-package: 0.0.0 minimist: 1.2.8 @@ -20639,7 +20639,6 @@ snapshots: '@img/sharp-win32-arm64': 0.34.5 '@img/sharp-win32-ia32': 0.34.5 '@img/sharp-win32-x64': 0.34.5 - optional: true shebang-command@2.0.0: dependencies: diff --git a/src/core/mentions/__tests__/resolveImageMentions.spec.ts b/src/core/mentions/__tests__/resolveImageMentions.spec.ts index 747c778819f..4fe55af8817 100644 --- a/src/core/mentions/__tests__/resolveImageMentions.spec.ts +++ b/src/core/mentions/__tests__/resolveImageMentions.spec.ts @@ -41,7 +41,10 @@ describe("resolveImageMentions", () => { }) expect(mockValidateImage).toHaveBeenCalled() - expect(mockReadImageAsDataUrl).toHaveBeenCalledWith(path.resolve("/workspace", "assets/cat.png")) + expect(mockReadImageAsDataUrl).toHaveBeenCalledWith(path.resolve("/workspace", "assets/cat.png"), { + maxDimension: undefined, + quality: undefined, + }) expect(result.text).toBe("Please look at @/assets/cat.png") expect(result.images).toEqual([dataUrl]) }) diff --git a/src/core/mentions/resolveImageMentions.ts b/src/core/mentions/resolveImageMentions.ts index 0a0344348f1..4e808b01306 100644 --- a/src/core/mentions/resolveImageMentions.ts +++ b/src/core/mentions/resolveImageMentions.ts @@ -23,6 +23,10 @@ export interface ResolveImageMentionsOptions { maxImageFileSize?: number /** Maximum total size of all images in MB. Defaults to 20MB. */ maxTotalImageSize?: number + /** Maximum dimension (width or height) in pixels for downscaling. 0 = disabled. */ + maxImageDimension?: number + /** JPEG/WebP quality (1-100) for re-encoding resized images. */ + imageDownscaleQuality?: number } export interface ResolveImageMentionsResult { @@ -65,6 +69,8 @@ export async function resolveImageMentions({ supportsImages = true, maxImageFileSize = DEFAULT_MAX_IMAGE_FILE_SIZE_MB, maxTotalImageSize = DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB, + maxImageDimension, + imageDownscaleQuality, }: ResolveImageMentionsOptions): Promise { const existingImages = Array.isArray(images) ? images : [] if (existingImages.length >= MAX_IMAGES_PER_MESSAGE) { @@ -127,7 +133,10 @@ export async function resolveImageMentions({ continue } - const { dataUrl } = await readImageAsDataUrlWithBuffer(absPath) + const { dataUrl } = await readImageAsDataUrlWithBuffer(absPath, { + maxDimension: maxImageDimension, + quality: imageDownscaleQuality, + }) newImages.push(dataUrl) // Track memory usage diff --git a/src/core/tools/ReadFileTool.ts b/src/core/tools/ReadFileTool.ts index 8ad6a3b33d1..77b34f7ecbc 100644 --- a/src/core/tools/ReadFileTool.ts +++ b/src/core/tools/ReadFileTool.ts @@ -171,6 +171,8 @@ export class ReadFileTool extends BaseTool<"read_file"> { const { maxImageFileSize = DEFAULT_MAX_IMAGE_FILE_SIZE_MB, maxTotalImageSize = DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB, + maxImageDimension, + imageDownscaleQuality, } = state ?? {} for (const fileResult of fileResults) { @@ -207,6 +209,8 @@ export class ReadFileTool extends BaseTool<"read_file"> { maxTotalImageSize, imageMemoryTracker, updateFileResult, + maxImageDimension, + imageDownscaleQuality, ) continue } @@ -341,6 +345,8 @@ export class ReadFileTool extends BaseTool<"read_file"> { maxTotalImageSize: number, imageMemoryTracker: ImageMemoryTracker, updateFileResult: (path: string, updates: Partial) => void, + maxImageDimension?: number, + imageDownscaleQuality?: number, ): Promise { const fileExtension = path.extname(relPath).toLowerCase() const supportedBinaryFormats = getSupportedBinaryFormats() @@ -364,7 +370,10 @@ export class ReadFileTool extends BaseTool<"read_file"> { return } - const imageResult = await processImageFile(fullPath) + const imageResult = await processImageFile(fullPath, { + maxDimension: maxImageDimension, + quality: imageDownscaleQuality, + }) imageMemoryTracker.addMemoryUsage(imageResult.sizeInMB) await task.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource) @@ -744,6 +753,8 @@ export class ReadFileTool extends BaseTool<"read_file"> { const { maxImageFileSize = DEFAULT_MAX_IMAGE_FILE_SIZE_MB, maxTotalImageSize = DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB, + maxImageDimension, + imageDownscaleQuality, } = state ?? {} const validation = await validateImageForProcessing( fullPath, @@ -756,7 +767,10 @@ export class ReadFileTool extends BaseTool<"read_file"> { results.push(`File: ${relPath}\nNotice: ${validation.notice ?? "Image validation failed"}`) continue } - const imageResult = await processImageFile(fullPath) + const imageResult = await processImageFile(fullPath, { + maxDimension: maxImageDimension, + quality: imageDownscaleQuality, + }) if (imageResult) { results.push(`File: ${relPath}\n[Image file - content processed for vision model]`) } diff --git a/src/core/tools/helpers/imageHelpers.ts b/src/core/tools/helpers/imageHelpers.ts index a1adb078e63..f1f1aae0cfe 100644 --- a/src/core/tools/helpers/imageHelpers.ts +++ b/src/core/tools/helpers/imageHelpers.ts @@ -2,6 +2,7 @@ import path from "path" import * as fs from "fs/promises" import { t } from "../../../i18n" import prettyBytes from "pretty-bytes" +import { maybeResizeImage, DEFAULT_IMAGE_DOWNSCALE_QUALITY } from "../../../integrations/misc/resize-image" /** * Default maximum allowed image file size in bytes (5MB) @@ -69,15 +70,37 @@ export interface ImageProcessingResult { notice: string } +export interface ReadImageOptions { + /** Maximum dimension (width or height) in pixels for downscaling. 0 = disabled. */ + maxDimension?: number + /** JPEG/WebP quality (1-100) for re-encoding resized images. */ + quality?: number +} + /** - * Reads an image file and returns both the data URL and buffer + * Reads an image file and returns both the data URL and buffer. + * Optionally downscales the image if maxDimension is set. */ -export async function readImageAsDataUrlWithBuffer(filePath: string): Promise<{ dataUrl: string; buffer: Buffer }> { - const fileBuffer = await fs.readFile(filePath) - const base64 = fileBuffer.toString("base64") +export async function readImageAsDataUrlWithBuffer( + filePath: string, + options?: ReadImageOptions, +): Promise<{ dataUrl: string; buffer: Buffer }> { + let fileBuffer = await fs.readFile(filePath) const ext = path.extname(filePath).toLowerCase() - const mimeType = IMAGE_MIME_TYPES[ext] || "image/png" + + // Downscale if configured + if (options?.maxDimension && options.maxDimension > 0) { + const resizeResult = await maybeResizeImage({ + buffer: fileBuffer, + mimeType, + maxDimension: options.maxDimension, + quality: options.quality ?? DEFAULT_IMAGE_DOWNSCALE_QUALITY, + }) + fileBuffer = resizeResult.buffer + } + + const base64 = fileBuffer.toString("base64") const dataUrl = `data:${mimeType};base64,${base64}` return { dataUrl, buffer: fileBuffer } @@ -145,13 +168,14 @@ export async function validateImageForProcessing( } /** - * Processes an image file and returns the result + * Processes an image file and returns the result. + * Optionally downscales the image if resize options are provided. */ -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) +export async function processImageFile(fullPath: string, options?: ReadImageOptions): Promise { + const { dataUrl, buffer } = await readImageAsDataUrlWithBuffer(fullPath, options) + // Use actual buffer length (which reflects resized size) for accurate tracking + const imageSizeInKB = Math.round(buffer.length / 1024) + const imageSizeInMB = buffer.length / (1024 * 1024) const noticeText = t("tools:readFile.imageWithSize", { size: imageSizeInKB }) return { diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 7bd969e52d0..ae488f12be5 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2182,6 +2182,8 @@ export class ClineProvider language, maxImageFileSize, maxTotalImageSize, + maxImageDimension, + imageDownscaleQuality, historyPreviewCollapsed, reasoningBlockCollapsed, enterBehavior, @@ -2307,6 +2309,8 @@ export class ClineProvider renderContext: this.renderContext, maxImageFileSize: maxImageFileSize ?? 5, maxTotalImageSize: maxTotalImageSize ?? 20, + maxImageDimension: maxImageDimension ?? 0, + imageDownscaleQuality: imageDownscaleQuality ?? 85, settingsImportedAt: this.settingsImportedAt, historyPreviewCollapsed: historyPreviewCollapsed ?? false, reasoningBlockCollapsed: reasoningBlockCollapsed ?? true, @@ -2531,6 +2535,8 @@ export class ClineProvider enableSubfolderRules: stateValues.enableSubfolderRules ?? false, maxImageFileSize: stateValues.maxImageFileSize ?? 5, maxTotalImageSize: stateValues.maxTotalImageSize ?? 20, + maxImageDimension: stateValues.maxImageDimension ?? 0, + imageDownscaleQuality: stateValues.imageDownscaleQuality ?? 85, historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false, reasoningBlockCollapsed: stateValues.reasoningBlockCollapsed ?? true, enterBehavior: stateValues.enterBehavior ?? "send", diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index cfa4b0317f8..8015d77ec0e 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -539,6 +539,8 @@ describe("ClineProvider", () => { renderContext: "sidebar", maxImageFileSize: 5, maxTotalImageSize: 20, + maxImageDimension: 0, + imageDownscaleQuality: 85, cloudUserInfo: null, organizationAllowList: ORGANIZATION_ALLOW_ALL, autoCondenseContext: true, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index d27fd6bec09..b06c209c1ac 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -185,6 +185,8 @@ export const webviewMessageHandler = async ( rooIgnoreController: currentTask?.rooIgnoreController, maxImageFileSize: state.maxImageFileSize, maxTotalImageSize: state.maxTotalImageSize, + maxImageDimension: state.maxImageDimension, + imageDownscaleQuality: state.imageDownscaleQuality, }) return resolved } @@ -766,8 +768,12 @@ export const webviewMessageHandler = async ( await updateGlobalState("lastShownAnnouncementId", provider.latestAnnouncementId) await provider.postStateToWebview() break - case "selectImages": - const images = await selectImages() + case "selectImages": { + const selectState = await provider.getState() + const images = await selectImages({ + maxDimension: selectState.maxImageDimension, + quality: selectState.imageDownscaleQuality, + }) await provider.postMessageToWebview({ type: "selectedImages", images, @@ -775,6 +781,7 @@ export const webviewMessageHandler = async ( messageTs: message.messageTs, }) break + } case "exportCurrentTask": const currentTaskId = provider.getCurrentTask()?.taskId if (currentTaskId) { diff --git a/src/integrations/misc/__tests__/resize-image.spec.ts b/src/integrations/misc/__tests__/resize-image.spec.ts new file mode 100644 index 00000000000..8e4a7df8d5b --- /dev/null +++ b/src/integrations/misc/__tests__/resize-image.spec.ts @@ -0,0 +1,162 @@ +import { maybeResizeImage, DEFAULT_IMAGE_DOWNSCALE_QUALITY } from "../resize-image" +import sharp from "sharp" + +describe("maybeResizeImage", () => { + /** + * Helper to create a test image buffer with specified dimensions. + */ + async function createTestImage( + width: number, + height: number, + format: "png" | "jpeg" | "webp" = "png", + ): Promise { + const channels = 3 + const rawData = Buffer.alloc(width * height * channels, 128) + let pipeline = sharp(rawData, { raw: { width, height, channels } }) + + if (format === "png") { + pipeline = pipeline.png() + } else if (format === "jpeg") { + pipeline = pipeline.jpeg() + } else if (format === "webp") { + pipeline = pipeline.webp() + } + + return pipeline.toBuffer() + } + + it("should return original buffer when maxDimension is 0", async () => { + const buffer = await createTestImage(100, 100) + const result = await maybeResizeImage({ + buffer, + mimeType: "image/png", + maxDimension: 0, + }) + expect(result.wasResized).toBe(false) + expect(result.buffer).toBe(buffer) // same reference + }) + + it("should return original buffer when maxDimension is undefined", async () => { + const buffer = await createTestImage(100, 100) + const result = await maybeResizeImage({ + buffer, + mimeType: "image/png", + }) + expect(result.wasResized).toBe(false) + expect(result.buffer).toBe(buffer) + }) + + it("should not resize when image is smaller than maxDimension", async () => { + const buffer = await createTestImage(200, 100) + const result = await maybeResizeImage({ + buffer, + mimeType: "image/png", + maxDimension: 300, + }) + expect(result.wasResized).toBe(false) + expect(result.buffer).toBe(buffer) + }) + + it("should downscale a wide image exceeding maxDimension", async () => { + const buffer = await createTestImage(2000, 1000, "png") + const result = await maybeResizeImage({ + buffer, + mimeType: "image/png", + maxDimension: 500, + }) + expect(result.wasResized).toBe(true) + + const metadata = await sharp(result.buffer).metadata() + expect(metadata.width).toBeLessThanOrEqual(500) + expect(metadata.height).toBeLessThanOrEqual(500) + // Check aspect ratio is roughly preserved (2:1) + expect(metadata.width! / metadata.height!).toBeCloseTo(2, 0) + }) + + it("should downscale a tall image exceeding maxDimension", async () => { + const buffer = await createTestImage(500, 2000, "jpeg") + const result = await maybeResizeImage({ + buffer, + mimeType: "image/jpeg", + maxDimension: 1000, + }) + expect(result.wasResized).toBe(true) + + const metadata = await sharp(result.buffer).metadata() + expect(metadata.width).toBeLessThanOrEqual(1000) + expect(metadata.height).toBeLessThanOrEqual(1000) + // Check aspect ratio is roughly preserved (1:4) + expect(metadata.height! / metadata.width!).toBeCloseTo(4, 0) + }) + + it("should handle webp format", async () => { + const buffer = await createTestImage(1500, 1500, "webp") + const result = await maybeResizeImage({ + buffer, + mimeType: "image/webp", + maxDimension: 800, + }) + expect(result.wasResized).toBe(true) + + const metadata = await sharp(result.buffer).metadata() + expect(metadata.format).toBe("webp") + expect(metadata.width).toBeLessThanOrEqual(800) + expect(metadata.height).toBeLessThanOrEqual(800) + }) + + it("should return original buffer for unsupported mime types", async () => { + const buffer = await createTestImage(2000, 2000) + const result = await maybeResizeImage({ + buffer, + mimeType: "image/gif", + maxDimension: 500, + }) + expect(result.wasResized).toBe(false) + expect(result.buffer).toBe(buffer) + }) + + it("should use default quality when not specified", async () => { + expect(DEFAULT_IMAGE_DOWNSCALE_QUALITY).toBe(85) + }) + + it("should accept custom quality setting for jpeg without error", async () => { + const buffer = await createTestImage(2000, 2000, "jpeg") + + const resultHighQ = await maybeResizeImage({ + buffer, + mimeType: "image/jpeg", + maxDimension: 500, + quality: 95, + }) + + const resultLowQ = await maybeResizeImage({ + buffer, + mimeType: "image/jpeg", + maxDimension: 500, + quality: 10, + }) + + // Both should be resized successfully + expect(resultHighQ.wasResized).toBe(true) + expect(resultLowQ.wasResized).toBe(true) + + // Both should produce valid image buffers + const metaHigh = await sharp(resultHighQ.buffer).metadata() + const metaLow = await sharp(resultLowQ.buffer).metadata() + expect(metaHigh.format).toBe("jpeg") + expect(metaLow.format).toBe("jpeg") + expect(metaHigh.width).toBeLessThanOrEqual(500) + expect(metaLow.width).toBeLessThanOrEqual(500) + }) + + it("should not upscale small images", async () => { + const buffer = await createTestImage(100, 50, "png") + const result = await maybeResizeImage({ + buffer, + mimeType: "image/png", + maxDimension: 500, + }) + expect(result.wasResized).toBe(false) + expect(result.buffer).toBe(buffer) + }) +}) diff --git a/src/integrations/misc/process-images.ts b/src/integrations/misc/process-images.ts index cf3e201538d..d10fa975299 100644 --- a/src/integrations/misc/process-images.ts +++ b/src/integrations/misc/process-images.ts @@ -1,9 +1,17 @@ import * as vscode from "vscode" import fs from "fs/promises" import * as path from "path" +import { maybeResizeImage, DEFAULT_IMAGE_DOWNSCALE_QUALITY } from "./resize-image" -export async function selectImages(): Promise { - const options: vscode.OpenDialogOptions = { +export interface SelectImagesOptions { + /** Maximum dimension (width or height) in pixels for downscaling. 0 = disabled. */ + maxDimension?: number + /** JPEG/WebP quality (1-100) for re-encoding resized images. */ + quality?: number +} + +export async function selectImages(options?: SelectImagesOptions): Promise { + const dialogOptions: vscode.OpenDialogOptions = { canSelectMany: true, openLabel: "Select", filters: { @@ -11,7 +19,7 @@ export async function selectImages(): Promise { }, } - const fileUris = await vscode.window.showOpenDialog(options) + const fileUris = await vscode.window.showOpenDialog(dialogOptions) if (!fileUris || fileUris.length === 0) { return [] @@ -20,9 +28,21 @@ export async function selectImages(): Promise { return await Promise.all( fileUris.map(async (uri) => { const imagePath = uri.fsPath - const buffer = await fs.readFile(imagePath) - const base64 = buffer.toString("base64") + let buffer = await fs.readFile(imagePath) const mimeType = getMimeType(imagePath) + + // Downscale if configured + if (options?.maxDimension && options.maxDimension > 0) { + const resizeResult = await maybeResizeImage({ + buffer, + mimeType, + maxDimension: options.maxDimension, + quality: options.quality ?? DEFAULT_IMAGE_DOWNSCALE_QUALITY, + }) + buffer = resizeResult.buffer + } + + const base64 = buffer.toString("base64") const dataUrl = `data:${mimeType};base64,${base64}` return dataUrl }), diff --git a/src/integrations/misc/resize-image.ts b/src/integrations/misc/resize-image.ts new file mode 100644 index 00000000000..ded2e623ea7 --- /dev/null +++ b/src/integrations/misc/resize-image.ts @@ -0,0 +1,99 @@ +import sharp from "sharp" + +/** + * Default image downscale quality (JPEG/WebP) when re-encoding resized images. + */ +export const DEFAULT_IMAGE_DOWNSCALE_QUALITY = 85 + +/** + * Mime type to sharp output format mapping. + */ +const MIME_TO_FORMAT: Record = { + "image/png": "png", + "image/jpeg": "jpeg", + "image/webp": "webp", +} + +export interface ResizeImageOptions { + /** The image buffer to potentially resize. */ + buffer: Buffer + /** The MIME type of the image (e.g. "image/png"). */ + mimeType: string + /** Maximum dimension (width or height) in pixels. 0 or undefined means no resizing. */ + maxDimension?: number + /** JPEG/WebP quality (1-100) for re-encoding. Defaults to 85. */ + quality?: number +} + +export interface ResizeImageResult { + /** The (possibly resized) image buffer. */ + buffer: Buffer + /** Whether the image was actually resized. */ + wasResized: boolean +} + +/** + * Conditionally downscales an image buffer if either dimension exceeds `maxDimension`. + * Preserves aspect ratio. Returns the original buffer unchanged if no resizing is needed + * or if the format is unsupported for resizing. + */ +export async function maybeResizeImage({ + buffer, + mimeType, + maxDimension, + quality = DEFAULT_IMAGE_DOWNSCALE_QUALITY, +}: ResizeImageOptions): Promise { + // If downscaling is disabled or dimension is 0/undefined, return as-is + if (!maxDimension || maxDimension <= 0) { + return { buffer, wasResized: false } + } + + // Only resize formats we can handle + const format = MIME_TO_FORMAT[mimeType] + if (!format) { + return { buffer, wasResized: false } + } + + const image = sharp(buffer) + const metadata = await image.metadata() + + if (!metadata.width || !metadata.height) { + return { buffer, wasResized: false } + } + + // Only downscale -- never upscale + if (metadata.width <= maxDimension && metadata.height <= maxDimension) { + return { buffer, wasResized: false } + } + + // Calculate new dimensions preserving aspect ratio + const aspectRatio = metadata.width / metadata.height + let newWidth: number + let newHeight: number + + if (metadata.width >= metadata.height) { + newWidth = maxDimension + newHeight = Math.round(maxDimension / aspectRatio) + } else { + newHeight = maxDimension + newWidth = Math.round(maxDimension * aspectRatio) + } + + // Perform the resize + let pipeline = image.resize(newWidth, newHeight, { + fit: "inside", + withoutEnlargement: true, + }) + + // Re-encode in the original format with quality setting where applicable + if (format === "jpeg") { + pipeline = pipeline.jpeg({ quality }) + } else if (format === "webp") { + pipeline = pipeline.webp({ quality }) + } else if (format === "png") { + pipeline = pipeline.png() + } + + const resizedBuffer = await pipeline.toBuffer() + return { buffer: resizedBuffer, wasResized: true } +} diff --git a/src/package.json b/src/package.json index 7c4889abd89..44de7f4e93e 100644 --- a/src/package.json +++ b/src/package.json @@ -519,6 +519,7 @@ "say": "^0.16.0", "semver-compare": "^1.0.0", "serialize-error": "^12.0.0", + "sharp": "^0.34.5", "shell-quote": "^1.8.2", "simple-git": "^3.27.0", "sound-play": "^1.1.0", @@ -558,6 +559,7 @@ "@types/proper-lockfile": "^4.1.4", "@types/ps-tree": "^1.1.6", "@types/semver-compare": "^1.0.3", + "@types/sharp": "^0.32.0", "@types/shell-quote": "^1.7.5", "@types/stream-json": "^1.7.8", "@types/string-similarity": "^4.0.2", diff --git a/webview-ui/src/components/settings/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx index 8663ea6e038..23d23764dd7 100644 --- a/webview-ui/src/components/settings/ContextManagementSettings.tsx +++ b/webview-ui/src/components/settings/ContextManagementSettings.tsx @@ -35,6 +35,8 @@ type ContextManagementSettingsProps = HTMLAttributes & { enableSubfolderRules?: boolean maxImageFileSize?: number maxTotalImageSize?: number + maxImageDimension?: number + imageDownscaleQuality?: number profileThresholds?: Record includeDiagnosticMessages?: boolean maxDiagnosticMessages?: number @@ -53,6 +55,8 @@ type ContextManagementSettingsProps = HTMLAttributes & { | "enableSubfolderRules" | "maxImageFileSize" | "maxTotalImageSize" + | "maxImageDimension" + | "imageDownscaleQuality" | "profileThresholds" | "includeDiagnosticMessages" | "maxDiagnosticMessages" @@ -74,6 +78,8 @@ export const ContextManagementSettings = ({ setCachedStateField, maxImageFileSize, maxTotalImageSize, + maxImageDimension, + imageDownscaleQuality, profileThresholds = {}, includeDiagnosticMessages, maxDiagnosticMessages, @@ -308,6 +314,66 @@ export const ContextManagementSettings = ({ + +
+ {t("settings:contextManagement.maxImageDimension.label")} +
+ { + const newValue = parseInt(e.target.value, 10) + if (!isNaN(newValue) && newValue >= 0 && newValue <= 8192) { + setCachedStateField("maxImageDimension", newValue) + } + }} + onClick={(e) => e.currentTarget.select()} + data-testid="max-image-dimension-input" + /> + + {(maxImageDimension ?? 0) === 0 + ? t("settings:contextManagement.maxImageDimension.disabled") + : t("settings:contextManagement.maxImageDimension.px")} + +
+
+
+ {t("settings:contextManagement.maxImageDimension.description")} +
+
+ + {(maxImageDimension ?? 0) > 0 && ( + + + {t("settings:contextManagement.imageDownscaleQuality.label")} + +
+ setCachedStateField("imageDownscaleQuality", value)} + /> + {imageDownscaleQuality ?? 85} +
+
+ {t("settings:contextManagement.imageDownscaleQuality.description")} +
+
+ )} + (({ onDone, t enableSubfolderRules, maxImageFileSize, maxTotalImageSize, + maxImageDimension, + imageDownscaleQuality, customSupportPrompts, profileThresholds, alwaysAllowFollowupQuestions, @@ -404,6 +406,8 @@ const SettingsView = forwardRef(({ onDone, t enableSubfolderRules: enableSubfolderRules ?? false, maxImageFileSize: maxImageFileSize ?? 5, maxTotalImageSize: maxTotalImageSize ?? 20, + maxImageDimension: maxImageDimension ?? 0, + imageDownscaleQuality: imageDownscaleQuality ?? 85, includeDiagnosticMessages: includeDiagnosticMessages !== undefined ? includeDiagnosticMessages : true, maxDiagnosticMessages: maxDiagnosticMessages ?? 50, @@ -837,6 +841,8 @@ const SettingsView = forwardRef(({ onDone, t enableSubfolderRules={enableSubfolderRules} maxImageFileSize={maxImageFileSize} maxTotalImageSize={maxTotalImageSize} + maxImageDimension={maxImageDimension} + imageDownscaleQuality={imageDownscaleQuality} profileThresholds={profileThresholds} includeDiagnosticMessages={includeDiagnosticMessages} maxDiagnosticMessages={maxDiagnosticMessages} diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 2c83cabbbcb..7d515620171 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -657,6 +657,16 @@ "mb": "MB", "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." }, + "maxImageDimension": { + "label": "Dimensió màxima de reducció d'imatge", + "px": "px", + "description": "Amplada o alçada màxima (en píxels) per a les imatges abans d'enviar-les al LLM. Les imatges que superin aquesta dimensió es redueixen proporcionalment, preservant la relació d'aspecte. Estableix a 0 per desactivar la reducció.", + "disabled": "Desactivat" + }, + "imageDownscaleQuality": { + "label": "Qualitat de reducció d'imatge", + "description": "Qualitat de codificació JPEG/WebP (1-100) utilitzada quan es recodifiquen imatges reduïdes. Valors més alts preserven més detall però produeixen arxius més grans. Només s'aplica quan la reducció d'imatge està activada." + }, "includeCurrentTime": { "label": "Inclou l'hora actual en el context", "description": "Quan està activat, l'hora actual i la informació del fus horari s'inclouran a la indicació del sistema. Desactiveu-ho si els models deixen de funcionar per problemes amb l'hora." diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index c31d29147d4..bef37289bf5 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -657,6 +657,16 @@ "mb": "MB", "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." }, + "maxImageDimension": { + "label": "Maximale Bildverkleinerungsdimension", + "px": "px", + "description": "Maximale Breite oder Höhe (in Pixeln) für Bilder vor dem Senden an das LLM. Bilder, die diese Dimension überschreiten, werden proportional verkleinert, wobei das Seitenverhältnis erhalten bleibt. Auf 0 setzen, um die Verkleinerung zu deaktivieren.", + "disabled": "Deaktiviert" + }, + "imageDownscaleQuality": { + "label": "Bildverkleinerungsqualität", + "description": "JPEG/WebP-Kodierungsqualität (1-100), die bei der Neukodierung verkleinerter Bilder verwendet wird. Höhere Werte bewahren mehr Details, erzeugen aber größere Dateien. Gilt nur, wenn die Bildverkleinerung aktiviert ist." + }, "includeCurrentTime": { "label": "Aktuelle Uhrzeit in den Kontext einbeziehen", "description": "Wenn aktiviert, werden die aktuelle Uhrzeit und Zeitzoneninformationen in den System-Prompt aufgenommen. Deaktiviere diese Option, wenn Modelle aufgrund von Zeitbedenken die Arbeit einstellen." diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 3b2497aaee7..6430a196eb7 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -695,6 +695,16 @@ "mb": "MB", "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." }, + "maxImageDimension": { + "label": "Image downscale max dimension", + "px": "px", + "description": "Maximum width or height (in pixels) for images before sending to the LLM. Images exceeding this dimension are proportionally downscaled, preserving aspect ratio. Set to 0 to disable downscaling.", + "disabled": "Disabled" + }, + "imageDownscaleQuality": { + "label": "Image downscale quality", + "description": "JPEG/WebP encoding quality (1-100) used when re-encoding downscaled images. Higher values preserve more detail but produce larger files. Only applies when image downscaling is enabled." + }, "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 6595c4f9079..dceba0e5021 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -632,6 +632,16 @@ "mb": "MB", "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." }, + "maxImageDimension": { + "label": "Dimensión máxima de reducción de imagen", + "px": "px", + "description": "Ancho o alto máximo (en píxeles) para imágenes antes de enviarlas al LLM. Las imágenes que excedan esta dimensión se reducen proporcionalmente, preservando la relación de aspecto. Establece en 0 para desactivar la reducción.", + "disabled": "Desactivado" + }, + "imageDownscaleQuality": { + "label": "Calidad de reducción de imagen", + "description": "Calidad de codificación JPEG/WebP (1-100) utilizada al recodificar imágenes reducidas. Valores más altos preservan más detalle pero producen archivos más grandes. Solo aplica cuando la reducción de imagen está activada." + }, "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 56337bda14c..8765b2bcc26 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -632,6 +632,16 @@ "mb": "MB", "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." }, + "maxImageDimension": { + "label": "Dimension maximale de réduction d'image", + "px": "px", + "description": "Largeur ou hauteur maximale (en pixels) pour les images avant envoi au LLM. Les images dépassant cette dimension sont réduites proportionnellement, en préservant le rapport d'aspect. Mettre à 0 pour désactiver la réduction.", + "disabled": "Désactivé" + }, + "imageDownscaleQuality": { + "label": "Qualité de réduction d'image", + "description": "Qualité d'encodage JPEG/WebP (1-100) utilisée lors du réencodage des images réduites. Des valeurs plus élevées préservent plus de détails mais produisent des fichiers plus volumineux. S'applique uniquement lorsque la réduction d'image est activée." + }, "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 abd334bec09..970746245c2 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -657,6 +657,16 @@ "mb": "MB", "description": "एकल read_file ऑपरेशन में संसाधित सभी छवियों के लिए अधिकतम संचयी आकार सीमा (MB में)। कई छवियों को पढ़ते समय, प्रत्येक छवि का आकार कुल में जोड़ा जाता है। यदि किसी अन्य छवि को शामिल करने से यह सीमा पार हो जाएगी, तो उसे छोड़ दिया जाएगा।" }, + "maxImageDimension": { + "label": "इमेज डाउनस्केल अधिकतम आयाम", + "px": "px", + "description": "LLM को भेजने से पहले इमेज की अधिकतम चौड़ाई या ऊँचाई (पिक्सेल में)। इस आयाम से अधिक इमेज को आनुपातिक रूप से कम किया जाता है, पक्षानुपात को बनाए रखते हुए। डाउनस्केलिंग अक्षम करने के लिए 0 सेट करें।", + "disabled": "अक्षम" + }, + "imageDownscaleQuality": { + "label": "इमेज डाउनस्केल गुणवत्ता", + "description": "डाउनस्केल की गई इमेज को री-एनकोड करते समय उपयोग की जाने वाली JPEG/WebP एनकोडिंग गुणवत्ता (1-100)। उच्च मान अधिक विवरण संरक्षित करते हैं लेकिन बड़ी फ़ाइलें बनाते हैं। केवल तब लागू होता है जब इमेज डाउनस्केलिंग सक्षम हो।" + }, "includeCurrentTime": { "label": "संदर्भ में वर्तमान समय शामिल करें", "description": "सक्षम होने पर, वर्तमान समय और समयक्षेत्र की जानकारी सिस्टम प्रॉम्प्ट में शामिल की जाएगी। यदि मॉडल समय संबंधी चिंताओं के कारण काम करना बंद कर देते हैं तो इसे अक्षम करें।" diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 1ebcf2073b6..20166b50a84 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -657,6 +657,16 @@ "mb": "MB", "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." }, + "maxImageDimension": { + "label": "Dimensi maksimum perkecilan gambar", + "px": "px", + "description": "Lebar atau tinggi maksimum (dalam piksel) untuk gambar sebelum dikirim ke LLM. Gambar yang melebihi dimensi ini akan diperkecil secara proporsional, mempertahankan rasio aspek. Atur ke 0 untuk menonaktifkan perkecilan.", + "disabled": "Dinonaktifkan" + }, + "imageDownscaleQuality": { + "label": "Kualitas perkecilan gambar", + "description": "Kualitas encoding JPEG/WebP (1-100) yang digunakan saat mengenkode ulang gambar yang diperkecil. Nilai lebih tinggi mempertahankan lebih banyak detail tetapi menghasilkan file lebih besar. Hanya berlaku ketika perkecilan gambar diaktifkan." + }, "includeCurrentTime": { "label": "Sertakan waktu saat ini dalam konteks", "description": "Ketika diaktifkan, waktu saat ini dan informasi zona waktu akan disertakan dalam prompt sistem. Nonaktifkan ini jika model berhenti bekerja karena masalah waktu." diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 4a0c7161654..14a707e2d21 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -657,6 +657,16 @@ "mb": "MB", "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." }, + "maxImageDimension": { + "label": "Dimensione massima riduzione immagine", + "px": "px", + "description": "Larghezza o altezza massima (in pixel) per le immagini prima dell'invio al LLM. Le immagini che superano questa dimensione vengono ridotte proporzionalmente, preservando le proporzioni. Imposta a 0 per disabilitare la riduzione.", + "disabled": "Disabilitato" + }, + "imageDownscaleQuality": { + "label": "Qualità riduzione immagine", + "description": "Qualità di codifica JPEG/WebP (1-100) utilizzata durante la ricodifica delle immagini ridotte. Valori più alti preservano più dettagli ma producono file più grandi. Si applica solo quando la riduzione delle immagini è abilitata." + }, "includeCurrentTime": { "label": "Includi l'ora corrente nel contesto", "description": "Se abilitato, l'ora corrente e le informazioni sul fuso orario verranno incluse nel prompt di sistema. Disabilita questa opzione se i modelli smettono di funzionare a causa di problemi di orario." diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index b0d921571af..1819c12605a 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -657,6 +657,16 @@ "mb": "MB", "description": "単一のread_file操作で処理されるすべての画像の累積サイズ制限(MB単位)。複数の画像を読み取る際、各画像のサイズが合計に加算されます。別の画像を含めるとこの制限を超える場合、その画像はスキップされます。" }, + "maxImageDimension": { + "label": "画像ダウンスケール最大サイズ", + "px": "px", + "description": "LLMに送信する前の画像の最大幅または高さ(ピクセル単位)。このサイズを超える画像はアスペクト比を維持しながら比例的に縮小されます。0に設定するとダウンスケールが無効になります。", + "disabled": "無効" + }, + "imageDownscaleQuality": { + "label": "画像ダウンスケール品質", + "description": "ダウンスケールされた画像を再エンコードする際に使用されるJPEG/WebPエンコード品質(1-100)。高い値はより多くの詳細を保持しますが、ファイルサイズが大きくなります。画像ダウンスケールが有効な場合のみ適用されます。" + }, "includeCurrentTime": { "label": "現在の時刻をコンテキストに含める", "description": "有効にすると、現在の時刻とタイムゾーン情報がシステムプロンプトに含まれます。モデルが時間に関する懸念で動作を停止する場合は無効にしてください。" diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 88fc8e6d79e..63719982bc7 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -657,6 +657,16 @@ "mb": "MB", "description": "단일 read_file 작업에서 처리되는 모든 이미지의 최대 누적 크기 제한(MB 단위)입니다. 여러 이미지를 읽을 때 각 이미지의 크기가 총계에 추가됩니다. 다른 이미지를 포함하면 이 제한을 초과하는 경우 해당 이미지는 건너뜁니다." }, + "maxImageDimension": { + "label": "이미지 다운스케일 최대 크기", + "px": "px", + "description": "LLM에 전송하기 전 이미지의 최대 너비 또는 높이(픽셀). 이 크기를 초과하는 이미지는 종횡비를 유지하면서 비례적으로 축소됩니다. 0으로 설정하면 다운스케일이 비활성화됩니다.", + "disabled": "비활성화" + }, + "imageDownscaleQuality": { + "label": "이미지 다운스케일 품질", + "description": "다운스케일된 이미지를 재인코딩할 때 사용되는 JPEG/WebP 인코딩 품질(1-100). 높은 값은 더 많은 세부 사항을 보존하지만 더 큰 파일을 생성합니다. 이미지 다운스케일이 활성화된 경우에만 적용됩니다." + }, "includeCurrentTime": { "label": "컨텍스트에 현재 시간 포함", "description": "활성화하면 현재 시간과 시간대 정보가 시스템 프롬프트에 포함됩니다. 시간 문제로 모델이 작동을 멈추면 비활성화하세요." diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index fcfad37d376..1c5cc9d5507 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -632,6 +632,16 @@ "mb": "MB", "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." }, + "maxImageDimension": { + "label": "Maximale afbeeldingsverkleiningsdimensie", + "px": "px", + "description": "Maximale breedte of hoogte (in pixels) voor afbeeldingen voordat ze naar het LLM worden gestuurd. Afbeeldingen die deze dimensie overschrijden worden proportioneel verkleind, met behoud van de beeldverhouding. Stel in op 0 om verkleining uit te schakelen.", + "disabled": "Uitgeschakeld" + }, + "imageDownscaleQuality": { + "label": "Afbeeldingsverkleiningskwaliteit", + "description": "JPEG/WebP-coderingskwaliteit (1-100) die wordt gebruikt bij het hercoderen van verkleinde afbeeldingen. Hogere waarden behouden meer detail maar produceren grotere bestanden. Geldt alleen wanneer afbeeldingsverkleining is ingeschakeld." + }, "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 fa48bc6b212..debf858c8bf 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -657,6 +657,16 @@ "mb": "MB", "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." }, + "maxImageDimension": { + "label": "Maksymalny wymiar zmniejszania obrazu", + "px": "px", + "description": "Maksymalna szerokość lub wysokość (w pikselach) dla obrazów przed wysłaniem do LLM. Obrazy przekraczające ten wymiar są proporcjonalnie zmniejszane z zachowaniem proporcji. Ustaw na 0, aby wyłączyć zmniejszanie.", + "disabled": "Wyłączony" + }, + "imageDownscaleQuality": { + "label": "Jakość zmniejszania obrazu", + "description": "Jakość kodowania JPEG/WebP (1-100) używana podczas ponownego kodowania zmniejszonych obrazów. Wyższe wartości zachowują więcej szczegółów, ale tworzą większe pliki. Dotyczy tylko gdy zmniejszanie obrazu jest włączone." + }, "includeCurrentTime": { "label": "Uwzględnij bieżący czas w kontekście", "description": "Gdy włączone, bieżący czas i informacje o strefie czasowej zostaną uwzględnione w promptcie systemowym. Wyłącz, jeśli modele przestają działać z powodu problemów z czasem." diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index a8387e05121..89e868dc886 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -657,6 +657,16 @@ "mb": "MB", "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." }, + "maxImageDimension": { + "label": "Dimensão máxima de redução de imagem", + "px": "px", + "description": "Largura ou altura máxima (em pixels) para imagens antes de enviar ao LLM. Imagens que excedem essa dimensão são reduzidas proporcionalmente, preservando a proporção. Defina como 0 para desativar a redução.", + "disabled": "Desativado" + }, + "imageDownscaleQuality": { + "label": "Qualidade de redução de imagem", + "description": "Qualidade de codificação JPEG/WebP (1-100) usada ao recodificar imagens reduzidas. Valores mais altos preservam mais detalhes, mas produzem arquivos maiores. Aplica-se apenas quando a redução de imagem está ativada." + }, "includeCurrentTime": { "label": "Incluir hora atual no contexto", "description": "Quando ativado, a hora atual e as informações de fuso horário serão incluídas no prompt do sistema. Desative se os modelos pararem de funcionar por problemas de tempo." diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index fe24ebee299..665460d8228 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -657,6 +657,16 @@ "mb": "МБ", "description": "Максимальный совокупный лимит размера (в МБ) для всех изображений, обрабатываемых в одной операции read_file. При чтении нескольких изображений размер каждого изображения добавляется к общему. Если включение другого изображения превысит этот лимит, оно будет пропущено." }, + "maxImageDimension": { + "label": "Максимальный размер уменьшения изображения", + "px": "px", + "description": "Максимальная ширина или высота (в пикселях) для изображений перед отправкой в LLM. Изображения, превышающие этот размер, пропорционально уменьшаются с сохранением соотношения сторон. Установите 0 для отключения уменьшения.", + "disabled": "Отключено" + }, + "imageDownscaleQuality": { + "label": "Качество уменьшения изображения", + "description": "Качество кодирования JPEG/WebP (1-100), используемое при перекодировании уменьшенных изображений. Более высокие значения сохраняют больше деталей, но создают файлы большего размера. Применяется только при включённом уменьшении изображений." + }, "includeCurrentTime": { "label": "Включить текущее время в контекст", "description": "Если включено, текущее время и информация о часовом поясе будут включены в системную подсказку. Отключите, если модели прекращают работу из-за проблем со временем." diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 7171718f1c5..3c28f04fb00 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -657,6 +657,16 @@ "mb": "MB", "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." }, + "maxImageDimension": { + "label": "Görüntü küçültme maksimum boyutu", + "px": "px", + "description": "LLM'ye göndermeden önce görüntüler için maksimum genişlik veya yükseklik (piksel cinsinden). Bu boyutu aşan görüntüler en-boy oranı korunarak orantılı olarak küçültülür. Küçültmeyi devre dışı bırakmak için 0 olarak ayarlayın.", + "disabled": "Devre dışı" + }, + "imageDownscaleQuality": { + "label": "Görüntü küçültme kalitesi", + "description": "Küçültülmüş görüntüleri yeniden kodlarken kullanılan JPEG/WebP kodlama kalitesi (1-100). Daha yüksek değerler daha fazla ayrıntı korur ancak daha büyük dosyalar üretir. Yalnızca görüntü küçültme etkinleştirildiğinde geçerlidir." + }, "includeCurrentTime": { "label": "Mevcut zamanı bağlama dahil et", "description": "Etkinleştirildiğinde, mevcut zaman ve saat dilimi bilgileri sistem istemine dahil edilecektir. Modeller zaman endişeleri nedeniyle çalışmayı durdurursa bunu devre dışı bırakın." diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 95b4f2d6863..3d69ffff473 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -657,6 +657,16 @@ "mb": "MB", "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." }, + "maxImageDimension": { + "label": "Kích thước tối đa thu nhỏ ảnh", + "px": "px", + "description": "Chiều rộng hoặc chiều cao tối đa (tính bằng pixel) cho ảnh trước khi gửi đến LLM. Ảnh vượt quá kích thước này được thu nhỏ tỷ lệ, giữ nguyên tỷ lệ khung hình. Đặt thành 0 để tắt thu nhỏ.", + "disabled": "Đã tắt" + }, + "imageDownscaleQuality": { + "label": "Chất lượng thu nhỏ ảnh", + "description": "Chất lượng mã hóa JPEG/WebP (1-100) được sử dụng khi mã hóa lại ảnh đã thu nhỏ. Giá trị cao hơn giữ nhiều chi tiết hơn nhưng tạo ra file lớn hơn. Chỉ áp dụng khi tính năng thu nhỏ ảnh được bật." + }, "includeCurrentTime": { "label": "Bao gồm thời gian hiện tại trong ngữ cảnh", "description": "Khi được bật, thời gian hiện tại và thông tin múi giờ sẽ được bao gồm trong lời nhắc hệ thống. Tắt nếu các mô hình ngừng hoạt động do lo ngại về thời gian." diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index eeba6bb079d..d244cda205b 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -657,6 +657,16 @@ "mb": "MB", "description": "单次 read_file 操作中处理的所有图片的最大累计大小限制(MB)。读取多张图片时,每张图片的大小会累加到总大小中。如果包含另一张图片会超过此限制,则会跳过该图片。" }, + "maxImageDimension": { + "label": "图片缩放最大尺寸", + "px": "px", + "description": "发送给 LLM 之前图片的最大宽度或高度(像素)。超过此尺寸的图片会按比例缩小,保持纵横比。设为 0 禁用缩放。", + "disabled": "已禁用" + }, + "imageDownscaleQuality": { + "label": "图片缩放质量", + "description": "重新编码缩小后的图片时使用的 JPEG/WebP 编码质量(1-100)。较高的值保留更多细节,但会产生更大的文件。仅在启用图片缩放时适用。" + }, "includeCurrentTime": { "label": "在上下文中包含当前时间", "description": "启用后,当前时间和时区信息将包含在系统提示中。如果模型因时间问题停止工作,请禁用此选项。" diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 9f4241c3dd9..6da91eb8917 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -642,6 +642,16 @@ "mb": "MB", "description": "單次 read_file 操作中處理的所有圖片的最大累計大小限制(MB)。讀取多張圖片時,每張圖片的大小會累加到總大小中。如果包含另一張圖片會超過此限制,則會跳過該圖片。" }, + "maxImageDimension": { + "label": "圖片縮放最大尺寸", + "px": "px", + "description": "傳送給 LLM 之前圖片的最大寬度或高度(像素)。超過此尺寸的圖片會按比例縮小,保持長寬比。設為 0 停用縮放。", + "disabled": "已停用" + }, + "imageDownscaleQuality": { + "label": "圖片縮放品質", + "description": "重新編碼縮小後的圖片時使用的 JPEG/WebP 編碼品質(1-100)。較高的值保留更多細節,但會產生更大的檔案。僅在啟用圖片縮放時適用。" + }, "diagnostics": { "includeMessages": { "label": "自動在上下文中包含診斷",