diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 3290adfa2d..53e3b3890f 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -137,19 +137,24 @@ export const webviewMessageHandler = async ( /** * Handles message editing operations with user confirmation */ - const handleEditOperation = async (messageTs: number, editedContent: string): Promise => { + const handleEditOperation = async (messageTs: number, editedContent: string, images?: string[]): Promise => { // Send message to webview to show edit confirmation dialog await provider.postMessageToWebview({ type: "showEditMessageDialog", messageTs, text: editedContent, + images, }) } /** * Handles confirmed message editing from webview dialog */ - const handleEditMessageConfirm = async (messageTs: number, editedContent: string): Promise => { + const handleEditMessageConfirm = async ( + messageTs: number, + editedContent: string, + images?: string[], + ): Promise => { // Only proceed if we have a current cline if (provider.getCurrentCline()) { const currentCline = provider.getCurrentCline()! @@ -168,6 +173,7 @@ export const webviewMessageHandler = async ( type: "askResponse", askResponse: "messageResponse", text: editedContent, + images, }) // Don't initialize with history item for edit operations @@ -193,11 +199,12 @@ export const webviewMessageHandler = async ( messageTs: number, operation: "delete" | "edit", editedContent?: string, + images?: string[], ): Promise => { if (operation === "delete") { await handleDeleteOperation(messageTs) } else if (operation === "edit" && editedContent) { - await handleEditOperation(messageTs, editedContent) + await handleEditOperation(messageTs, editedContent, images) } } @@ -367,7 +374,12 @@ export const webviewMessageHandler = async ( break case "selectImages": const images = await selectImages() - await provider.postMessageToWebview({ type: "selectedImages", images }) + await provider.postMessageToWebview({ + type: "selectedImages", + images, + context: message.context, + messageTs: message.messageTs, + }) break case "exportCurrentTask": const currentTaskId = provider.getCurrentCline()?.taskId @@ -1144,7 +1156,12 @@ export const webviewMessageHandler = async ( message.value && message.editedMessageContent ) { - await handleMessageModificationsOperation(message.value, "edit", message.editedMessageContent) + await handleMessageModificationsOperation( + message.value, + "edit", + message.editedMessageContent, + message.images, + ) } break } @@ -1484,7 +1501,7 @@ export const webviewMessageHandler = async ( break case "editMessageConfirm": if (message.messageTs && message.text) { - await handleEditMessageConfirm(message.messageTs, message.text) + await handleEditMessageConfirm(message.messageTs, message.text, message.images) } break case "getListApiConfiguration": diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 41e2b2ab83..6991762f09 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -160,6 +160,7 @@ export interface ExtensionMessage { rulesFolderPath?: string settings?: any messageTs?: number + context?: string } export type ExtensionState = Pick< diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index b0139f82de..3b3f2abdc7 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -199,6 +199,7 @@ export interface WebviewMessage { editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account" disabled?: boolean + context?: string dataUri?: string askResponse?: ClineAskResponse apiConfiguration?: ProviderSettings diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index d33237a8b3..3e149f28ed 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -79,10 +79,12 @@ const App = () => { isOpen: boolean messageTs: number text: string + images?: string[] }>({ isOpen: false, messageTs: 0, text: "", + images: [], }) const settingsRef = useRef(null) @@ -145,7 +147,12 @@ const App = () => { } if (message.type === "showEditMessageDialog" && message.messageTs && message.text) { - setEditMessageDialogState({ isOpen: true, messageTs: message.messageTs, text: message.text }) + setEditMessageDialogState({ + isOpen: true, + messageTs: message.messageTs, + text: message.text, + images: message.images || [], + }) } if (message.type === "acceptInput") { @@ -253,6 +260,7 @@ const App = () => { type: "editMessageConfirm", messageTs: editMessageDialogState.messageTs, text: editMessageDialogState.text, + images: editMessageDialogState.images, }) setEditMessageDialogState((prev) => ({ ...prev, isOpen: false })) }} diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index d467424ec9..31fb762faa 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -1,4 +1,5 @@ import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from "react" +import { appendImages } from "@src/utils/imageUtils" import { McpExecution } from "./McpExecution" import { useSize } from "react-use" import { useTranslation, Trans } from "react-i18next" @@ -22,6 +23,7 @@ import { getLanguageFromPath } from "@src/utils/getLanguageFromPath" import { Button } from "@src/components/ui" import ChatTextArea from "./ChatTextArea" +import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock" import UpdateTodoListToolBlock from "./UpdateTodoListToolBlock" @@ -122,6 +124,19 @@ export const ChatRowContent = ({ const [editImages, setEditImages] = useState([]) const { copyWithFeedback } = useCopyToClipboard() + // Handle message events for image selection during edit mode + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const msg = event.data + if (msg.type === "selectedImages" && msg.context === "edit" && msg.messageTs === message.ts && isEditing) { + setEditImages((prevImages) => appendImages(prevImages, msg.images, MAX_IMAGES_PER_MESSAGE)) + } + } + + window.addEventListener("message", handleMessage) + return () => window.removeEventListener("message", handleMessage) + }, [isEditing, message.ts]) + // Memoized callback to prevent re-renders caused by inline arrow functions const handleToggleExpand = useCallback(() => { onToggleExpand(message.ts) @@ -153,13 +168,14 @@ export const ChatRowContent = ({ type: "submitEditedMessage", value: message.ts, editedMessageContent: editedContent, + images: editImages, }) - }, [message.ts, editedContent]) + }, [message.ts, editedContent, editImages]) // Handle image selection for editing const handleSelectImages = useCallback(() => { - vscode.postMessage({ type: "selectImages" }) - }, []) + vscode.postMessage({ type: "selectImages", context: "edit", messageTs: message.ts }) + }, [message.ts]) const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => { if (message.text !== null && message.text !== undefined && message.say === "api_req_started") { @@ -1073,7 +1089,7 @@ export const ChatRowContent = ({