Skip to content

Commit fad219e

Browse files
authored
feat: optimize memory usage for image handling in webview (#7556)
* feat: optimize memory usage for image handling in webview - Replace base64 image data with webview URIs to reduce memory footprint - Add proper resource roots to webview for workspace file access - Implement convertToWebviewUri method for safe file-to-URI conversion - Update ImageViewer to handle both webview URIs and file paths separately - Add image message type for proper image rendering in chat - Improve error handling and display for failed image loads - Add comprehensive tests for ImageViewer component - Format display paths as relative for better readability This change significantly reduces memory usage by avoiding base64 encoding of images and instead using VSCode's webview URI system for direct file access. Images are now loaded on-demand from disk rather than being held in memory as base64 strings. * fix: address PR review comments - Use safeJsonParse instead of JSON.parse in ChatRow.tsx - Add type definition for parsed image info - Add more specific error types in ClineProvider.ts - Add comprehensive JSDoc comments to ImageBlock.tsx - Improve error handling and type safety * fix: address MrUbens' review comments - Remove hardcoded 'rc1' pattern in formatDisplayPath, use generic workspace detection - Internationalize 'No image data' text using i18n system * chore: remove useless comment * chore(i18n): add image.noData to all locales to fix translation check * test: update ImageViewer.spec to align with i18n key and flexible path formatting
1 parent 47f594f commit fad219e

File tree

26 files changed

+417
-58
lines changed

26 files changed

+417
-58
lines changed

packages/types/src/message.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ export const clineSays = [
146146
"api_req_retry_delayed",
147147
"api_req_deleted",
148148
"text",
149+
"image",
149150
"reasoning",
150151
"completion_result",
151152
"user_feedback",

src/core/tools/generateImageTool.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { fileExistsAtPath } from "../../utils/fs"
88
import { getReadablePath } from "../../utils/path"
99
import { isPathOutsideWorkspace } from "../../utils/pathUtils"
1010
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
11-
import { safeWriteJson } from "../../utils/safeWriteJson"
1211
import { OpenRouterHandler } from "../../api/providers/openrouter"
1312

1413
// Hardcoded list of image generation models for now
@@ -237,12 +236,18 @@ export async function generateImageTool(
237236

238237
cline.didEditFile = true
239238

240-
// Display the generated image in the chat using a text message with the image
241-
await cline.say("text", getReadablePath(cline.cwd, finalPath), [result.imageData])
242-
243239
// Record successful tool usage
244240
cline.recordToolUsage("generate_image")
245241

242+
// Get the webview URI for the image
243+
const provider = cline.providerRef.deref()
244+
const fullImagePath = path.join(cline.cwd, finalPath)
245+
246+
// Convert to webview URI if provider is available
247+
const imageUri = provider?.convertToWebviewUri?.(fullImagePath) ?? vscode.Uri.file(fullImagePath).toString()
248+
249+
// Send the image with the webview URI
250+
await cline.say("image", JSON.stringify({ imageUri, imagePath: fullImagePath }))
246251
pushToolResult(formatResponse.toolResult(getReadablePath(cline.cwd, finalPath)))
247252

248253
return

src/core/webview/ClineProvider.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -600,9 +600,17 @@ export class ClineProvider
600600
setTtsSpeed(ttsSpeed ?? 1)
601601
})
602602

603+
// Set up webview options with proper resource roots
604+
const resourceRoots = [this.contextProxy.extensionUri]
605+
606+
// Add workspace folders to allow access to workspace files
607+
if (vscode.workspace.workspaceFolders) {
608+
resourceRoots.push(...vscode.workspace.workspaceFolders.map((folder) => folder.uri))
609+
}
610+
603611
webviewView.webview.options = {
604612
enableScripts: true,
605-
localResourceRoots: [this.contextProxy.extensionUri],
613+
localResourceRoots: resourceRoots,
606614
}
607615

608616
webviewView.webview.html =
@@ -2466,4 +2474,40 @@ export class ClineProvider
24662474
public get cwd() {
24672475
return getWorkspacePath()
24682476
}
2477+
2478+
/**
2479+
* Convert a file path to a webview-accessible URI
2480+
* This method safely converts file paths to URIs that can be loaded in the webview
2481+
*
2482+
* @param filePath - The absolute file path to convert
2483+
* @returns The webview URI string, or the original file URI if conversion fails
2484+
* @throws {Error} When webview is not available
2485+
* @throws {TypeError} When file path is invalid
2486+
*/
2487+
public convertToWebviewUri(filePath: string): string {
2488+
try {
2489+
const fileUri = vscode.Uri.file(filePath)
2490+
2491+
// Check if we have a webview available
2492+
if (this.view?.webview) {
2493+
const webviewUri = this.view.webview.asWebviewUri(fileUri)
2494+
return webviewUri.toString()
2495+
}
2496+
2497+
// Specific error for no webview available
2498+
const error = new Error("No webview available for URI conversion")
2499+
console.error(error.message)
2500+
// Fallback to file URI if no webview available
2501+
return fileUri.toString()
2502+
} catch (error) {
2503+
// More specific error handling
2504+
if (error instanceof TypeError) {
2505+
console.error("Invalid file path provided for URI conversion:", error)
2506+
} else {
2507+
console.error("Failed to convert to webview URI:", error)
2508+
}
2509+
// Return file URI as fallback
2510+
return vscode.Uri.file(filePath).toString()
2511+
}
2512+
}
24692513
}

src/integrations/misc/image-handler.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,46 @@ import * as vscode from "vscode"
44
import { getWorkspacePath } from "../../utils/path"
55
import { t } from "../../i18n"
66

7-
export async function openImage(dataUri: string, options?: { values?: { action?: string } }) {
8-
const matches = dataUri.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/)
7+
export async function openImage(dataUriOrPath: string, options?: { values?: { action?: string } }) {
8+
// Check if it's a file path (absolute or relative)
9+
const isFilePath =
10+
!dataUriOrPath.startsWith("data:") &&
11+
!dataUriOrPath.startsWith("http:") &&
12+
!dataUriOrPath.startsWith("https:") &&
13+
!dataUriOrPath.startsWith("vscode-resource:") &&
14+
!dataUriOrPath.startsWith("file+.vscode-resource")
15+
16+
if (isFilePath) {
17+
// Handle file path - open directly in VSCode
18+
try {
19+
// Resolve the path relative to workspace if needed
20+
let filePath = dataUriOrPath
21+
if (!path.isAbsolute(filePath)) {
22+
const workspacePath = getWorkspacePath()
23+
if (workspacePath) {
24+
filePath = path.join(workspacePath, filePath)
25+
}
26+
}
27+
28+
const fileUri = vscode.Uri.file(filePath)
29+
30+
// Check if this is a copy action
31+
if (options?.values?.action === "copy") {
32+
await vscode.env.clipboard.writeText(filePath)
33+
vscode.window.showInformationMessage(t("common:info.path_copied_to_clipboard"))
34+
return
35+
}
36+
37+
// Open the image file directly
38+
await vscode.commands.executeCommand("vscode.open", fileUri)
39+
} catch (error) {
40+
vscode.window.showErrorMessage(t("common:errors.error_opening_image", { error }))
41+
}
42+
return
43+
}
44+
45+
// Handle data URI (existing logic)
46+
const matches = dataUriOrPath.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/)
947
if (!matches) {
1048
vscode.window.showErrorMessage(t("common:errors.invalid_data_uri"))
1149
return

webview-ui/src/components/chat/ChatRow.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1159,6 +1159,17 @@ export const ChatRowContent = ({
11591159
return <CodebaseSearchResultsDisplay results={results} />
11601160
case "user_edit_todos":
11611161
return <UpdateTodoListToolBlock userEdited onChange={() => {}} />
1162+
case "image":
1163+
// Parse the JSON to get imageUri and imagePath
1164+
const imageInfo = safeJsonParse<{ imageUri: string; imagePath: string }>(message.text || "{}")
1165+
if (!imageInfo) {
1166+
return null
1167+
}
1168+
return (
1169+
<div style={{ marginTop: "10px" }}>
1170+
<ImageBlock imageUri={imageInfo.imageUri} imagePath={imageInfo.imagePath} />
1171+
</div>
1172+
)
11621173
default:
11631174
return (
11641175
<>
Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,66 @@
11
import React from "react"
22
import { ImageViewer } from "./ImageViewer"
33

4+
/**
5+
* Props for the ImageBlock component
6+
*/
47
interface ImageBlockProps {
5-
imageData: string
8+
/**
9+
* The webview-accessible URI for rendering the image.
10+
* This is the preferred format for new image generation tools.
11+
* Should be a URI that can be directly loaded in the webview context.
12+
*/
13+
imageUri?: string
14+
15+
/**
16+
* The actual file path for display purposes and file operations.
17+
* Used to show the path to the user and for opening the file in the editor.
18+
* This is typically an absolute or relative path to the image file.
19+
*/
20+
imagePath?: string
21+
22+
/**
23+
* Base64 data or regular URL for backward compatibility.
24+
* @deprecated Use imageUri instead for new implementations.
25+
* This is maintained for compatibility with Mermaid diagrams and legacy code.
26+
*/
27+
imageData?: string
28+
29+
/**
30+
* Optional path for Mermaid diagrams.
31+
* @deprecated Use imagePath instead for new implementations.
32+
* This is maintained for backward compatibility with existing Mermaid diagram rendering.
33+
*/
634
path?: string
735
}
836

9-
export default function ImageBlock({ imageData, path }: ImageBlockProps) {
37+
export default function ImageBlock({ imageUri, imagePath, imageData, path }: ImageBlockProps) {
38+
// Determine which props to use based on what's provided
39+
let finalImageUri: string
40+
let finalImagePath: string | undefined
41+
42+
if (imageUri) {
43+
// New format: explicit imageUri and imagePath
44+
finalImageUri = imageUri
45+
finalImagePath = imagePath
46+
} else if (imageData) {
47+
// Legacy format: use imageData as direct URI (for Mermaid diagrams)
48+
finalImageUri = imageData
49+
finalImagePath = path
50+
} else {
51+
// No valid image data provided
52+
console.error("ImageBlock: No valid image data provided")
53+
return null
54+
}
55+
1056
return (
1157
<div className="my-2">
12-
<ImageViewer imageData={imageData} path={path} alt="AI Generated Image" showControls={true} />
58+
<ImageViewer
59+
imageUri={finalImageUri}
60+
imagePath={finalImagePath}
61+
alt="AI Generated Image"
62+
showControls={true}
63+
/>
1364
</div>
1465
)
1566
}

0 commit comments

Comments
 (0)