Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
53 changes: 26 additions & 27 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion src/core/mentions/__tests__/resolveImageMentions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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])
})
Expand Down
11 changes: 10 additions & 1 deletion src/core/mentions/resolveImageMentions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<ResolveImageMentionsResult> {
const existingImages = Array.isArray(images) ? images : []
if (existingImages.length >= MAX_IMAGES_PER_MESSAGE) {
Expand Down Expand Up @@ -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
Expand Down
18 changes: 16 additions & 2 deletions src/core/tools/ReadFileTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -207,6 +209,8 @@ export class ReadFileTool extends BaseTool<"read_file"> {
maxTotalImageSize,
imageMemoryTracker,
updateFileResult,
maxImageDimension,
imageDownscaleQuality,
)
continue
}
Expand Down Expand Up @@ -341,6 +345,8 @@ export class ReadFileTool extends BaseTool<"read_file"> {
maxTotalImageSize: number,
imageMemoryTracker: ImageMemoryTracker,
updateFileResult: (path: string, updates: Partial<FileResult>) => void,
maxImageDimension?: number,
imageDownscaleQuality?: number,
): Promise<void> {
const fileExtension = path.extname(relPath).toLowerCase()
const supportedBinaryFormats = getSupportedBinaryFormats()
Expand All @@ -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)

Expand Down Expand Up @@ -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,
Expand All @@ -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]`)
}
Expand Down
46 changes: 35 additions & 11 deletions src/core/tools/helpers/imageHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -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<ImageProcessingResult> {
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<ImageProcessingResult> {
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 {
Expand Down
Loading
Loading