diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx
index d43db0c794..60786945df 100644
--- a/webview-ui/src/components/chat/ChatRow.tsx
+++ b/webview-ui/src/components/chat/ChatRow.tsx
@@ -156,11 +156,6 @@ export const ChatRowContent = ({
})
}, [message.ts, editedContent])
- // Handle sending edited message (when Enter is pressed in ChatTextArea)
- const handleSendEdit = useCallback(() => {
- handleSaveEdit()
- }, [handleSaveEdit])
-
// Handle image selection for editing
const handleSelectImages = useCallback(() => {
vscode.postMessage({ type: "selectImages" })
@@ -1059,21 +1054,15 @@ export const ChatRowContent = ({
placeholderText={t("chat:editMessage.placeholder")}
selectedImages={editImages}
setSelectedImages={setEditImages}
- onSend={handleSendEdit}
+ onSend={handleSaveEdit}
onSelectImages={handleSelectImages}
shouldDisableImages={false}
mode={editMode}
setMode={setEditMode}
modeShortcutText=""
+ isEditMode={true}
+ onCancel={handleCancelEdit}
/>
-
-
-
-
) : (
diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx
index b522b43ecc..b12e9b1961 100644
--- a/webview-ui/src/components/chat/ChatTextArea.tsx
+++ b/webview-ui/src/components/chat/ChatTextArea.tsx
@@ -46,6 +46,9 @@ interface ChatTextAreaProps {
mode: Mode
setMode: (value: Mode) => void
modeShortcutText: string
+ // Edit mode props
+ isEditMode?: boolean
+ onCancel?: () => void
}
const ChatTextArea = forwardRef(
@@ -65,6 +68,8 @@ const ChatTextArea = forwardRef(
mode,
setMode,
modeShortcutText,
+ isEditMode = false,
+ onCancel,
},
ref,
) => {
@@ -972,15 +977,27 @@ const ChatTextArea = forwardRef(
/>
-
-
-
+ {isEditMode ? (
+
+
+
+ ) : (
+
+
+
+ )}
{!inputValue && (
(
+ {isEditMode && (
+
+ )}
{
expect(apiConfigDropdown).toHaveAttribute("disabled")
})
})
+ describe("edit mode functionality", () => {
+ it("should show edit button instead of send button when isEditMode is true", () => {
+ ;(useExtensionState as ReturnType).mockReturnValue({
+ filePaths: [],
+ openedTabs: [],
+ taskHistory: [],
+ cwd: "/test/workspace",
+ })
+
+ render()
+
+ // Should show edit button (codicon-edit)
+ const editButton = screen.getByRole("button", {
+ name: (_, element) => {
+ return element.querySelector(".codicon-edit") !== null
+ },
+ })
+ expect(editButton).toBeInTheDocument()
+
+ // Should not show send button (codicon-send)
+ const sendButton = screen.queryByRole("button", {
+ name: (_, element) => {
+ return element.querySelector(".codicon-send") !== null
+ },
+ })
+ expect(sendButton).not.toBeInTheDocument()
+ })
+
+ it("should show send button when isEditMode is false", () => {
+ ;(useExtensionState as ReturnType).mockReturnValue({
+ filePaths: [],
+ openedTabs: [],
+ taskHistory: [],
+ cwd: "/test/workspace",
+ })
+
+ render()
+
+ // Should show send button (codicon-send)
+ const sendButton = screen.getByRole("button", {
+ name: (_, element) => {
+ return element.querySelector(".codicon-send") !== null
+ },
+ })
+ expect(sendButton).toBeInTheDocument()
+
+ // Should not show edit button (codicon-edit)
+ const editButton = screen.queryByRole("button", {
+ name: (_, element) => {
+ return element.querySelector(".codicon-edit") !== null
+ },
+ })
+ expect(editButton).not.toBeInTheDocument()
+ })
+
+ it("should show cancel button in edit mode", () => {
+ ;(useExtensionState as ReturnType).mockReturnValue({
+ filePaths: [],
+ openedTabs: [],
+ taskHistory: [],
+ cwd: "/test/workspace",
+ })
+
+ render()
+
+ // Should show cancel button with text "Cancel"
+ const cancelButton = screen.getByRole("button", { name: /cancel/i })
+ expect(cancelButton).toBeInTheDocument()
+ expect(cancelButton).toHaveTextContent("Cancel")
+ })
+
+ it("should not show cancel button when not in edit mode", () => {
+ ;(useExtensionState as ReturnType).mockReturnValue({
+ filePaths: [],
+ openedTabs: [],
+ taskHistory: [],
+ cwd: "/test/workspace",
+ })
+
+ render()
+
+ // Should not show cancel button
+ const cancelButton = screen.queryByRole("button", { name: /cancel/i })
+ expect(cancelButton).not.toBeInTheDocument()
+ })
+
+ it("should call onSend when edit button is clicked", () => {
+ const onSend = vi.fn()
+ ;(useExtensionState as ReturnType).mockReturnValue({
+ filePaths: [],
+ openedTabs: [],
+ taskHistory: [],
+ cwd: "/test/workspace",
+ })
+
+ render()
+
+ const editButton = screen.getByRole("button", {
+ name: (_, element) => {
+ return element.querySelector(".codicon-edit") !== null
+ },
+ })
+
+ fireEvent.click(editButton)
+ expect(onSend).toHaveBeenCalledTimes(1)
+ })
+
+ it("should call onCancel when cancel button is clicked", () => {
+ const onCancel = vi.fn()
+ ;(useExtensionState as ReturnType).mockReturnValue({
+ filePaths: [],
+ openedTabs: [],
+ taskHistory: [],
+ cwd: "/test/workspace",
+ })
+
+ render()
+
+ const cancelButton = screen.getByRole("button", { name: /cancel/i })
+
+ fireEvent.click(cancelButton)
+ expect(onCancel).toHaveBeenCalledTimes(1)
+ })
+
+ it("should disable edit button when sendingDisabled is true", () => {
+ ;(useExtensionState as ReturnType).mockReturnValue({
+ filePaths: [],
+ openedTabs: [],
+ taskHistory: [],
+ cwd: "/test/workspace",
+ })
+
+ render()
+
+ const editButton = screen.getByRole("button", {
+ name: (_, element) => {
+ return element.querySelector(".codicon-edit") !== null
+ },
+ })
+
+ expect(editButton).toBeDisabled()
+ })
+
+ it("should disable cancel button when sendingDisabled is true", () => {
+ ;(useExtensionState as ReturnType).mockReturnValue({
+ filePaths: [],
+ openedTabs: [],
+ taskHistory: [],
+ cwd: "/test/workspace",
+ })
+
+ render()
+
+ const cancelButton = screen.getByRole("button", { name: /cancel/i })
+ expect(cancelButton).toBeDisabled()
+ })
+
+ it("should have correct tooltip for edit button", () => {
+ ;(useExtensionState as ReturnType).mockReturnValue({
+ filePaths: [],
+ openedTabs: [],
+ taskHistory: [],
+ cwd: "/test/workspace",
+ })
+
+ render()
+
+ const editButton = screen.getByRole("button", {
+ name: (_, element) => {
+ return element.querySelector(".codicon-edit") !== null
+ },
+ })
+
+ // Check that the button has the correct aria-label (which is used for tooltip)
+ expect(editButton).toHaveAttribute("aria-label", expect.stringMatching(/save/i))
+ })
+
+ it("should position cancel button correctly relative to camera button", () => {
+ ;(useExtensionState as ReturnType).mockReturnValue({
+ filePaths: [],
+ openedTabs: [],
+ taskHistory: [],
+ cwd: "/test/workspace",
+ })
+
+ const { container } = render()
+
+ const cancelButton = screen.getByRole("button", { name: /cancel/i })
+ const cameraButton = screen.getByRole("button", {
+ name: (_, element) => {
+ return element.querySelector(".codicon-device-camera") !== null
+ },
+ })
+
+ // Both buttons should be in the same container (bottom toolbar)
+ const bottomToolbar = container.querySelector(".flex.items-center.gap-0\\.5.shrink-0")
+ expect(bottomToolbar).toContainElement(cancelButton)
+ expect(bottomToolbar).toContainElement(cameraButton)
+
+ // Cancel button should come before camera button in DOM order
+ const buttons = bottomToolbar?.querySelectorAll("button")
+ const cancelIndex = Array.from(buttons || []).indexOf(cancelButton as HTMLButtonElement)
+ const cameraIndex = Array.from(buttons || []).indexOf(cameraButton as HTMLButtonElement)
+ expect(cancelIndex).toBeLessThan(cameraIndex)
+ })
+ })
})