diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 23ec50af37..be6f21eb30 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -61,6 +61,7 @@ interface ChatRowProps { onFollowUpUnmount?: () => void isFollowUpAnswered?: boolean editable?: boolean + onBeginEdit?: (message: ClineMessage) => void } // eslint-disable-next-line @typescript-eslint/no-empty-object-type @@ -113,6 +114,7 @@ export const ChatRowContent = ({ onBatchFileResponse, isFollowUpAnswered, editable, + onBeginEdit, }: ChatRowContentProps) => { const { t } = useTranslation() @@ -146,13 +148,15 @@ export const ChatRowContent = ({ // Handle edit button click const handleEditClick = useCallback(() => { + if (onBeginEdit) { + onBeginEdit(message) + return + } setIsEditing(true) setEditedContent(message.text || "") setEditImages(message.images || []) setEditMode(mode || "code") - // Edit mode is now handled entirely in the frontend - // No need to notify the backend - }, [message.text, message.images, mode]) + }, [onBeginEdit, message, mode]) // Handle cancel edit const handleCancelEdit = useCallback(() => { diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index e0528da24c..afc41701e9 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -50,6 +50,7 @@ import Announcement from "./Announcement" import BrowserSessionRow from "./BrowserSessionRow" import ChatRow from "./ChatRow" import { ChatTextArea } from "./ChatTextArea" +import { Mention } from "./Mention" import TaskHeader from "./TaskHeader" import AutoApproveMenu from "./AutoApproveMenu" import SystemPromptWarning from "./SystemPromptWarning" @@ -177,6 +178,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction(null) const [sendingDisabled, setSendingDisabled] = useState(false) const [selectedImages, setSelectedImages] = useState([]) + const [editingOverlay, setEditingOverlay] = useState<{ ts: number; text: string; images: string[] } | null>(null) // we need to hold on to the ask because useEffect > lastMessage will always let us know when an ask comes in and handle it, but by the time handleMessage is called, the last message might not be the ask anymore (it could be a say that followed) const [clineAsk, setClineAsk] = useState(undefined) @@ -773,7 +775,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction vscode.postMessage({ type: "selectImages" }), []) + const selectImages = useCallback(() => { + if (editingOverlay) { + vscode.postMessage({ type: "selectImages", context: "edit", messageTs: editingOverlay.ts }) + } else { + vscode.postMessage({ type: "selectImages" }) + } + }, [editingOverlay]) const shouldDisableImages = !model?.supportsImages || selectedImages.length >= MAX_IMAGES_PER_MESSAGE @@ -795,9 +803,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction + appendImages(prevImages, message.images, MAX_IMAGES_PER_MESSAGE), + ) + } + } else { setSelectedImages((prevImages: string[]) => appendImages(prevImages, message.images, MAX_IMAGES_PER_MESSAGE), ) @@ -841,6 +855,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + setEditingOverlay({ + ts: message.ts, + text: message.text || "", + images: message.images || [], + }) + setInputValue(message.text || "") + setSelectedImages(message.images || []) + // Focus input when beginning edit + setTimeout(() => textAreaRef.current?.focus(), 0) + }, []) + + const handleCancelEditOverlay = useCallback(() => { + setEditingOverlay(null) + setInputValue("") + setSelectedImages([]) + setTimeout(() => textAreaRef.current?.focus(), 0) + }, []) + + const handleSubmitEdited = useCallback(() => { + if (!editingOverlay) return + vscode.postMessage({ + type: "submitEditedMessage", + value: editingOverlay.ts, + editedMessageContent: inputValue, + images: selectedImages, + }) + setEditingOverlay(null) + setInputValue("") + setSelectedImages([]) + }, [editingOverlay, inputValue, selectedImages]) + // NOTE: the VSCode window needs to be focused for this to work. useMount(() => textAreaRef.current?.focus()) @@ -1560,6 +1608,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction ) }, @@ -1577,6 +1626,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + // Handle Escape key to exit editing mode + if (event.key === "Escape" && editingOverlay) { + event.preventDefault() + handleCancelEditOverlay() + return + } + // Check for Command/Ctrl + Period (with or without Shift) // Using event.key to respect keyboard layouts (e.g., Dvorak) if ((event.metaKey || event.ctrlKey) && event.key === ".") { @@ -1741,7 +1798,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { @@ -1892,7 +1949,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction -
+
-
- -
- {areButtonsVisible && ( -
- {showScrollToBottom ? ( - - { - scrollToBottomSmooth() - disableAutoScrollRef.current = false - }}> - - - - ) : ( - <> - {primaryButtonText && !isStreaming && ( - - handlePrimaryButtonClick(inputValue, selectedImages)}> - {primaryButtonText} - - - )} - {(secondaryButtonText || isStreaming) && ( - - handleSecondaryButtonClick(inputValue, selectedImages)}> - {isStreaming ? t("chat:cancel.title") : secondaryButtonText} - - - )} - - )} + +
+ {editingOverlay && ( +
+
+
+ Editing message +
+
+ +
+
+
+ )} +
+
- )} + {areButtonsVisible && ( +
+ {showScrollToBottom ? ( + + { + scrollToBottomSmooth() + disableAutoScrollRef.current = false + }}> + + + + ) : ( + <> + {primaryButtonText && !isStreaming && ( + + + handlePrimaryButtonClick(inputValue, selectedImages) + }> + {primaryButtonText} + + + )} + {(secondaryButtonText || isStreaming) && ( + + + handleSecondaryButtonClick(inputValue, selectedImages) + }> + {isStreaming ? t("chat:cancel.title") : secondaryButtonText} + + + )} + + )} +
+ )} +
)} - { - if (messageQueue[index]) { - vscode.postMessage({ type: "removeQueuedMessage", text: messageQueue[index].id }) - } - }} - onUpdate={(index, newText) => { - if (messageQueue[index]) { - vscode.postMessage({ - type: "editQueuedMessage", - payload: { id: messageQueue[index].id, text: newText, images: messageQueue[index].images }, - }) - } - }} - /> - handleSendMessage(inputValue, selectedImages)} - onSelectImages={selectImages} - shouldDisableImages={shouldDisableImages} - onHeightChange={() => { - if (isAtBottom) { - scrollToBottomAuto() - } - }} - mode={mode} - setMode={setMode} - modeShortcutText={modeShortcutText} - /> +
+ { + if (messageQueue[index]) { + vscode.postMessage({ type: "removeQueuedMessage", text: messageQueue[index].id }) + } + }} + onUpdate={(index, newText) => { + if (messageQueue[index]) { + vscode.postMessage({ + type: "editQueuedMessage", + payload: { + id: messageQueue[index].id, + text: newText, + images: messageQueue[index].images, + }, + }) + } + }} + /> + handleSendMessage(inputValue, selectedImages)} + onSelectImages={selectImages} + shouldDisableImages={shouldDisableImages} + onHeightChange={() => { + if (isAtBottom) { + scrollToBottomAuto() + } + }} + mode={mode} + setMode={setMode} + modeShortcutText={modeShortcutText} + isEditMode={!!editingOverlay} + onCancel={handleCancelEditOverlay} + /> - {isProfileDisabled && ( -
- -
- )} + {isProfileDisabled && ( +
+ +
+ )} +