Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions packages/types/src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export const clineSays = [
"api_req_retry_delayed",
"api_req_deleted",
"text",
"image",
"reasoning",
"completion_result",
"user_feedback",
Expand Down
13 changes: 9 additions & 4 deletions src/core/tools/generateImageTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { fileExistsAtPath } from "../../utils/fs"
import { getReadablePath } from "../../utils/path"
import { isPathOutsideWorkspace } from "../../utils/pathUtils"
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this import still needed? It appears to be unused after the refactoring:

import { safeWriteJson } from "../../utils/safeWriteJson"
import { OpenRouterHandler } from "../../api/providers/openrouter"

// Hardcoded list of image generation models for now
Expand Down Expand Up @@ -239,12 +238,18 @@ export async function generateImageTool(

cline.didEditFile = true

// Display the generated image in the chat using a text message with the image
await cline.say("text", getReadablePath(cline.cwd, finalPath), [result.imageData])

// Record successful tool usage
cline.recordToolUsage("generate_image")

// Get the webview URI for the image
const provider = cline.providerRef.deref()
const fullImagePath = path.join(cline.cwd, finalPath)

// Convert to webview URI if provider is available
const imageUri = provider?.convertToWebviewUri?.(fullImagePath) ?? vscode.Uri.file(fullImagePath).toString()

// Send the image with the webview URI
await cline.say("image", JSON.stringify({ imageUri, imagePath: fullImagePath }))
pushToolResult(formatResponse.toolResult(getReadablePath(cline.cwd, finalPath)))

return
Expand Down
36 changes: 35 additions & 1 deletion src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -658,9 +658,17 @@ export class ClineProvider
setTtsSpeed(ttsSpeed ?? 1)
})

// Set up webview options with proper resource roots
const resourceRoots = [this.contextProxy.extensionUri]

// Add workspace folders to allow access to workspace files
if (vscode.workspace.workspaceFolders) {
resourceRoots.push(...vscode.workspace.workspaceFolders.map((folder) => folder.uri))
}

webviewView.webview.options = {
enableScripts: true,
localResourceRoots: [this.contextProxy.extensionUri],
localResourceRoots: resourceRoots,
}

webviewView.webview.html =
Expand Down Expand Up @@ -2399,6 +2407,32 @@ export class ClineProvider
})
}
}

/**
* Convert a file path to a webview-accessible URI
* This method safely converts file paths to URIs that can be loaded in the webview
*
* @param filePath - The absolute file path to convert
* @returns The webview URI string, or the original file URI if conversion fails
*/
public convertToWebviewUri(filePath: string): string {
try {
const fileUri = vscode.Uri.file(filePath)

// Check if we have a webview available
if (this.view?.webview) {
const webviewUri = this.view.webview.asWebviewUri(fileUri)
return webviewUri.toString()
}

// Fallback to file URI if no webview available
return fileUri.toString()
} catch (error) {
console.error("Failed to convert to webview URI:", error)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good implementation! Consider adding more specific error types here to help with debugging. For example, you could differentiate between 'no webview available' vs 'URI conversion failed' errors.

// Return file URI as fallback
return vscode.Uri.file(filePath).toString()
}
}
}

class OrganizationAllowListViolationError extends Error {
Expand Down
42 changes: 40 additions & 2 deletions src/integrations/misc/image-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,46 @@ import * as vscode from "vscode"
import { getWorkspacePath } from "../../utils/path"
import { t } from "../../i18n"

export async function openImage(dataUri: string, options?: { values?: { action?: string } }) {
const matches = dataUri.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/)
export async function openImage(dataUriOrPath: string, options?: { values?: { action?: string } }) {
// Check if it's a file path (absolute or relative)
const isFilePath =
!dataUriOrPath.startsWith("data:") &&
!dataUriOrPath.startsWith("http:") &&
!dataUriOrPath.startsWith("https:") &&
!dataUriOrPath.startsWith("vscode-resource:") &&
!dataUriOrPath.startsWith("file+.vscode-resource")

if (isFilePath) {
// Handle file path - open directly in VSCode
try {
// Resolve the path relative to workspace if needed
let filePath = dataUriOrPath
if (!path.isAbsolute(filePath)) {
const workspacePath = getWorkspacePath()
if (workspacePath) {
filePath = path.join(workspacePath, filePath)
}
}

const fileUri = vscode.Uri.file(filePath)

// Check if this is a copy action
if (options?.values?.action === "copy") {
await vscode.env.clipboard.writeText(filePath)
vscode.window.showInformationMessage(t("common:info.path_copied_to_clipboard"))
return
}

// Open the image file directly
await vscode.commands.executeCommand("vscode.open", fileUri)
} catch (error) {
vscode.window.showErrorMessage(t("common:errors.error_opening_image", { error }))
}
return
}

// Handle data URI (existing logic)
const matches = dataUriOrPath.match(/^data:image\/([a-zA-Z]+);base64,(.+)$/)
if (!matches) {
vscode.window.showErrorMessage(t("common:errors.invalid_data_uri"))
return
Expand Down
8 changes: 8 additions & 0 deletions webview-ui/src/components/chat/ChatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1159,6 +1159,14 @@ export const ChatRowContent = ({
return <CodebaseSearchResultsDisplay results={results} />
case "user_edit_todos":
return <UpdateTodoListToolBlock userEdited onChange={() => {}} />
case "image":
// Parse the JSON to get imageUri and imagePath
const imageInfo = JSON.parse(message.text || "{}")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using a safe JSON parsing utility (e.g. safeJsonParse) instead of directly calling JSON.parse on message.text to avoid runtime exceptions if the JSON is malformed.

Suggested change
const imageInfo = JSON.parse(message.text || "{}")
const imageInfo = safeJsonParse<any>(message.text || "{}")

This comment was generated because it violated a code review rule: irule_PTI8rjtnhwrWq6jS.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a proper type definition for the parsed image info instead of using a generic object:

return (
<div style={{ marginTop: "10px" }}>
<ImageBlock imageUri={imageInfo.imageUri} imagePath={imageInfo.imagePath} />
</div>
)
default:
return (
<>
Expand Down
36 changes: 32 additions & 4 deletions webview-ui/src/components/common/ImageBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,42 @@ import React from "react"
import { ImageViewer } from "./ImageViewer"

interface ImageBlockProps {
imageData: string
path?: string
// For new image generation tool format (preferred)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding JSDoc comments to explain the distinction between imageUri and imagePath props. This would help future developers understand when to use each:

imageUri?: string // The webview-accessible URI for rendering
imagePath?: string // The actual file path for display and opening

// For backward compatibility with Mermaid diagrams and old format
imageData?: string // Base64 data or regular URL (legacy)
path?: string // Optional path for Mermaid diagrams (legacy)
}

export default function ImageBlock({ imageData, path }: ImageBlockProps) {
export default function ImageBlock({ imageUri, imagePath, imageData, path }: ImageBlockProps) {
// Determine which props to use based on what's provided
let finalImageUri: string
let finalImagePath: string | undefined

if (imageUri) {
// New format: explicit imageUri and imagePath
finalImageUri = imageUri
finalImagePath = imagePath
} else if (imageData) {
// Legacy format: use imageData as direct URI (for Mermaid diagrams)
finalImageUri = imageData
finalImagePath = path
} else {
// No valid image data provided
console.error("ImageBlock: No valid image data provided")
return null
}

return (
<div className="my-2">
<ImageViewer imageData={imageData} path={path} alt="AI Generated Image" showControls={true} />
<ImageViewer
imageUri={finalImageUri}
imagePath={finalImagePath}
alt="AI Generated Image"
showControls={true}
/>
</div>
)
}
Loading