Browse Source

Message edit/delete overhaul (#5538)

* improved chat row first pass

* big UI improvements

* working functionality

* tests working

* ok finally tests working for real!

* translations

* add back hidden flag

* remove option to skip notif

* fixed image issue

* ui fix

* put back edit flag

* oops test fix

* reduce margins

* code review
Will Li 7 months ago
parent
commit
fb374b3e94
68 changed files with 1460 additions and 710 deletions
  1. 216 137
      src/core/webview/__tests__/ClineProvider.spec.ts
  2. 52 0
      src/core/webview/__tests__/webviewMessageHandler.spec.ts
  3. 58 80
      src/core/webview/webviewMessageHandler.ts
  4. 1 6
      src/i18n/locales/ca/common.json
  5. 1 6
      src/i18n/locales/de/common.json
  6. 1 6
      src/i18n/locales/en/common.json
  7. 1 6
      src/i18n/locales/es/common.json
  8. 1 6
      src/i18n/locales/fr/common.json
  9. 1 6
      src/i18n/locales/hi/common.json
  10. 1 6
      src/i18n/locales/id/common.json
  11. 1 6
      src/i18n/locales/it/common.json
  12. 1 6
      src/i18n/locales/ja/common.json
  13. 1 6
      src/i18n/locales/ko/common.json
  14. 1 6
      src/i18n/locales/nl/common.json
  15. 1 6
      src/i18n/locales/pl/common.json
  16. 1 6
      src/i18n/locales/pt-BR/common.json
  17. 1 6
      src/i18n/locales/ru/common.json
  18. 1 6
      src/i18n/locales/tr/common.json
  19. 1 6
      src/i18n/locales/vi/common.json
  20. 1 6
      src/i18n/locales/zh-CN/common.json
  21. 1 6
      src/i18n/locales/zh-TW/common.json
  22. 4 0
      src/shared/ExtensionMessage.ts
  23. 4 0
      src/shared/WebviewMessage.ts
  24. 76 7
      webview-ui/src/App.tsx
  25. 50 18
      webview-ui/src/components/chat/ChatRow.tsx
  26. 397 339
      webview-ui/src/components/chat/ChatTextArea.tsx
  27. 5 3
      webview-ui/src/components/chat/ChatView.tsx
  28. 115 0
      webview-ui/src/components/chat/EditModeControls.tsx
  29. 62 0
      webview-ui/src/components/chat/MessageModificationConfirmationDialog.tsx
  30. 50 0
      webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx
  31. 138 0
      webview-ui/src/components/chat/__tests__/EditModeControls.spec.tsx
  32. 4 1
      webview-ui/src/i18n/locales/ca/chat.json
  33. 7 0
      webview-ui/src/i18n/locales/ca/common.json
  34. 4 1
      webview-ui/src/i18n/locales/de/chat.json
  35. 7 0
      webview-ui/src/i18n/locales/de/common.json
  36. 4 1
      webview-ui/src/i18n/locales/en/chat.json
  37. 7 0
      webview-ui/src/i18n/locales/en/common.json
  38. 4 1
      webview-ui/src/i18n/locales/es/chat.json
  39. 7 0
      webview-ui/src/i18n/locales/es/common.json
  40. 4 1
      webview-ui/src/i18n/locales/fr/chat.json
  41. 7 0
      webview-ui/src/i18n/locales/fr/common.json
  42. 4 1
      webview-ui/src/i18n/locales/hi/chat.json
  43. 7 0
      webview-ui/src/i18n/locales/hi/common.json
  44. 4 1
      webview-ui/src/i18n/locales/id/chat.json
  45. 7 0
      webview-ui/src/i18n/locales/id/common.json
  46. 4 1
      webview-ui/src/i18n/locales/it/chat.json
  47. 7 0
      webview-ui/src/i18n/locales/it/common.json
  48. 4 1
      webview-ui/src/i18n/locales/ja/chat.json
  49. 7 0
      webview-ui/src/i18n/locales/ja/common.json
  50. 4 1
      webview-ui/src/i18n/locales/ko/chat.json
  51. 7 0
      webview-ui/src/i18n/locales/ko/common.json
  52. 4 1
      webview-ui/src/i18n/locales/nl/chat.json
  53. 7 0
      webview-ui/src/i18n/locales/nl/common.json
  54. 4 1
      webview-ui/src/i18n/locales/pl/chat.json
  55. 7 0
      webview-ui/src/i18n/locales/pl/common.json
  56. 4 1
      webview-ui/src/i18n/locales/pt-BR/chat.json
  57. 7 0
      webview-ui/src/i18n/locales/pt-BR/common.json
  58. 4 1
      webview-ui/src/i18n/locales/ru/chat.json
  59. 7 0
      webview-ui/src/i18n/locales/ru/common.json
  60. 4 1
      webview-ui/src/i18n/locales/tr/chat.json
  61. 7 0
      webview-ui/src/i18n/locales/tr/common.json
  62. 4 1
      webview-ui/src/i18n/locales/vi/chat.json
  63. 7 0
      webview-ui/src/i18n/locales/vi/common.json
  64. 4 1
      webview-ui/src/i18n/locales/zh-CN/chat.json
  65. 7 0
      webview-ui/src/i18n/locales/zh-CN/common.json
  66. 4 1
      webview-ui/src/i18n/locales/zh-TW/chat.json
  67. 7 0
      webview-ui/src/i18n/locales/zh-TW/common.json
  68. 17 0
      webview-ui/src/utils/imageUtils.ts

+ 216 - 137
src/core/webview/__tests__/ClineProvider.spec.ts

@@ -1163,15 +1163,10 @@ describe("ClineProvider", () => {
 
 	describe("deleteMessage", () => {
 		beforeEach(async () => {
-			// Mock window.showInformationMessage
-			;(vscode.window.showInformationMessage as any) = vi.fn()
 			await provider.resolveWebviewView(mockWebviewView)
 		})
 
-		test('handles "Just this message" deletion correctly', async () => {
-			// Mock user selecting "Just this message"
-			;(vscode.window.showInformationMessage as any).mockResolvedValue("confirmation.delete_just_this_message")
-
+		test("handles deletion with confirmation dialog", async () => {
 			// Setup mock messages
 			const mockMessages = [
 				{ ts: 1000, type: "say", say: "user_feedback" }, // User message 1
@@ -1202,103 +1197,58 @@ describe("ClineProvider", () => {
 				historyItem: { id: "test-task-id" },
 			})
 
+			// Mock initClineWithHistoryItem
+			;(provider as any).initClineWithHistoryItem = vi.fn()
+
 			// Trigger message deletion
 			const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
 			await messageHandler({ type: "deleteMessage", value: 4000 })
 
-			// Verify correct messages were kept
-			expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([
-				mockMessages[0],
-				mockMessages[1],
-				mockMessages[4],
-				mockMessages[5],
-			])
-
-			// Verify correct API messages were kept
-			expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([
-				mockApiHistory[0],
-				mockApiHistory[1],
-				mockApiHistory[4],
-				mockApiHistory[5],
-			])
-		})
-
-		test('handles "This and all subsequent messages" deletion correctly', async () => {
-			// Mock user selecting "This and all subsequent messages"
-			;(vscode.window.showInformationMessage as any).mockResolvedValue("confirmation.delete_this_and_subsequent")
-
-			// Setup mock messages
-			const mockMessages = [
-				{ ts: 1000, type: "say", say: "user_feedback" },
-				{ ts: 2000, type: "say", say: "text", value: 3000 }, // Message to delete
-				{ ts: 3000, type: "say", say: "user_feedback" },
-				{ ts: 4000, type: "say", say: "user_feedback" },
-			] as ClineMessage[]
-
-			const mockApiHistory = [
-				{ ts: 1000 },
-				{ ts: 2000 },
-				{ ts: 3000 },
-				{ ts: 4000 },
-			] as (Anthropic.MessageParam & {
-				ts?: number
-			})[]
-
-			// Setup Cline instance with auto-mock from the top of the file
-			const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance
-			mockCline.clineMessages = mockMessages
-			mockCline.apiConversationHistory = mockApiHistory
-			await provider.addClineToStack(mockCline)
-
-			// Mock getTaskWithId
-			;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({
-				historyItem: { id: "test-task-id" },
+			// Verify that the dialog message was sent to webview
+			expect(mockPostMessage).toHaveBeenCalledWith({
+				type: "showDeleteMessageDialog",
+				messageTs: 4000,
 			})
 
-			// Trigger message deletion
-			const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
-			await messageHandler({ type: "deleteMessage", value: 3000 })
+			// Simulate user confirming deletion through the dialog
+			await messageHandler({ type: "deleteMessageConfirm", messageTs: 4000 })
 
 			// Verify only messages before the deleted message were kept
-			expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0]])
+			expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0], mockMessages[1]])
 
 			// Verify only API messages before the deleted message were kept
-			expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([mockApiHistory[0]])
-		})
+			expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([
+				mockApiHistory[0],
+				mockApiHistory[1],
+			])
 
-		test("handles Cancel correctly", async () => {
-			// Mock user selecting "Cancel"
-			;(vscode.window.showInformationMessage as any).mockResolvedValue("Cancel")
+			// Verify initClineWithHistoryItem was called
+			expect((provider as any).initClineWithHistoryItem).toHaveBeenCalledWith({ id: "test-task-id" })
+		})
 
-			// Setup Cline instance with auto-mock from the top of the file
-			const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance
-			mockCline.clineMessages = [{ ts: 1000 }, { ts: 2000 }] as ClineMessage[]
-			mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as (Anthropic.MessageParam & {
-				ts?: number
-			})[]
-			await provider.addClineToStack(mockCline)
+		test("handles case when no current task exists", async () => {
+			// Clear the cline stack
+			;(provider as any).clineStack = []
 
 			// Trigger message deletion
 			const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0]
 			await messageHandler({ type: "deleteMessage", value: 2000 })
 
-			// Verify no messages were deleted
-			expect(mockCline.overwriteClineMessages).not.toHaveBeenCalled()
-			expect(mockCline.overwriteApiConversationHistory).not.toHaveBeenCalled()
+			// Verify no dialog was shown since there's no current cline
+			expect(mockPostMessage).not.toHaveBeenCalledWith(
+				expect.objectContaining({
+					type: "showDeleteMessageDialog",
+				}),
+			)
 		})
 	})
 
 	describe("editMessage", () => {
 		beforeEach(async () => {
-			// Mock window.showWarningMessage
-			;(vscode.window.showWarningMessage as any) = vi.fn()
 			await provider.resolveWebviewView(mockWebviewView)
 		})
 
-		test('handles "Proceed" edit correctly', async () => {
-			// Mock user selecting "Proceed" - need to use the localized string key
-			;(vscode.window.showWarningMessage as any).mockResolvedValue("confirmation.proceed")
-
+		test("handles edit with confirmation dialog", async () => {
 			// Setup mock messages
 			const mockMessages = [
 				{ ts: 1000, type: "say", say: "user_feedback" }, // User message 1
@@ -1346,6 +1296,20 @@ describe("ClineProvider", () => {
 				editedMessageContent: "Edited message content",
 			})
 
+			// Verify that the dialog message was sent to webview
+			expect(mockPostMessage).toHaveBeenCalledWith({
+				type: "showEditMessageDialog",
+				messageTs: 4000,
+				text: "Edited message content",
+			})
+
+			// Simulate user confirming edit through the dialog
+			await messageHandler({
+				type: "editMessageConfirm",
+				messageTs: 4000,
+				text: "Edited message content",
+			})
+
 			// Verify correct messages were kept (only messages before the edited one)
 			expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0], mockMessages[1]])
 
@@ -1355,12 +1319,9 @@ describe("ClineProvider", () => {
 				mockApiHistory[1],
 			])
 
-			// Verify handleWebviewAskResponse was called with the edited content
-			expect(mockCline.handleWebviewAskResponse).toHaveBeenCalledWith(
-				"messageResponse",
-				"Edited message content",
-				undefined,
-			)
+			// The new flow calls webviewMessageHandler recursively with askResponse
+			// We need to verify the recursive call happened by checking if the handler was called again
+			expect((mockWebviewView.webview.onDidReceiveMessage as any).mock.calls.length).toBeGreaterThanOrEqual(1)
 		})
 	})
 
@@ -2705,13 +2666,10 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 
 	describe("Edit Messages with Images and Attachments", () => {
 		beforeEach(async () => {
-			;(vscode.window.showInformationMessage as any) = vi.fn()
 			await provider.resolveWebviewView(mockWebviewView)
 		})
 
 		test("handles editing messages containing images", async () => {
-			;(vscode.window.showWarningMessage as any).mockResolvedValue("confirmation.proceed")
-
 			const mockMessages = [
 				{ ts: 1000, type: "say", say: "user_feedback", text: "Original message" },
 				{
@@ -2746,17 +2704,26 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 				editedMessageContent: "Edited message with preserved images",
 			})
 
-			expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
-			expect(mockCline.handleWebviewAskResponse).toHaveBeenCalledWith(
-				"messageResponse",
-				"Edited message with preserved images",
-				undefined,
-			)
+			// Verify dialog was shown
+			expect(mockPostMessage).toHaveBeenCalledWith({
+				type: "showEditMessageDialog",
+				messageTs: 3000,
+				text: "Edited message with preserved images",
+			})
+
+			// Simulate confirmation
+			await messageHandler({
+				type: "editMessageConfirm",
+				messageTs: 3000,
+				text: "Edited message with preserved images",
+			})
+
+			// Verify messages were edited correctly - only the first message should remain
+			expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0]])
+			expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([{ ts: 1000 }])
 		})
 
 		test("handles editing messages with file attachments", async () => {
-			;(vscode.window.showWarningMessage as any).mockResolvedValue("confirmation.proceed")
-
 			const mockMessages = [
 				{ ts: 1000, type: "say", say: "user_feedback", text: "Original message" },
 				{
@@ -2789,6 +2756,20 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 				editedMessageContent: "Edited message with file attachment",
 			})
 
+			// Verify dialog was shown
+			expect(mockPostMessage).toHaveBeenCalledWith({
+				type: "showEditMessageDialog",
+				messageTs: 3000,
+				text: "Edited message with file attachment",
+			})
+
+			// Simulate user confirming the edit
+			await messageHandler({
+				type: "editMessageConfirm",
+				messageTs: 3000,
+				text: "Edited message with file attachment",
+			})
+
 			expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
 			expect(mockCline.handleWebviewAskResponse).toHaveBeenCalledWith(
 				"messageResponse",
@@ -2805,8 +2786,6 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 		})
 
 		test("handles network timeout during edit submission", async () => {
-			;(vscode.window.showInformationMessage as any).mockResolvedValue("confirmation.proceed")
-
 			const mockCline = new Task(defaultTaskOptions)
 			mockCline.clineMessages = [
 				{ ts: 1000, type: "say", say: "user_feedback", text: "Original message", value: 2000 },
@@ -2833,12 +2812,20 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 				}),
 			).resolves.toBeUndefined()
 
+			// Verify dialog was shown
+			expect(mockPostMessage).toHaveBeenCalledWith({
+				type: "showEditMessageDialog",
+				messageTs: 2000,
+				text: "Edited message",
+			})
+
+			// Simulate user confirming the edit
+			await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: "Edited message" })
+
 			expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
 		})
 
 		test("handles connection drops during edit operation", async () => {
-			;(vscode.window.showWarningMessage as any).mockResolvedValue("confirmation.proceed")
-
 			const mockCline = new Task(defaultTaskOptions)
 			mockCline.clineMessages = [
 				{ ts: 1000, type: "say", say: "user_feedback", text: "Original message", value: 2000 },
@@ -2865,6 +2852,17 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 				}),
 			).resolves.toBeUndefined()
 
+			// Verify dialog was shown
+			expect(mockPostMessage).toHaveBeenCalledWith({
+				type: "showEditMessageDialog",
+				messageTs: 2000,
+				text: "Edited message",
+			})
+
+			// Simulate user confirming the edit
+			await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: "Edited message" })
+
+			// The error should be caught and shown
 			expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Error editing message: Connection lost")
 		})
 	})
@@ -2876,8 +2874,6 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 		})
 
 		test("handles race conditions with simultaneous edits", async () => {
-			;(vscode.window.showWarningMessage as any).mockResolvedValue("confirmation.proceed")
-
 			const mockCline = new Task(defaultTaskOptions)
 			mockCline.clineMessages = [
 				{ ts: 1000, type: "say", say: "user_feedback", text: "Message 1", value: 2000 },
@@ -2912,6 +2908,22 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 
 			await Promise.all([edit1Promise, edit2Promise])
 
+			// Verify dialogs were shown for both edits
+			expect(mockPostMessage).toHaveBeenCalledWith({
+				type: "showEditMessageDialog",
+				messageTs: 2000,
+				text: "Edited message 1",
+			})
+			expect(mockPostMessage).toHaveBeenCalledWith({
+				type: "showEditMessageDialog",
+				messageTs: 4000,
+				text: "Edited message 2",
+			})
+
+			// Simulate user confirming both edits
+			await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: "Edited message 1" })
+			await messageHandler({ type: "editMessageConfirm", messageTs: 4000, text: "Edited message 2" })
+
 			// Both operations should complete without throwing
 			expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
 		})
@@ -2940,8 +2952,6 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 		})
 
 		test("handles authorization failures during edit", async () => {
-			;(vscode.window.showWarningMessage as any).mockResolvedValue("confirmation.proceed")
-
 			const mockCline = new Task(defaultTaskOptions)
 			mockCline.clineMessages = [
 				{ ts: 1000, type: "say", say: "user_feedback", text: "Original message", value: 2000 },
@@ -2965,6 +2975,13 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 				editedMessageContent: "Edited message",
 			})
 
+			// Simulate confirmation
+			await messageHandler({
+				type: "editMessageConfirm",
+				messageTs: 2000,
+				text: "Edited message",
+			})
+
 			expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Error editing message: Unauthorized")
 		})
 
@@ -3058,8 +3075,6 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 			})
 
 			test("handles edit operations on deleted messages", async () => {
-				;(vscode.window.showWarningMessage as any).mockResolvedValue("confirmation.proceed")
-
 				const mockCline = new Task(defaultTaskOptions)
 				mockCline.clineMessages = [
 					{ ts: 1000, type: "say", say: "user_feedback", text: "Existing message" },
@@ -3083,17 +3098,26 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 					editedMessageContent: "Edited non-existent message",
 				})
 
-				// Should show confirmation dialog but not perform any operations
-				expect(vscode.window.showWarningMessage).toHaveBeenCalled()
+				// Should show edit dialog
+				expect(mockPostMessage).toHaveBeenCalledWith({
+					type: "showEditMessageDialog",
+					messageTs: 5000,
+					text: "Edited non-existent message",
+				})
+
+				// Simulate user confirming the edit
+				await messageHandler({
+					type: "editMessageConfirm",
+					messageTs: 5000,
+					text: "Edited non-existent message",
+				})
+
+				// Should not perform any operations since message doesn't exist
 				expect(mockCline.overwriteClineMessages).not.toHaveBeenCalled()
 				expect(mockCline.handleWebviewAskResponse).not.toHaveBeenCalled()
 			})
 
 			test("handles delete operations on non-existent messages", async () => {
-				;(vscode.window.showInformationMessage as any).mockResolvedValue(
-					"confirmation.delete_just_this_message",
-				)
-
 				const mockCline = new Task(defaultTaskOptions)
 				mockCline.clineMessages = [
 					{ ts: 1000, type: "say", say: "user_feedback", text: "Existing message" },
@@ -3115,8 +3139,16 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 					value: 5000,
 				})
 
-				// Should show confirmation dialog but not perform any operations
-				expect(vscode.window.showInformationMessage).toHaveBeenCalled()
+				// Should show delete dialog
+				expect(mockPostMessage).toHaveBeenCalledWith({
+					type: "showDeleteMessageDialog",
+					messageTs: 5000,
+				})
+
+				// Simulate user confirming the delete
+				await messageHandler({ type: "deleteMessageConfirm", messageTs: 5000 })
+
+				// Should not perform any operations since message doesn't exist
 				expect(mockCline.overwriteClineMessages).not.toHaveBeenCalled()
 			})
 		})
@@ -3128,8 +3160,6 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 			})
 
 			test("validates proper cleanup during failed edit operations", async () => {
-				;(vscode.window.showWarningMessage as any).mockResolvedValue("confirmation.proceed")
-
 				const mockCline = new Task(defaultTaskOptions)
 				mockCline.clineMessages = [
 					{ ts: 1000, type: "say", say: "user_feedback", text: "Original message", value: 2000 },
@@ -3159,16 +3189,22 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 					editedMessageContent: "Edited message",
 				})
 
+				// Should show edit dialog
+				expect(mockPostMessage).toHaveBeenCalledWith({
+					type: "showEditMessageDialog",
+					messageTs: 2000,
+					text: "Edited message",
+				})
+
+				// Simulate user confirming the edit
+				await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: "Edited message" })
+
 				// Verify cleanup was attempted before failure
 				expect(cleanupSpy).toHaveBeenCalled()
 				expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Error editing message: Operation failed")
 			})
 
 			test("validates proper cleanup during failed delete operations", async () => {
-				;(vscode.window.showInformationMessage as any).mockResolvedValue(
-					"confirmation.delete_just_this_message",
-				)
-
 				const mockCline = new Task(defaultTaskOptions)
 				mockCline.clineMessages = [
 					{ ts: 1000, type: "say", say: "user_feedback", text: "Message to delete" },
@@ -3193,6 +3229,15 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 
 				await messageHandler({ type: "deleteMessage", value: 2000 })
 
+				// Should show delete dialog
+				expect(mockPostMessage).toHaveBeenCalledWith({
+					type: "showDeleteMessageDialog",
+					messageTs: 2000,
+				})
+
+				// Simulate user confirming the delete
+				await messageHandler({ type: "deleteMessageConfirm", messageTs: 2000 })
+
 				// Verify cleanup was attempted before failure
 				expect(cleanupSpy).toHaveBeenCalled()
 				expect(vscode.window.showErrorMessage).toHaveBeenCalledWith(
@@ -3208,8 +3253,6 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 			})
 
 			test("handles editing messages with large text content", async () => {
-				;(vscode.window.showWarningMessage as any).mockResolvedValue("confirmation.proceed")
-
 				// Create a large message (10KB of text)
 				const largeText = "A".repeat(10000)
 				const mockMessages = [
@@ -3238,6 +3281,16 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 					editedMessageContent: largeEditedContent,
 				})
 
+				// Should show edit dialog
+				expect(mockPostMessage).toHaveBeenCalledWith({
+					type: "showEditMessageDialog",
+					messageTs: 2000,
+					text: largeEditedContent,
+				})
+
+				// Simulate user confirming the edit
+				await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: largeEditedContent })
+
 				expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
 				expect(mockCline.handleWebviewAskResponse).toHaveBeenCalledWith(
 					"messageResponse",
@@ -3247,10 +3300,6 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 			})
 
 			test("handles deleting messages with large payloads", async () => {
-				;(vscode.window.showInformationMessage as any).mockResolvedValue(
-					"confirmation.delete_this_and_subsequent",
-				)
-
 				// Create messages with large payloads
 				const largeText = "X".repeat(50000)
 				const mockMessages = [
@@ -3275,6 +3324,15 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 
 				await messageHandler({ type: "deleteMessage", value: 3000 })
 
+				// Should show delete dialog
+				expect(mockPostMessage).toHaveBeenCalledWith({
+					type: "showDeleteMessageDialog",
+					messageTs: 3000,
+				})
+
+				// Simulate user confirming the delete
+				await messageHandler({ type: "deleteMessageConfirm", messageTs: 3000 })
+
 				// Should handle large payloads without issues
 				expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0]])
 				expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([{ ts: 1000 }])
@@ -3285,10 +3343,6 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 			// Note: Error messaging test removed as the implementation may not have proper error handling in place
 
 			test("provides user feedback for successful operations", async () => {
-				;(vscode.window.showInformationMessage as any).mockResolvedValue(
-					"confirmation.delete_just_this_message",
-				)
-
 				const mockCline = new Task(defaultTaskOptions)
 				mockCline.clineMessages = [
 					{ ts: 1000, type: "say", say: "user_feedback", text: "Message to delete" },
@@ -3308,6 +3362,15 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 
 				await messageHandler({ type: "deleteMessage", value: 2000 })
 
+				// Should show delete dialog
+				expect(mockPostMessage).toHaveBeenCalledWith({
+					type: "showDeleteMessageDialog",
+					messageTs: 2000,
+				})
+
+				// Simulate user confirming the delete
+				await messageHandler({ type: "deleteMessageConfirm", messageTs: 2000 })
+
 				// Verify successful operation completed
 				expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
 				expect(provider.initClineWithHistoryItem).toHaveBeenCalled()
@@ -3315,8 +3378,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 			})
 
 			test("handles user cancellation gracefully", async () => {
-				// Mock user canceling the operation
-				;(vscode.window.showWarningMessage as any).mockResolvedValue(undefined)
+				// Test cancellation by not sending confirmation
 
 				const mockCline = new Task(defaultTaskOptions)
 				mockCline.clineMessages = [
@@ -3353,10 +3415,6 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 			})
 
 			test("handles messages with identical timestamps", async () => {
-				;(vscode.window.showInformationMessage as any).mockResolvedValue(
-					"confirmation.delete_just_this_message",
-				)
-
 				const mockCline = new Task(defaultTaskOptions)
 				mockCline.clineMessages = [
 					{ ts: 1000, type: "say", say: "user_feedback", text: "Message 1" },
@@ -3377,13 +3435,20 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 
 				await messageHandler({ type: "deleteMessage", value: 1000 })
 
+				// Should show delete dialog
+				expect(mockPostMessage).toHaveBeenCalledWith({
+					type: "showDeleteMessageDialog",
+					messageTs: 1000,
+				})
+
+				// Simulate user confirming the delete
+				await messageHandler({ type: "deleteMessageConfirm", messageTs: 1000 })
+
 				// Should handle identical timestamps gracefully
 				expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
 			})
 
 			test("handles messages with future timestamps", async () => {
-				;(vscode.window.showWarningMessage as any).mockResolvedValue("confirmation.proceed")
-
 				const futureTimestamp = Date.now() + 100000 // Future timestamp
 				const mockCline = new Task(defaultTaskOptions)
 				mockCline.clineMessages = [
@@ -3419,6 +3484,20 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
 					editedMessageContent: "Edited future message",
 				})
 
+				// Should show edit dialog
+				expect(mockPostMessage).toHaveBeenCalledWith({
+					type: "showEditMessageDialog",
+					messageTs: futureTimestamp + 1000,
+					text: "Edited future message",
+				})
+
+				// Simulate user confirming the edit
+				await messageHandler({
+					type: "editMessageConfirm",
+					messageTs: futureTimestamp + 1000,
+					text: "Edited future message",
+				})
+
 				// Should handle future timestamps correctly
 				expect(mockCline.overwriteClineMessages).toHaveBeenCalled()
 				expect(mockCline.handleWebviewAskResponse).toHaveBeenCalled()

+ 52 - 0
src/core/webview/__tests__/webviewMessageHandler.spec.ts

@@ -28,9 +28,13 @@ const mockClineProvider = {
 			globalStorageUri: { fsPath: "/mock/global/storage" },
 		},
 		setValue: vi.fn(),
+		getValue: vi.fn(),
 	},
 	log: vi.fn(),
 	postStateToWebview: vi.fn(),
+	getCurrentCline: vi.fn(),
+	getTaskWithId: vi.fn(),
+	initClineWithHistoryItem: vi.fn(),
 } as unknown as ClineProvider
 
 import { t } from "../../../i18n"
@@ -482,3 +486,51 @@ describe("webviewMessageHandler - deleteCustomMode", () => {
 		expect(mockClineProvider.postMessageToWebview).not.toHaveBeenCalled()
 	})
 })
+
+describe("webviewMessageHandler - message dialog preferences", () => {
+	beforeEach(() => {
+		vi.clearAllMocks()
+		// Mock a current Cline instance
+		vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({
+			taskId: "test-task-id",
+			apiConversationHistory: [],
+			clineMessages: [],
+		} as any)
+		// Reset getValue mock
+		vi.mocked(mockClineProvider.contextProxy.getValue).mockReturnValue(false)
+	})
+
+	describe("deleteMessage", () => {
+		it("should always show dialog for delete confirmation", async () => {
+			vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({} as any) // Mock current cline exists
+
+			await webviewMessageHandler(mockClineProvider, {
+				type: "deleteMessage",
+				value: 123456789, // Changed from messageTs to value
+			})
+
+			expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
+				type: "showDeleteMessageDialog",
+				messageTs: 123456789,
+			})
+		})
+	})
+
+	describe("submitEditedMessage", () => {
+		it("should always show dialog for edit confirmation", async () => {
+			vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({} as any) // Mock current cline exists
+
+			await webviewMessageHandler(mockClineProvider, {
+				type: "submitEditedMessage",
+				value: 123456789, // messageTs as number
+				editedMessageContent: "edited content", // text content in editedMessageContent field
+			})
+
+			expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
+				type: "showEditMessageDialog",
+				messageTs: 123456789,
+				text: "edited content",
+			})
+		})
+	})
+})

+ 58 - 80
src/core/webview/webviewMessageHandler.ts

@@ -77,55 +77,6 @@ export const webviewMessageHandler = async (
 		return { messageIndex, apiConversationHistoryIndex }
 	}
 
-	/**
-	 * Removes just the target message, preserving messages after the next user message
-	 */
-	const removeMessagesJustThis = async (
-		currentCline: any,
-		messageIndex: number,
-		apiConversationHistoryIndex: number,
-	) => {
-		// Find the next user message first
-		const nextUserMessage = currentCline.clineMessages
-			.slice(messageIndex + 1)
-			.find((msg: ClineMessage) => msg.type === "say" && msg.say === "user_feedback")
-
-		// Handle UI messages
-		if (nextUserMessage) {
-			// Find absolute index of next user message
-			const nextUserMessageIndex = currentCline.clineMessages.findIndex(
-				(msg: ClineMessage) => msg === nextUserMessage,
-			)
-
-			// Keep messages before current message and after next user message
-			await currentCline.overwriteClineMessages([
-				...currentCline.clineMessages.slice(0, messageIndex),
-				...currentCline.clineMessages.slice(nextUserMessageIndex),
-			])
-		} else {
-			// If no next user message, keep only messages before current message
-			await currentCline.overwriteClineMessages(currentCline.clineMessages.slice(0, messageIndex))
-		}
-
-		// Handle API messages
-		if (apiConversationHistoryIndex !== -1) {
-			if (nextUserMessage && nextUserMessage.ts) {
-				// Keep messages before current API message and after next user message
-				await currentCline.overwriteApiConversationHistory([
-					...currentCline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
-					...currentCline.apiConversationHistory.filter(
-						(msg: ApiMessage) => msg.ts && msg.ts >= nextUserMessage.ts,
-					),
-				])
-			} else {
-				// If no next user message, keep only messages before current API message
-				await currentCline.overwriteApiConversationHistory(
-					currentCline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
-				)
-			}
-		}
-	}
-
 	/**
 	 * Removes the target message and all subsequent messages
 	 */
@@ -148,19 +99,19 @@ export const webviewMessageHandler = async (
 	 * Handles message deletion operations with user confirmation
 	 */
 	const handleDeleteOperation = async (messageTs: number): Promise<void> => {
-		const options = [
-			t("common:confirmation.delete_just_this_message"),
-			t("common:confirmation.delete_this_and_subsequent"),
-		]
-
-		const answer = await vscode.window.showInformationMessage(
-			t("common:confirmation.delete_message"),
-			{ modal: true },
-			...options,
-		)
+		// Send message to webview to show delete confirmation dialog
+		await provider.postMessageToWebview({
+			type: "showDeleteMessageDialog",
+			messageTs,
+		})
+	}
 
-		// Only proceed if user selected one of the options and we have a current cline
-		if (answer && options.includes(answer) && provider.getCurrentCline()) {
+	/**
+	 * Handles confirmed message deletion from webview dialog
+	 */
+	const handleDeleteMessageConfirm = async (messageTs: number): Promise<void> => {
+		// Only proceed if we have a current cline
+		if (provider.getCurrentCline()) {
 			const currentCline = provider.getCurrentCline()!
 			const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline)
 
@@ -168,14 +119,8 @@ export const webviewMessageHandler = async (
 				try {
 					const { historyItem } = await provider.getTaskWithId(currentCline.taskId)
 
-					// Check which option the user selected
-					if (answer === options[0]) {
-						// Delete just this message
-						await removeMessagesJustThis(currentCline, messageIndex, apiConversationHistoryIndex)
-					} else if (answer === options[1]) {
-						// Delete this message and all subsequent
-						await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex)
-					}
+					// Delete this message and all subsequent messages
+					await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex)
 
 					// Initialize with history item after deletion
 					await provider.initClineWithHistoryItem(historyItem)
@@ -192,15 +137,26 @@ export const webviewMessageHandler = async (
 	/**
 	 * Handles message editing operations with user confirmation
 	 */
-	const handleEditOperation = async (messageTs: number, editedContent: string): Promise<void> => {
-		const answer = await vscode.window.showWarningMessage(
-			t("common:confirmation.edit_warning"),
-			{ modal: true },
-			t("common:confirmation.proceed"),
-		)
+	const handleEditOperation = async (messageTs: number, editedContent: string, images?: string[]): Promise<void> => {
+		// Send message to webview to show edit confirmation dialog
+		await provider.postMessageToWebview({
+			type: "showEditMessageDialog",
+			messageTs,
+			text: editedContent,
+			images,
+		})
+	}
 
-		// Only proceed if user selected "Proceed" and we have a current cline
-		if (answer === t("common:confirmation.proceed") && provider.getCurrentCline()) {
+	/**
+	 * Handles confirmed message editing from webview dialog
+	 */
+	const handleEditMessageConfirm = async (
+		messageTs: number,
+		editedContent: string,
+		images?: string[],
+	): Promise<void> => {
+		// Only proceed if we have a current cline
+		if (provider.getCurrentCline()) {
 			const currentCline = provider.getCurrentCline()!
 
 			// Use findMessageIndices to find messages based on timestamp
@@ -217,6 +173,7 @@ export const webviewMessageHandler = async (
 						type: "askResponse",
 						askResponse: "messageResponse",
 						text: editedContent,
+						images,
 					})
 
 					// Don't initialize with history item for edit operations
@@ -242,11 +199,12 @@ export const webviewMessageHandler = async (
 		messageTs: number,
 		operation: "delete" | "edit",
 		editedContent?: string,
+		images?: string[],
 	): Promise<void> => {
 		if (operation === "delete") {
 			await handleDeleteOperation(messageTs)
 		} else if (operation === "edit" && editedContent) {
-			await handleEditOperation(messageTs, editedContent)
+			await handleEditOperation(messageTs, editedContent, images)
 		}
 	}
 
@@ -416,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
@@ -1209,7 +1172,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
 		}
@@ -1542,6 +1510,16 @@ export const webviewMessageHandler = async (
 				}
 			}
 			break
+		case "deleteMessageConfirm":
+			if (message.messageTs) {
+				await handleDeleteMessageConfirm(message.messageTs)
+			}
+			break
+		case "editMessageConfirm":
+			if (message.messageTs && message.text) {
+				await handleEditMessageConfirm(message.messageTs, message.text, message.images)
+			}
+			break
 		case "getListApiConfiguration":
 			try {
 				const listApiConfig = await provider.providerSettingsManager.listConfig()

+ 1 - 6
src/i18n/locales/ca/common.json

@@ -21,12 +21,7 @@
 	"confirmation": {
 		"reset_state": "Estàs segur que vols restablir tots els estats i emmagatzematge secret a l'extensió? Això no es pot desfer.",
 		"delete_config_profile": "Estàs segur que vols eliminar aquest perfil de configuració?",
-		"delete_custom_mode_with_rules": "Esteu segur que voleu suprimir aquest mode {scope}?\n\nAixò també suprimirà la carpeta de regles associada a:\n{rulesFolderPath}",
-		"delete_message": "Què vols eliminar?",
-		"edit_warning": "Editar aquest missatge eliminarà tots els missatges posteriors de la conversa. Vols continuar?",
-		"delete_just_this_message": "Només aquest missatge",
-		"delete_this_and_subsequent": "Aquest i tots els missatges posteriors",
-		"proceed": "Continuar"
+		"delete_custom_mode_with_rules": "Esteu segur que voleu suprimir aquest mode {scope}?\n\nAixò també suprimirà la carpeta de regles associada a:\n{rulesFolderPath}"
 	},
 	"errors": {
 		"invalid_data_uri": "Format d'URI de dades no vàlid",

+ 1 - 6
src/i18n/locales/de/common.json

@@ -17,12 +17,7 @@
 	"confirmation": {
 		"reset_state": "Möchtest du wirklich alle Zustände und geheimen Speicher in der Erweiterung zurücksetzen? Dies kann nicht rückgängig gemacht werden.",
 		"delete_config_profile": "Möchtest du dieses Konfigurationsprofil wirklich löschen?",
-		"delete_custom_mode_with_rules": "Bist du sicher, dass du diesen {scope}-Modus löschen möchtest?\n\nDadurch wird auch der zugehörige Regelordner unter folgender Adresse gelöscht:\n{rulesFolderPath}",
-		"delete_message": "Was möchtest du löschen?",
-		"edit_warning": "Das Bearbeiten dieser Nachricht wird alle nachfolgenden Nachrichten in der Unterhaltung löschen. Möchtest du fortfahren?",
-		"delete_just_this_message": "Nur diese Nachricht",
-		"delete_this_and_subsequent": "Diese und alle nachfolgenden Nachrichten",
-		"proceed": "Fortfahren"
+		"delete_custom_mode_with_rules": "Bist du sicher, dass du diesen {scope}-Modus löschen möchtest?\n\nDadurch wird auch der zugehörige Regelordner unter folgender Adresse gelöscht:\n{rulesFolderPath}"
 	},
 	"errors": {
 		"invalid_data_uri": "Ungültiges Daten-URI-Format",

+ 1 - 6
src/i18n/locales/en/common.json

@@ -17,12 +17,7 @@
 	"confirmation": {
 		"reset_state": "Are you sure you want to reset all state and secret storage in the extension? This cannot be undone.",
 		"delete_config_profile": "Are you sure you want to delete this configuration profile?",
-		"delete_custom_mode_with_rules": "Are you sure you want to delete this {scope} mode?\n\nThis will also delete the associated rules folder at:\n{rulesFolderPath}",
-		"delete_message": "What would you like to delete?",
-		"edit_warning": "Editing this message will delete all subsequent messages in the conversation. Do you want to proceed?",
-		"delete_just_this_message": "Just this message",
-		"delete_this_and_subsequent": "This and all subsequent messages",
-		"proceed": "Proceed"
+		"delete_custom_mode_with_rules": "Are you sure you want to delete this {scope} mode?\n\nThis will also delete the associated rules folder at:\n{rulesFolderPath}"
 	},
 	"errors": {
 		"invalid_data_uri": "Invalid data URI format",

+ 1 - 6
src/i18n/locales/es/common.json

@@ -17,12 +17,7 @@
 	"confirmation": {
 		"reset_state": "¿Estás seguro de que deseas restablecer todo el estado y el almacenamiento secreto en la extensión? Esta acción no se puede deshacer.",
 		"delete_config_profile": "¿Estás seguro de que deseas eliminar este perfil de configuración?",
-		"delete_custom_mode_with_rules": "¿Estás seguro de que quieres eliminar este modo {scope}?\n\nEsto también eliminará la carpeta de reglas asociada en:\n{rulesFolderPath}",
-		"delete_message": "¿Qué deseas eliminar?",
-		"edit_warning": "Editar este mensaje eliminará todos los mensajes posteriores en la conversación. ¿Deseas continuar?",
-		"delete_just_this_message": "Solo este mensaje",
-		"delete_this_and_subsequent": "Este y todos los mensajes posteriores",
-		"proceed": "Continuar"
+		"delete_custom_mode_with_rules": "¿Estás seguro de que quieres eliminar este modo {scope}?\n\nEsto también eliminará la carpeta de reglas asociada en:\n{rulesFolderPath}"
 	},
 	"errors": {
 		"invalid_data_uri": "Formato de URI de datos no válido",

+ 1 - 6
src/i18n/locales/fr/common.json

@@ -17,12 +17,7 @@
 	"confirmation": {
 		"reset_state": "Êtes-vous sûr de vouloir réinitialiser le global state et le stockage de secrets de l'extension ? Cette action est irréversible.",
 		"delete_config_profile": "Êtes-vous sûr de vouloir supprimer ce profil de configuration ?",
-		"delete_custom_mode_with_rules": "Êtes-vous sûr de vouloir supprimer ce mode {scope} ?\n\nCela supprimera également le dossier de règles associé à l'adresse :\n{rulesFolderPath}",
-		"delete_message": "Que souhaitez-vous supprimer ?",
-		"edit_warning": "Modifier ce message supprimera tous les messages suivants dans la conversation. Voulez-vous continuer ?",
-		"delete_just_this_message": "Uniquement ce message",
-		"delete_this_and_subsequent": "Ce message et tous les messages suivants",
-		"proceed": "Continuer"
+		"delete_custom_mode_with_rules": "Êtes-vous sûr de vouloir supprimer ce mode {scope} ?\n\nCela supprimera également le dossier de règles associé à l'adresse :\n{rulesFolderPath}"
 	},
 	"errors": {
 		"invalid_data_uri": "Format d'URI de données invalide",

+ 1 - 6
src/i18n/locales/hi/common.json

@@ -17,12 +17,7 @@
 	"confirmation": {
 		"reset_state": "क्या आप वाकई एक्सटेंशन में सभी स्टेट और गुप्त स्टोरेज रीसेट करना चाहते हैं? इसे पूर्ववत नहीं किया जा सकता है।",
 		"delete_config_profile": "क्या आप वाकई इस कॉन्फ़िगरेशन प्रोफ़ाइल को हटाना चाहते हैं?",
-		"delete_custom_mode_with_rules": "क्या आप वाकई इस {scope} मोड को हटाना चाहते हैं?\n\nयह संबंधित नियम फ़ोल्डर को भी यहाँ हटा देगा:\n{rulesFolderPath}",
-		"delete_message": "आप क्या हटाना चाहते हैं?",
-		"edit_warning": "इस संदेश को संपादित करने से बातचीत के सभी बाद के संदेश हट जाएंगे। क्या आप जारी रखना चाहते हैं?",
-		"delete_just_this_message": "सिर्फ यह संदेश",
-		"delete_this_and_subsequent": "यह और सभी बाद के संदेश",
-		"proceed": "जारी रखें"
+		"delete_custom_mode_with_rules": "क्या आप वाकई इस {scope} मोड को हटाना चाहते हैं?\n\nयह संबंधित नियम फ़ोल्डर को भी यहाँ हटा देगा:\n{rulesFolderPath}"
 	},
 	"errors": {
 		"invalid_data_uri": "अमान्य डेटा URI फॉर्मेट",

+ 1 - 6
src/i18n/locales/id/common.json

@@ -17,12 +17,7 @@
 	"confirmation": {
 		"reset_state": "Apakah kamu yakin ingin mereset semua state dan secret storage di ekstensi? Ini tidak dapat dibatalkan.",
 		"delete_config_profile": "Apakah kamu yakin ingin menghapus profil konfigurasi ini?",
-		"delete_custom_mode_with_rules": "Anda yakin ingin menghapus mode {scope} ini?\n\nIni juga akan menghapus folder aturan terkait di:\n{rulesFolderPath}",
-		"delete_message": "Apa yang ingin kamu hapus?",
-		"edit_warning": "Mengedit pesan ini akan menghapus semua pesan selanjutnya dalam percakapan. Apakah kamu ingin melanjutkan?",
-		"delete_just_this_message": "Hanya pesan ini",
-		"delete_this_and_subsequent": "Ini dan semua pesan selanjutnya",
-		"proceed": "Lanjutkan"
+		"delete_custom_mode_with_rules": "Anda yakin ingin menghapus mode {scope} ini?\n\nIni juga akan menghapus folder aturan terkait di:\n{rulesFolderPath}"
 	},
 	"errors": {
 		"invalid_data_uri": "Format data URI tidak valid",

+ 1 - 6
src/i18n/locales/it/common.json

@@ -17,12 +17,7 @@
 	"confirmation": {
 		"reset_state": "Sei sicuro di voler reimpostare tutti gli stati e l'archiviazione segreta nell'estensione? Questa azione non può essere annullata.",
 		"delete_config_profile": "Sei sicuro di voler eliminare questo profilo di configurazione?",
-		"delete_custom_mode_with_rules": "Sei sicuro di voler eliminare questa modalità {scope}?\n\nQuesto eliminerà anche la cartella delle regole associata in:\n{rulesFolderPath}",
-		"delete_message": "Cosa desideri eliminare?",
-		"edit_warning": "Modificare questo messaggio eliminerà tutti i messaggi successivi nella conversazione. Vuoi continuare?",
-		"delete_just_this_message": "Solo questo messaggio",
-		"delete_this_and_subsequent": "Questo e tutti i messaggi successivi",
-		"proceed": "Continua"
+		"delete_custom_mode_with_rules": "Sei sicuro di voler eliminare questa modalità {scope}?\n\nQuesto eliminerà anche la cartella delle regole associata in:\n{rulesFolderPath}"
 	},
 	"errors": {
 		"invalid_data_uri": "Formato URI dati non valido",

+ 1 - 6
src/i18n/locales/ja/common.json

@@ -17,12 +17,7 @@
 	"confirmation": {
 		"reset_state": "拡張機能のすべての状態とシークレットストレージをリセットしてもよろしいですか?この操作は元に戻せません。",
 		"delete_config_profile": "この設定プロファイルを削除してもよろしいですか?",
-		"delete_custom_mode_with_rules": "この{scope}モードを削除してもよろしいですか?\n\nこれにより、関連するルールフォルダも次の場所で削除されます:\n{rulesFolderPath}",
-		"delete_message": "何を削除しますか?",
-		"edit_warning": "このメッセージを編集すると、会話内のすべての後続メッセージが削除されます。続行しますか?",
-		"delete_just_this_message": "このメッセージのみ",
-		"delete_this_and_subsequent": "これ以降のすべてのメッセージ",
-		"proceed": "続行"
+		"delete_custom_mode_with_rules": "この{scope}モードを削除してもよろしいですか?\n\nこれにより、関連するルールフォルダも次の場所で削除されます:\n{rulesFolderPath}"
 	},
 	"errors": {
 		"invalid_data_uri": "データURIフォーマットが無効です",

+ 1 - 6
src/i18n/locales/ko/common.json

@@ -17,12 +17,7 @@
 	"confirmation": {
 		"reset_state": "확장 프로그램의 모든 상태와 보안 저장소를 재설정하시겠습니까? 이 작업은 취소할 수 없습니다.",
 		"delete_config_profile": "이 구성 프로필을 삭제하시겠습니까?",
-		"delete_custom_mode_with_rules": "이 {scope} 모드를 삭제하시겠습니까?\n\n이렇게 하면 연결된 규칙 폴더도 다음 위치에서 삭제됩니다:\n{rulesFolderPath}",
-		"delete_message": "무엇을 삭제하시겠습니까?",
-		"edit_warning": "이 메시지를 편집하면 대화의 모든 후속 메시지가 삭제됩니다. 계속하시겠습니까?",
-		"delete_just_this_message": "이 메시지만",
-		"delete_this_and_subsequent": "이 메시지와 모든 후속 메시지",
-		"proceed": "계속"
+		"delete_custom_mode_with_rules": "이 {scope} 모드를 삭제하시겠습니까?\n\n이렇게 하면 연결된 규칙 폴더도 다음 위치에서 삭제됩니다:\n{rulesFolderPath}"
 	},
 	"errors": {
 		"invalid_data_uri": "잘못된 데이터 URI 형식",

+ 1 - 6
src/i18n/locales/nl/common.json

@@ -17,12 +17,7 @@
 	"confirmation": {
 		"reset_state": "Weet je zeker dat je alle status en geheime opslag in de extensie wilt resetten? Dit kan niet ongedaan worden gemaakt.",
 		"delete_config_profile": "Weet je zeker dat je dit configuratieprofiel wilt verwijderen?",
-		"delete_custom_mode_with_rules": "Weet je zeker dat je deze {scope}-modus wilt verwijderen?\n\nDit verwijdert ook de bijbehorende regelsmap op:\n{rulesFolderPath}",
-		"delete_message": "Wat wil je verwijderen?",
-		"delete_just_this_message": "Alleen dit bericht",
-		"delete_this_and_subsequent": "Dit en alle volgende berichten",
-		"edit_warning": "Het bewerken van dit bericht zal alle volgende berichten in het gesprek verwijderen. Wil je doorgaan?",
-		"proceed": "Doorgaan"
+		"delete_custom_mode_with_rules": "Weet je zeker dat je deze {scope}-modus wilt verwijderen?\n\nDit verwijdert ook de bijbehorende regelsmap op:\n{rulesFolderPath}"
 	},
 	"errors": {
 		"invalid_data_uri": "Ongeldig data-URI-formaat",

+ 1 - 6
src/i18n/locales/pl/common.json

@@ -17,12 +17,7 @@
 	"confirmation": {
 		"reset_state": "Czy na pewno chcesz zresetować wszystkie stany i tajne magazyny w rozszerzeniu? Tej operacji nie można cofnąć.",
 		"delete_config_profile": "Czy na pewno chcesz usunąć ten profil konfiguracyjny?",
-		"delete_custom_mode_with_rules": "Czy na pewno chcesz usunąć ten tryb {scope}?\n\nSpowoduje to również usunięcie powiązanego folderu reguł pod adresem:\n{rulesFolderPath}",
-		"delete_message": "Co chcesz usunąć?",
-		"delete_just_this_message": "Tylko tę wiadomość",
-		"delete_this_and_subsequent": "Tę i wszystkie kolejne wiadomości",
-		"edit_warning": "Edytowanie tej wiadomości usunie wszystkie kolejne wiadomości w rozmowie. Czy chcesz kontynuować?",
-		"proceed": "Kontynuuj"
+		"delete_custom_mode_with_rules": "Czy na pewno chcesz usunąć ten tryb {scope}?\n\nSpowoduje to również usunięcie powiązanego folderu reguł pod adresem:\n{rulesFolderPath}"
 	},
 	"errors": {
 		"invalid_data_uri": "Nieprawidłowy format URI danych",

+ 1 - 6
src/i18n/locales/pt-BR/common.json

@@ -21,12 +21,7 @@
 	"confirmation": {
 		"reset_state": "Tem certeza de que deseja redefinir todo o estado e armazenamento secreto na extensão? Isso não pode ser desfeito.",
 		"delete_config_profile": "Tem certeza de que deseja excluir este perfil de configuração?",
-		"delete_custom_mode_with_rules": "Tem certeza de que deseja excluir este modo {scope}?\n\nIsso também excluirá a pasta de regras associada em:\n{rulesFolderPath}",
-		"delete_message": "O que você gostaria de excluir?",
-		"delete_just_this_message": "Apenas esta mensagem",
-		"delete_this_and_subsequent": "Esta e todas as mensagens subsequentes",
-		"edit_warning": "Editar esta mensagem excluirá todas as mensagens subsequentes na conversa. Deseja continuar?",
-		"proceed": "Continuar"
+		"delete_custom_mode_with_rules": "Tem certeza de que deseja excluir este modo {scope}?\n\nIsso também excluirá a pasta de regras associada em:\n{rulesFolderPath}"
 	},
 	"errors": {
 		"invalid_data_uri": "Formato de URI de dados inválido",

+ 1 - 6
src/i18n/locales/ru/common.json

@@ -17,12 +17,7 @@
 	"confirmation": {
 		"reset_state": "Вы уверены, что хотите сбросить все состояние и секретное хранилище в расширении? Это действие нельзя отменить.",
 		"delete_config_profile": "Вы уверены, что хотите удалить этот профиль конфигурации?",
-		"delete_custom_mode_with_rules": "Вы уверены, что хотите удалить этот режим {scope}?\n\nЭто также приведет к удалению соответствующей папки правил по адресу:\n{rulesFolderPath}",
-		"delete_message": "Что вы хотите удалить?",
-		"delete_just_this_message": "Только это сообщение",
-		"delete_this_and_subsequent": "Это и все последующие сообщения",
-		"edit_warning": "Редактирование этого сообщения удалит все последующие сообщения в разговоре. Хотите продолжить?",
-		"proceed": "Продолжить"
+		"delete_custom_mode_with_rules": "Вы уверены, что хотите удалить этот режим {scope}?\n\nЭто также приведет к удалению соответствующей папки правил по адресу:\n{rulesFolderPath}"
 	},
 	"errors": {
 		"invalid_data_uri": "Неверный формат URI данных",

+ 1 - 6
src/i18n/locales/tr/common.json

@@ -17,12 +17,7 @@
 	"confirmation": {
 		"reset_state": "Uzantıdaki tüm durumları ve gizli depolamayı sıfırlamak istediğinizden emin misiniz? Bu işlem geri alınamaz.",
 		"delete_config_profile": "Bu yapılandırma profilini silmek istediğinizden emin misiniz?",
-		"delete_custom_mode_with_rules": "Bu {scope} modunu silmek istediğinizden emin misiniz?\n\nBu işlem, ilişkili kurallar klasörünü de şu konumdan silecektir:\n{rulesFolderPath}",
-		"delete_message": "Neyi silmek istersiniz?",
-		"delete_just_this_message": "Sadece bu mesajı",
-		"delete_this_and_subsequent": "Bu ve sonraki tüm mesajları",
-		"edit_warning": "Bu mesajı düzenlemek konuşmadaki tüm sonraki mesajları silecektir. Devam etmek istiyor musunuz?",
-		"proceed": "Devam et"
+		"delete_custom_mode_with_rules": "Bu {scope} modunu silmek istediğinizden emin misiniz?\n\nBu işlem, ilişkili kurallar klasörünü de şu konumdan silecektir:\n{rulesFolderPath}"
 	},
 	"errors": {
 		"invalid_data_uri": "Geçersiz veri URI formatı",

+ 1 - 6
src/i18n/locales/vi/common.json

@@ -17,12 +17,7 @@
 	"confirmation": {
 		"reset_state": "Bạn có chắc chắn muốn đặt lại tất cả trạng thái và lưu trữ bí mật trong tiện ích mở rộng không? Hành động này không thể hoàn tác.",
 		"delete_config_profile": "Bạn có chắc chắn muốn xóa hồ sơ cấu hình này không?",
-		"delete_custom_mode_with_rules": "Bạn có chắc chắn muốn xóa chế độ {scope} này không?\n\nThao tác này cũng sẽ xóa thư mục quy tắc liên quan tại:\n{rulesFolderPath}",
-		"delete_message": "Bạn muốn xóa gì?",
-		"delete_just_this_message": "Chỉ tin nhắn này",
-		"delete_this_and_subsequent": "Tin nhắn này và tất cả tin nhắn tiếp theo",
-		"edit_warning": "Chỉnh sửa tin nhắn này sẽ xóa tất cả tin nhắn tiếp theo trong cuộc trò chuyện. Bạn có muốn tiếp tục không?",
-		"proceed": "Tiếp tục"
+		"delete_custom_mode_with_rules": "Bạn có chắc chắn muốn xóa chế độ {scope} này không?\n\nThao tác này cũng sẽ xóa thư mục quy tắc liên quan tại:\n{rulesFolderPath}"
 	},
 	"errors": {
 		"invalid_data_uri": "Định dạng URI dữ liệu không hợp lệ",

+ 1 - 6
src/i18n/locales/zh-CN/common.json

@@ -17,12 +17,7 @@
 	"confirmation": {
 		"reset_state": "您确定要重置扩展中的所有状态和密钥存储吗?此操作无法撤消。",
 		"delete_config_profile": "您确定要删除此配置文件吗?",
-		"delete_custom_mode_with_rules": "您确定要删除此 {scope} 模式吗?\n\n这也将删除位于以下位置的关联规则文件夹:\n{rulesFolderPath}",
-		"delete_message": "您想删除什么?",
-		"edit_warning": "编辑此消息将删除对话中的所有后续消息。您要继续吗?",
-		"delete_just_this_message": "仅此消息",
-		"delete_this_and_subsequent": "此消息及所有后续消息",
-		"proceed": "继续"
+		"delete_custom_mode_with_rules": "您确定要删除此 {scope} 模式吗?\n\n这也将删除位于以下位置的关联规则文件夹:\n{rulesFolderPath}"
 	},
 	"errors": {
 		"invalid_mcp_config": "项目MCP配置格式无效",

+ 1 - 6
src/i18n/locales/zh-TW/common.json

@@ -17,12 +17,7 @@
 	"confirmation": {
 		"reset_state": "您確定要重設擴充套件中的所有狀態和金鑰儲存嗎?此操作無法復原。",
 		"delete_config_profile": "您確定要刪除此設定檔案嗎?",
-		"delete_custom_mode_with_rules": "您確定要刪除此 {scope} 模式嗎?\n\n這也將刪除位於以下位置的關聯規則資料夾:\n{rulesFolderPath}",
-		"delete_message": "您想刪除哪些內容?",
-		"edit_warning": "編輯此訊息將刪除對話中的所有後續訊息。您要繼續嗎?",
-		"delete_just_this_message": "僅這則訊息",
-		"delete_this_and_subsequent": "這則訊息及所有後續訊息",
-		"proceed": "繼續"
+		"delete_custom_mode_with_rules": "您確定要刪除此 {scope} 模式嗎?\n\n這也將刪除位於以下位置的關聯規則資料夾:\n{rulesFolderPath}"
 	},
 	"errors": {
 		"invalid_data_uri": "資料 URI 格式無效",

+ 4 - 0
src/shared/ExtensionMessage.ts

@@ -105,6 +105,8 @@ export interface ExtensionMessage {
 		| "shareTaskSuccess"
 		| "codeIndexSettingsSaved"
 		| "codeIndexSecretStatus"
+		| "showDeleteMessageDialog"
+		| "showEditMessageDialog"
 	text?: string
 	payload?: any // Add a generic payload for now, can refine later
 	action?:
@@ -157,6 +159,8 @@ export interface ExtensionMessage {
 	visibility?: ShareVisibility
 	rulesFolderPath?: string
 	settings?: any
+	messageTs?: number
+	context?: string
 }
 
 export type ExtensionState = Pick<

+ 4 - 0
src/shared/WebviewMessage.ts

@@ -111,7 +111,9 @@ export interface WebviewMessage {
 		| "enhancedPrompt"
 		| "draggedImages"
 		| "deleteMessage"
+		| "deleteMessageConfirm"
 		| "submitEditedMessage"
+		| "editMessageConfirm"
 		| "terminalOutputLineLimit"
 		| "terminalShellIntegrationTimeout"
 		| "terminalShellIntegrationDisabled"
@@ -198,6 +200,7 @@ export interface WebviewMessage {
 	editedMessageContent?: string
 	tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
 	disabled?: boolean
+	context?: string
 	dataUri?: string
 	askResponse?: ClineAskResponse
 	apiConfiguration?: ProviderSettings
@@ -226,6 +229,7 @@ export interface WebviewMessage {
 	ids?: string[]
 	hasSystemPromptOverride?: boolean
 	terminalOperation?: "continue" | "abort"
+	messageTs?: number
 	historyPreviewCollapsed?: boolean
 	filters?: { type?: string; search?: string; tags?: string[] }
 	url?: string // For openExternal

+ 76 - 7
webview-ui/src/App.tsx

@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useRef, useState, useMemo } from "react"
+import React, { useCallback, useEffect, useRef, useState, useMemo } from "react"
 import { useEvent } from "react-use"
 import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
 
@@ -18,6 +18,7 @@ import McpView from "./components/mcp/McpView"
 import { MarketplaceView } from "./components/marketplace/MarketplaceView"
 import ModesView from "./components/modes/ModesView"
 import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog"
+import { DeleteMessageDialog, EditMessageDialog } from "./components/chat/MessageModificationConfirmationDialog"
 import { AccountView } from "./components/account/AccountView"
 import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonInteractiveClick"
 import { TooltipProvider } from "./components/ui/tooltip"
@@ -25,6 +26,29 @@ import { STANDARD_TOOLTIP_DELAY } from "./components/ui/standard-tooltip"
 
 type Tab = "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
 
+interface HumanRelayDialogState {
+	isOpen: boolean
+	requestId: string
+	promptText: string
+}
+
+interface DeleteMessageDialogState {
+	isOpen: boolean
+	messageTs: number
+}
+
+interface EditMessageDialogState {
+	isOpen: boolean
+	messageTs: number
+	text: string
+	images?: string[]
+}
+
+// Memoize dialog components to prevent unnecessary re-renders
+const MemoizedDeleteMessageDialog = React.memo(DeleteMessageDialog)
+const MemoizedEditMessageDialog = React.memo(EditMessageDialog)
+const MemoizedHumanRelayDialog = React.memo(HumanRelayDialog)
+
 const tabsByMessageAction: Partial<Record<NonNullable<ExtensionMessage["action"]>, Tab>> = {
 	chatButtonClicked: "chat",
 	settingsButtonClicked: "settings",
@@ -56,16 +80,24 @@ const App = () => {
 	const [showAnnouncement, setShowAnnouncement] = useState(false)
 	const [tab, setTab] = useState<Tab>("chat")
 
-	const [humanRelayDialogState, setHumanRelayDialogState] = useState<{
-		isOpen: boolean
-		requestId: string
-		promptText: string
-	}>({
+	const [humanRelayDialogState, setHumanRelayDialogState] = useState<HumanRelayDialogState>({
 		isOpen: false,
 		requestId: "",
 		promptText: "",
 	})
 
+	const [deleteMessageDialogState, setDeleteMessageDialogState] = useState<DeleteMessageDialogState>({
+		isOpen: false,
+		messageTs: 0,
+	})
+
+	const [editMessageDialogState, setEditMessageDialogState] = useState<EditMessageDialogState>({
+		isOpen: false,
+		messageTs: 0,
+		text: "",
+		images: [],
+	})
+
 	const settingsRef = useRef<SettingsViewRef>(null)
 	const chatViewRef = useRef<ChatViewRef>(null)
 
@@ -121,6 +153,19 @@ const App = () => {
 				setHumanRelayDialogState({ isOpen: true, requestId, promptText })
 			}
 
+			if (message.type === "showDeleteMessageDialog" && message.messageTs) {
+				setDeleteMessageDialogState({ isOpen: true, messageTs: message.messageTs })
+			}
+
+			if (message.type === "showEditMessageDialog" && message.messageTs && message.text) {
+				setEditMessageDialogState({
+					isOpen: true,
+					messageTs: message.messageTs,
+					text: message.text,
+					images: message.images || [],
+				})
+			}
+
 			if (message.type === "acceptInput") {
 				chatViewRef.current?.acceptInput()
 			}
@@ -199,7 +244,7 @@ const App = () => {
 				showAnnouncement={showAnnouncement}
 				hideAnnouncement={() => setShowAnnouncement(false)}
 			/>
-			<HumanRelayDialog
+			<MemoizedHumanRelayDialog
 				isOpen={humanRelayDialogState.isOpen}
 				requestId={humanRelayDialogState.requestId}
 				promptText={humanRelayDialogState.promptText}
@@ -207,6 +252,30 @@ const App = () => {
 				onSubmit={(requestId, text) => vscode.postMessage({ type: "humanRelayResponse", requestId, text })}
 				onCancel={(requestId) => vscode.postMessage({ type: "humanRelayCancel", requestId })}
 			/>
+			<MemoizedDeleteMessageDialog
+				open={deleteMessageDialogState.isOpen}
+				onOpenChange={(open) => setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: open }))}
+				onConfirm={() => {
+					vscode.postMessage({
+						type: "deleteMessageConfirm",
+						messageTs: deleteMessageDialogState.messageTs,
+					})
+					setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: false }))
+				}}
+			/>
+			<MemoizedEditMessageDialog
+				open={editMessageDialogState.isOpen}
+				onOpenChange={(open) => setEditMessageDialogState((prev) => ({ ...prev, isOpen: open }))}
+				onConfirm={() => {
+					vscode.postMessage({
+						type: "editMessageConfirm",
+						messageTs: editMessageDialogState.messageTs,
+						text: editMessageDialogState.text,
+						images: editMessageDialogState.images,
+					})
+					setEditMessageDialogState((prev) => ({ ...prev, isOpen: false }))
+				}}
+			/>
 		</>
 	)
 }

+ 50 - 18
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"
@@ -6,6 +7,7 @@ import deepEqual from "fast-deep-equal"
 import { VSCodeBadge, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
 
 import type { ClineMessage } from "@roo-code/types"
+import { Mode } from "@roo/modes"
 
 import { ClineApiReqInfo, ClineAskUseMcpServer, ClineSayTool } from "@roo/ExtensionMessage"
 import { COMMAND_OUTPUT_STRING } from "@roo/combineCommandSequences"
@@ -20,6 +22,9 @@ import { removeLeadingNonAlphanumeric } from "@src/utils/removeLeadingNonAlphanu
 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"
 import CodeAccordian from "../common/CodeAccordian"
@@ -109,14 +114,29 @@ export const ChatRowContent = ({
 	editable,
 }: ChatRowContentProps) => {
 	const { t } = useTranslation()
-	const { mcpServers, alwaysAllowMcp, currentCheckpoint } = useExtensionState()
+	const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode } = useExtensionState()
 	const [reasoningCollapsed, setReasoningCollapsed] = useState(true)
 	const [isDiffErrorExpanded, setIsDiffErrorExpanded] = useState(false)
 	const [showCopySuccess, setShowCopySuccess] = useState(false)
 	const [isEditing, setIsEditing] = useState(false)
 	const [editedContent, setEditedContent] = useState("")
+	const [editMode, setEditMode] = useState<Mode>(mode || "code")
+	const [editImages, setEditImages] = useState<string[]>([])
 	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)
@@ -126,15 +146,19 @@ export const ChatRowContent = ({
 	const handleEditClick = useCallback(() => {
 		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.text, message.images, mode])
 
 	// Handle cancel edit
 	const handleCancelEdit = useCallback(() => {
 		setIsEditing(false)
 		setEditedContent(message.text || "")
-	}, [message.text])
+		setEditImages(message.images || [])
+		setEditMode(mode || "code")
+	}, [message.text, message.images, mode])
 
 	// Handle save edit
 	const handleSaveEdit = useCallback(() => {
@@ -144,8 +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", context: "edit", messageTs: message.ts })
+	}, [message.ts])
 
 	const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => {
 		if (message.text !== null && message.text !== undefined && message.say === "api_req_started") {
@@ -1032,21 +1062,23 @@ export const ChatRowContent = ({
 						<div className="bg-vscode-editor-background border rounded-xs p-1 overflow-hidden whitespace-pre-wrap">
 							{isEditing ? (
 								<div className="flex flex-col gap-2 p-2">
-									<textarea
-										className="w-full p-2 bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded-xs"
-										value={editedContent}
-										onChange={(e) => setEditedContent(e.target.value)}
-										rows={5}
-										autoFocus
+									<ChatTextArea
+										inputValue={editedContent}
+										setInputValue={setEditedContent}
+										sendingDisabled={false}
+										selectApiConfigDisabled={true}
+										placeholderText={t("chat:editMessage.placeholder")}
+										selectedImages={editImages}
+										setSelectedImages={setEditImages}
+										onSend={handleSaveEdit}
+										onSelectImages={handleSelectImages}
+										shouldDisableImages={false}
+										mode={editMode}
+										setMode={setEditMode}
+										modeShortcutText=""
+										isEditMode={true}
+										onCancel={handleCancelEdit}
 									/>
-									<div className="flex justify-end gap-2">
-										<Button variant="secondary" size="sm" onClick={handleCancelEdit}>
-											{t("chat:cancel.title")}
-										</Button>
-										<Button variant="default" size="sm" onClick={handleSaveEdit}>
-											{t("chat:save.title")}
-										</Button>
-									</div>
 								</div>
 							) : (
 								<div className="flex justify-between">

+ 397 - 339
webview-ui/src/components/chat/ChatTextArea.tsx

@@ -29,6 +29,7 @@ import { VolumeX, Pin, Check, Image, WandSparkles, SendHorizontal } from "lucide
 import { IndexingStatusBadge } from "./IndexingStatusBadge"
 import { cn } from "@/lib/utils"
 import { usePromptHistory } from "./hooks/usePromptHistory"
+import { EditModeControls } from "./EditModeControls"
 
 interface ChatTextAreaProps {
 	inputValue: string
@@ -45,6 +46,9 @@ interface ChatTextAreaProps {
 	mode: Mode
 	setMode: (value: Mode) => void
 	modeShortcutText: string
+	// Edit mode props
+	isEditMode?: boolean
+	onCancel?: () => void
 }
 
 const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
@@ -64,6 +68,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 			mode,
 			setMode,
 			modeShortcutText,
+			isEditMode = false,
+			onCancel,
 		},
 		ref,
 	) => {
@@ -796,6 +802,378 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 
 		const placeholderBottomText = `\n(${t("chat:addContext")}${shouldDisableImages ? `, ${t("chat:dragFiles")}` : `, ${t("chat:dragFilesImages")}`})`
 
+		// Common mode selector handler
+		const handleModeChange = useCallback(
+			(value: Mode) => {
+				setMode(value)
+				vscode.postMessage({ type: "mode", text: value })
+			},
+			[setMode],
+		)
+
+		// Helper function to render mode selector
+		const renderModeSelector = () => (
+			<ModeSelector
+				value={mode}
+				title={t("chat:selectMode")}
+				onChange={handleModeChange}
+				triggerClassName="w-full"
+				modeShortcutText={modeShortcutText}
+				customModes={customModes}
+				customModePrompts={customModePrompts}
+			/>
+		)
+
+		// Helper function to get API config dropdown options
+		const getApiConfigOptions = useMemo(() => {
+			const pinnedConfigs = (listApiConfigMeta || [])
+				.filter((config) => pinnedApiConfigs && pinnedApiConfigs[config.id])
+				.map((config) => ({
+					value: config.id,
+					label: config.name,
+					name: config.name,
+					type: DropdownOptionType.ITEM,
+					pinned: true,
+				}))
+				.sort((a, b) => a.label.localeCompare(b.label))
+
+			const unpinnedConfigs = (listApiConfigMeta || [])
+				.filter((config) => !pinnedApiConfigs || !pinnedApiConfigs[config.id])
+				.map((config) => ({
+					value: config.id,
+					label: config.name,
+					name: config.name,
+					type: DropdownOptionType.ITEM,
+					pinned: false,
+				}))
+				.sort((a, b) => a.label.localeCompare(b.label))
+
+			const hasPinnedAndUnpinned = pinnedConfigs.length > 0 && unpinnedConfigs.length > 0
+
+			return [
+				...pinnedConfigs,
+				...(hasPinnedAndUnpinned
+					? [
+							{
+								value: "sep-pinned",
+								label: t("chat:separator"),
+								type: DropdownOptionType.SEPARATOR,
+							},
+						]
+					: []),
+				...unpinnedConfigs,
+				{
+					value: "sep-2",
+					label: t("chat:separator"),
+					type: DropdownOptionType.SEPARATOR,
+				},
+				{
+					value: "settingsButtonClicked",
+					label: t("chat:edit"),
+					type: DropdownOptionType.ACTION,
+				},
+			]
+		}, [listApiConfigMeta, pinnedApiConfigs, t])
+
+		// Helper function to handle API config change
+		const handleApiConfigChange = useCallback((value: string) => {
+			if (value === "settingsButtonClicked") {
+				vscode.postMessage({
+					type: "loadApiConfiguration",
+					text: value,
+					values: { section: "providers" },
+				})
+			} else {
+				vscode.postMessage({ type: "loadApiConfigurationById", text: value })
+			}
+		}, [])
+
+		// Helper function to render API config item
+		const renderApiConfigItem = useCallback(
+			({ type, value, label, pinned }: any) => {
+				if (type !== DropdownOptionType.ITEM) {
+					return label
+				}
+
+				const config = listApiConfigMeta?.find((c) => c.id === value)
+				const isCurrentConfig = config?.name === currentApiConfigName
+
+				return (
+					<div className="flex justify-between gap-2 w-full h-5">
+						<div
+							className={cn("truncate min-w-0 overflow-hidden", {
+								"font-medium": isCurrentConfig,
+							})}>
+							{label}
+						</div>
+						<div className="flex justify-end w-10 flex-shrink-0">
+							<div
+								className={cn("size-5 p-1", {
+									"block group-hover:hidden": !pinned,
+									hidden: !isCurrentConfig,
+								})}>
+								<Check className="size-3" />
+							</div>
+							<StandardTooltip content={pinned ? t("chat:unpin") : t("chat:pin")}>
+								<Button
+									variant="ghost"
+									size="icon"
+									onClick={(e) => {
+										e.stopPropagation()
+										togglePinnedApiConfig(value)
+										vscode.postMessage({
+											type: "toggleApiConfigPin",
+											text: value,
+										})
+									}}
+									className={cn("size-5", {
+										"hidden group-hover:flex": !pinned,
+										"bg-accent": pinned,
+									})}>
+									<Pin className="size-3 p-0.5 opacity-50" />
+								</Button>
+							</StandardTooltip>
+						</div>
+					</div>
+				)
+			},
+			[listApiConfigMeta, currentApiConfigName, t, togglePinnedApiConfig],
+		)
+
+		// Helper function to render non-edit mode controls
+		const renderNonEditModeControls = () => (
+			<div className={cn("flex", "justify-between", "items-center", "mt-auto")}>
+				<div className={cn("flex", "items-center", "gap-1", "min-w-0")}>
+					<div className="shrink-0">{renderModeSelector()}</div>
+
+					<div className={cn("flex-1", "min-w-0", "overflow-hidden")}>
+						<SelectDropdown
+							value={currentConfigId}
+							disabled={selectApiConfigDisabled}
+							title={t("chat:selectApiConfig")}
+							disableSearch={false}
+							placeholder={displayName}
+							options={getApiConfigOptions}
+							onChange={handleApiConfigChange}
+							triggerClassName="w-full text-ellipsis overflow-hidden"
+							itemClassName="group"
+							renderItem={renderApiConfigItem}
+						/>
+					</div>
+				</div>
+
+				<div className={cn("flex", "items-center", "gap-0.5", "shrink-0")}>
+					{isTtsPlaying && (
+						<StandardTooltip content={t("chat:stopTts")}>
+							<button
+								aria-label={t("chat:stopTts")}
+								onClick={() => vscode.postMessage({ type: "stopTts" })}
+								className={cn(
+									"relative inline-flex items-center justify-center",
+									"bg-transparent border-none p-1.5",
+									"rounded-md min-w-[28px] min-h-[28px]",
+									"text-vscode-foreground opacity-85",
+									"transition-all duration-150",
+									"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
+									"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
+									"active:bg-[rgba(255,255,255,0.1)]",
+									"cursor-pointer",
+								)}>
+								<VolumeX className="w-4 h-4" />
+							</button>
+						</StandardTooltip>
+					)}
+					<IndexingStatusBadge />
+					<StandardTooltip content={t("chat:addImages")}>
+						<button
+							aria-label={t("chat:addImages")}
+							disabled={shouldDisableImages}
+							onClick={!shouldDisableImages ? onSelectImages : undefined}
+							className={cn(
+								"relative inline-flex items-center justify-center",
+								"bg-transparent border-none p-1.5",
+								"rounded-md min-w-[28px] min-h-[28px]",
+								"text-vscode-foreground opacity-85",
+								"transition-all duration-150",
+								"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
+								"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
+								"active:bg-[rgba(255,255,255,0.1)]",
+								!shouldDisableImages && "cursor-pointer",
+								shouldDisableImages &&
+									"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
+								"mr-1",
+							)}>
+							<Image className="w-4 h-4" />
+						</button>
+					</StandardTooltip>
+				</div>
+			</div>
+		)
+
+		// Helper function to render the text area section
+		const renderTextAreaSection = () => (
+			<div
+				className={cn(
+					"relative",
+					"flex-1",
+					"flex",
+					"flex-col-reverse",
+					"min-h-0",
+					"overflow-hidden",
+					"rounded",
+				)}>
+				<div
+					ref={highlightLayerRef}
+					className={cn(
+						"absolute",
+						"inset-0",
+						"pointer-events-none",
+						"whitespace-pre-wrap",
+						"break-words",
+						"text-transparent",
+						"overflow-hidden",
+						"font-vscode-font-family",
+						"text-vscode-editor-font-size",
+						"leading-vscode-editor-line-height",
+						"py-2",
+						"px-[9px]",
+						"z-10",
+						"forced-color-adjust-none",
+					)}
+					style={{
+						color: "transparent",
+					}}
+				/>
+				<DynamicTextArea
+					ref={(el) => {
+						if (typeof ref === "function") {
+							ref(el)
+						} else if (ref) {
+							ref.current = el
+						}
+						textAreaRef.current = el
+					}}
+					value={inputValue}
+					onChange={(e) => {
+						handleInputChange(e)
+						updateHighlights()
+					}}
+					onFocus={() => setIsFocused(true)}
+					onKeyDown={handleKeyDown}
+					onKeyUp={handleKeyUp}
+					onBlur={handleBlur}
+					onPaste={handlePaste}
+					onSelect={updateCursorPosition}
+					onMouseUp={updateCursorPosition}
+					onHeightChange={(height) => {
+						if (textAreaBaseHeight === undefined || height < textAreaBaseHeight) {
+							setTextAreaBaseHeight(height)
+						}
+
+						onHeightChange?.(height)
+					}}
+					placeholder={placeholderText}
+					minRows={3}
+					maxRows={15}
+					autoFocus={true}
+					className={cn(
+						"w-full",
+						"text-vscode-input-foreground",
+						"font-vscode-font-family",
+						"text-vscode-editor-font-size",
+						"leading-vscode-editor-line-height",
+						"cursor-text",
+						isEditMode ? "pt-1.5 pb-10 px-2" : "py-1.5 px-2",
+						isFocused
+							? "border border-vscode-focusBorder outline outline-vscode-focusBorder"
+							: isDraggingOver
+								? "border-2 border-dashed border-vscode-focusBorder"
+								: "border border-transparent",
+						isDraggingOver
+							? "bg-[color-mix(in_srgb,var(--vscode-input-background)_95%,var(--vscode-focusBorder))]"
+							: "bg-vscode-input-background",
+						"transition-background-color duration-150 ease-in-out",
+						"will-change-background-color",
+						"min-h-[90px]",
+						"box-border",
+						"rounded",
+						"resize-none",
+						"overflow-x-hidden",
+						"overflow-y-auto",
+						"pr-9",
+						"flex-none flex-grow",
+						"z-[2]",
+						"scrollbar-none",
+						"scrollbar-hide",
+					)}
+					onScroll={() => updateHighlights()}
+				/>
+
+				<div className="absolute top-1 right-1 z-30">
+					<StandardTooltip content={t("chat:enhancePrompt")}>
+						<button
+							aria-label={t("chat:enhancePrompt")}
+							disabled={sendingDisabled}
+							onClick={!sendingDisabled ? handleEnhancePrompt : undefined}
+							className={cn(
+								"relative inline-flex items-center justify-center",
+								"bg-transparent border-none p-1.5",
+								"rounded-md min-w-[28px] min-h-[28px]",
+								"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
+								"transition-all duration-150",
+								"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
+								"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
+								"active:bg-[rgba(255,255,255,0.1)]",
+								!sendingDisabled && "cursor-pointer",
+								sendingDisabled &&
+									"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
+							)}>
+							<WandSparkles className={cn("w-4 h-4", isEnhancingPrompt && "animate-spin")} />
+						</button>
+					</StandardTooltip>
+				</div>
+
+				{!isEditMode && (
+					<div className="absolute bottom-1 right-1 z-30">
+						<StandardTooltip content={t("chat:sendMessage")}>
+							<button
+								aria-label={t("chat:sendMessage")}
+								disabled={sendingDisabled}
+								onClick={!sendingDisabled ? onSend : undefined}
+								className={cn(
+									"relative inline-flex items-center justify-center",
+									"bg-transparent border-none p-1.5",
+									"rounded-md min-w-[28px] min-h-[28px]",
+									"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
+									"transition-all duration-150",
+									"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
+									"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
+									"active:bg-[rgba(255,255,255,0.1)]",
+									!sendingDisabled && "cursor-pointer",
+									sendingDisabled &&
+										"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
+								)}>
+								<SendHorizontal className="w-4 h-4" />
+							</button>
+						</StandardTooltip>
+					</div>
+				)}
+
+				{!inputValue && !isEditMode && (
+					<div
+						className="absolute left-2 z-30 pr-9 flex items-center h-8"
+						style={{
+							bottom: "0.25rem",
+							color: "var(--vscode-tab-inactiveForeground)",
+							userSelect: "none",
+							pointerEvents: "none",
+						}}>
+						{placeholderBottomText}
+					</div>
+				)}
+			</div>
+		)
+
 		return (
 			<div
 				className={cn(
@@ -804,12 +1182,12 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 					"flex-col",
 					"gap-1",
 					"bg-editor-background",
-					"px-1.5",
+					isEditMode ? "px-0" : "px-1.5",
 					"pb-1",
 					"outline-none",
 					"border",
 					"border-none",
-					"w-[calc(100%-16px)]",
+					isEditMode ? "w-full" : "w-[calc(100%-16px)]",
 					"ml-auto",
 					"mr-auto",
 					"box-border",
@@ -870,165 +1248,24 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 								/>
 							</div>
 						)}
-						<div
-							className={cn(
-								"relative",
-								"flex-1",
-								"flex",
-								"flex-col-reverse",
-								"min-h-0",
-								"overflow-hidden",
-								"rounded",
-							)}>
-							<div
-								ref={highlightLayerRef}
-								className={cn(
-									"absolute",
-									"inset-0",
-									"pointer-events-none",
-									"whitespace-pre-wrap",
-									"break-words",
-									"text-transparent",
-									"overflow-hidden",
-									"font-vscode-font-family",
-									"text-vscode-editor-font-size",
-									"leading-vscode-editor-line-height",
-									"py-2",
-									"px-[9px]",
-									"z-10",
-									"forced-color-adjust-none",
-								)}
-								style={{
-									color: "transparent",
-								}}
-							/>
-							<DynamicTextArea
-								ref={(el) => {
-									if (typeof ref === "function") {
-										ref(el)
-									} else if (ref) {
-										ref.current = el
-									}
-									textAreaRef.current = el
-								}}
-								value={inputValue}
-								onChange={(e) => {
-									handleInputChange(e)
-									updateHighlights()
-								}}
-								onFocus={() => setIsFocused(true)}
-								onKeyDown={handleKeyDown}
-								onKeyUp={handleKeyUp}
-								onBlur={handleBlur}
-								onPaste={handlePaste}
-								onSelect={updateCursorPosition}
-								onMouseUp={updateCursorPosition}
-								onHeightChange={(height) => {
-									if (textAreaBaseHeight === undefined || height < textAreaBaseHeight) {
-										setTextAreaBaseHeight(height)
-									}
-
-									onHeightChange?.(height)
-								}}
-								placeholder={placeholderText}
-								minRows={3}
-								maxRows={15}
-								autoFocus={true}
-								className={cn(
-									"w-full",
-									"text-vscode-input-foreground",
-									"font-vscode-font-family",
-									"text-vscode-editor-font-size",
-									"leading-vscode-editor-line-height",
-									"cursor-text",
-									"py-1.5 px-2",
-									isFocused
-										? "border border-vscode-focusBorder outline outline-vscode-focusBorder"
-										: isDraggingOver
-											? "border-2 border-dashed border-vscode-focusBorder"
-											: "border border-transparent",
-									isDraggingOver
-										? "bg-[color-mix(in_srgb,var(--vscode-input-background)_95%,var(--vscode-focusBorder))]"
-										: "bg-vscode-input-background",
-									"transition-background-color duration-150 ease-in-out",
-									"will-change-background-color",
-									"min-h-[90px]",
-									"box-border",
-									"rounded",
-									"resize-none",
-									"overflow-x-hidden",
-									"overflow-y-auto",
-									"pr-9",
-									"flex-none flex-grow",
-									"z-[2]",
-									"scrollbar-none",
-									"scrollbar-hide",
-								)}
-								onScroll={() => updateHighlights()}
-							/>
-
-							<div className="absolute top-1 right-1 z-30">
-								<StandardTooltip content={t("chat:enhancePrompt")}>
-									<button
-										aria-label={t("chat:enhancePrompt")}
-										disabled={sendingDisabled}
-										onClick={!sendingDisabled ? handleEnhancePrompt : undefined}
-										className={cn(
-											"relative inline-flex items-center justify-center",
-											"bg-transparent border-none p-1.5",
-											"rounded-md min-w-[28px] min-h-[28px]",
-											"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
-											"transition-all duration-150",
-											"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
-											"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
-											"active:bg-[rgba(255,255,255,0.1)]",
-											!sendingDisabled && "cursor-pointer",
-											sendingDisabled &&
-												"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
-										)}>
-										<WandSparkles className={cn("w-4 h-4", isEnhancingPrompt && "animate-spin")} />
-									</button>
-								</StandardTooltip>
-							</div>
-
-							<div className="absolute bottom-1 right-1 z-30">
-								<StandardTooltip content={t("chat:sendMessage")}>
-									<button
-										aria-label={t("chat:sendMessage")}
-										disabled={sendingDisabled}
-										onClick={!sendingDisabled ? onSend : undefined}
-										className={cn(
-											"relative inline-flex items-center justify-center",
-											"bg-transparent border-none p-1.5",
-											"rounded-md min-w-[28px] min-h-[28px]",
-											"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
-											"transition-all duration-150",
-											"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
-											"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
-											"active:bg-[rgba(255,255,255,0.1)]",
-											!sendingDisabled && "cursor-pointer",
-											sendingDisabled &&
-												"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
-										)}>
-										<SendHorizontal className="w-4 h-4" />
-									</button>
-								</StandardTooltip>
-							</div>
 
-							{!inputValue && (
-								<div
-									className="absolute left-2 z-30 pr-9 flex items-center h-8"
-									style={{
-										bottom: "0.25rem",
-										color: "var(--vscode-tab-inactiveForeground)",
-										userSelect: "none",
-										pointerEvents: "none",
-									}}>
-									{placeholderBottomText}
-								</div>
-							)}
-						</div>
+						{renderTextAreaSection()}
 					</div>
+
+					{isEditMode && (
+						<EditModeControls
+							mode={mode}
+							onModeChange={handleModeChange}
+							modeShortcutText={modeShortcutText}
+							customModes={customModes}
+							customModePrompts={customModePrompts}
+							onCancel={onCancel}
+							onSend={onSend}
+							onSelectImages={onSelectImages}
+							sendingDisabled={sendingDisabled}
+							shouldDisableImages={shouldDisableImages}
+						/>
+					)}
 				</div>
 
 				{selectedImages.length > 0 && (
@@ -1043,186 +1280,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 					/>
 				)}
 
-				<div className={cn("flex", "justify-between", "items-center", "mt-auto")}>
-					<div className={cn("flex", "items-center", "gap-1", "min-w-0")}>
-						<div className="shrink-0">
-							<ModeSelector
-								value={mode}
-								title={t("chat:selectMode")}
-								onChange={(value) => {
-									setMode(value)
-									vscode.postMessage({ type: "mode", text: value })
-								}}
-								triggerClassName="w-full"
-								modeShortcutText={modeShortcutText}
-								customModes={customModes}
-								customModePrompts={customModePrompts}
-							/>
-						</div>
-
-						<div className={cn("flex-1", "min-w-0", "overflow-hidden")}>
-							<SelectDropdown
-								value={currentConfigId}
-								disabled={selectApiConfigDisabled}
-								title={t("chat:selectApiConfig")}
-								disableSearch={false}
-								placeholder={displayName}
-								options={[
-									// Pinned items first.
-									...(listApiConfigMeta || [])
-										.filter((config) => pinnedApiConfigs && pinnedApiConfigs[config.id])
-										.map((config) => ({
-											value: config.id,
-											label: config.name,
-											name: config.name, // Keep name for comparison with currentApiConfigName.
-											type: DropdownOptionType.ITEM,
-											pinned: true,
-										}))
-										.sort((a, b) => a.label.localeCompare(b.label)),
-									// If we have pinned items and unpinned items, add a separator.
-									...(pinnedApiConfigs &&
-									Object.keys(pinnedApiConfigs).length > 0 &&
-									(listApiConfigMeta || []).some((config) => !pinnedApiConfigs[config.id])
-										? [
-												{
-													value: "sep-pinned",
-													label: t("chat:separator"),
-													type: DropdownOptionType.SEPARATOR,
-												},
-											]
-										: []),
-									// Unpinned items sorted alphabetically.
-									...(listApiConfigMeta || [])
-										.filter((config) => !pinnedApiConfigs || !pinnedApiConfigs[config.id])
-										.map((config) => ({
-											value: config.id,
-											label: config.name,
-											name: config.name, // Keep name for comparison with currentApiConfigName.
-											type: DropdownOptionType.ITEM,
-											pinned: false,
-										}))
-										.sort((a, b) => a.label.localeCompare(b.label)),
-									{
-										value: "sep-2",
-										label: t("chat:separator"),
-										type: DropdownOptionType.SEPARATOR,
-									},
-									{
-										value: "settingsButtonClicked",
-										label: t("chat:edit"),
-										type: DropdownOptionType.ACTION,
-									},
-								]}
-								onChange={(value) => {
-									if (value === "settingsButtonClicked") {
-										vscode.postMessage({
-											type: "loadApiConfiguration",
-											text: value,
-											values: { section: "providers" },
-										})
-									} else {
-										vscode.postMessage({ type: "loadApiConfigurationById", text: value })
-									}
-								}}
-								triggerClassName="w-full text-ellipsis overflow-hidden"
-								itemClassName="group"
-								renderItem={({ type, value, label, pinned }) => {
-									if (type !== DropdownOptionType.ITEM) {
-										return label
-									}
-
-									const config = listApiConfigMeta?.find((c) => c.id === value)
-									const isCurrentConfig = config?.name === currentApiConfigName
-
-									return (
-										<div className="flex justify-between gap-2 w-full h-5">
-											<div
-												className={cn("truncate min-w-0 overflow-hidden", {
-													"font-medium": isCurrentConfig,
-												})}>
-												{label}
-											</div>
-											<div className="flex justify-end w-10 flex-shrink-0">
-												<div
-													className={cn("size-5 p-1", {
-														"block group-hover:hidden": !pinned,
-														hidden: !isCurrentConfig,
-													})}>
-													<Check className="size-3" />
-												</div>
-												<StandardTooltip content={pinned ? t("chat:unpin") : t("chat:pin")}>
-													<Button
-														variant="ghost"
-														size="icon"
-														onClick={(e) => {
-															e.stopPropagation()
-															togglePinnedApiConfig(value)
-															vscode.postMessage({
-																type: "toggleApiConfigPin",
-																text: value,
-															})
-														}}
-														className={cn("size-5", {
-															"hidden group-hover:flex": !pinned,
-															"bg-accent": pinned,
-														})}>
-														<Pin className="size-3 p-0.5 opacity-50" />
-													</Button>
-												</StandardTooltip>
-											</div>
-										</div>
-									)
-								}}
-							/>
-						</div>
-					</div>
-
-					<div className={cn("flex", "items-center", "gap-0.5", "shrink-0")}>
-						{isTtsPlaying && (
-							<StandardTooltip content={t("chat:stopTts")}>
-								<button
-									aria-label={t("chat:stopTts")}
-									onClick={() => vscode.postMessage({ type: "stopTts" })}
-									className={cn(
-										"relative inline-flex items-center justify-center",
-										"bg-transparent border-none p-1.5",
-										"rounded-md min-w-[28px] min-h-[28px]",
-										"text-vscode-foreground opacity-85",
-										"transition-all duration-150",
-										"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
-										"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
-										"active:bg-[rgba(255,255,255,0.1)]",
-										"cursor-pointer",
-									)}>
-									<VolumeX className="w-4 h-4" />
-								</button>
-							</StandardTooltip>
-						)}
-						<IndexingStatusBadge />
-						<StandardTooltip content={t("chat:addImages")}>
-							<button
-								aria-label={t("chat:addImages")}
-								disabled={shouldDisableImages}
-								onClick={!shouldDisableImages ? onSelectImages : undefined}
-								className={cn(
-									"relative inline-flex items-center justify-center",
-									"bg-transparent border-none p-1.5",
-									"rounded-md min-w-[28px] min-h-[28px]",
-									"text-vscode-foreground opacity-85",
-									"transition-all duration-150",
-									"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
-									"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
-									"active:bg-[rgba(255,255,255,0.1)]",
-									!shouldDisableImages && "cursor-pointer",
-									shouldDisableImages &&
-										"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
-									"mr-1",
-								)}>
-								<Image className="w-4 h-4" />
-							</button>
-						</StandardTooltip>
-					</div>
-				</div>
+				{!isEditMode && renderNonEditModeControls()}
 			</div>
 		)
 	},

+ 5 - 3
webview-ui/src/components/chat/ChatView.tsx

@@ -9,6 +9,7 @@ import useSound from "use-sound"
 import { LRUCache } from "lru-cache"
 
 import { useDebounceEffect } from "@src/utils/useDebounceEffect"
+import { appendImages } from "@src/utils/imageUtils"
 
 import type { ClineAsk, ClineMessage } from "@roo-code/types"
 
@@ -722,10 +723,11 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 					}
 					break
 				case "selectedImages":
-					const newImages = message.images ?? []
-					if (newImages.length > 0) {
+					// Only handle selectedImages if it's not for editing context
+					// When context is "edit", ChatRow will handle the images
+					if (message.context !== "edit") {
 						setSelectedImages((prevImages) =>
-							[...prevImages, ...newImages].slice(0, MAX_IMAGES_PER_MESSAGE),
+							appendImages(prevImages, message.images, MAX_IMAGES_PER_MESSAGE),
 						)
 					}
 					break

+ 115 - 0
webview-ui/src/components/chat/EditModeControls.tsx

@@ -0,0 +1,115 @@
+import React from "react"
+import { Mode } from "@roo/modes"
+import { Button, StandardTooltip } from "@/components/ui"
+import { Image, SendHorizontal } from "lucide-react"
+import { cn } from "@/lib/utils"
+import ModeSelector from "./ModeSelector"
+import { useAppTranslation } from "@/i18n/TranslationContext"
+
+interface EditModeControlsProps {
+	mode: Mode
+	onModeChange: (value: Mode) => void
+	modeShortcutText: string
+	customModes: any
+	customModePrompts: any
+	onCancel?: () => void
+	onSend: () => void
+	onSelectImages: () => void
+	sendingDisabled: boolean
+	shouldDisableImages: boolean
+}
+
+export const EditModeControls: React.FC<EditModeControlsProps> = ({
+	mode,
+	onModeChange,
+	modeShortcutText,
+	customModes,
+	customModePrompts,
+	onCancel,
+	onSend,
+	onSelectImages,
+	sendingDisabled,
+	shouldDisableImages,
+}) => {
+	const { t } = useAppTranslation()
+
+	return (
+		<div
+			className={cn(
+				"flex",
+				"items-center",
+				"justify-between",
+				"absolute",
+				"bottom-2",
+				"left-2",
+				"right-2",
+				"z-30",
+			)}>
+			<div className={cn("flex", "items-center", "gap-1", "flex-1", "min-w-0")}>
+				<div className="shrink-0">
+					<ModeSelector
+						value={mode}
+						title={t("chat:selectMode")}
+						onChange={onModeChange}
+						triggerClassName="w-full"
+						modeShortcutText={modeShortcutText}
+						customModes={customModes}
+						customModePrompts={customModePrompts}
+					/>
+				</div>
+			</div>
+			<div className={cn("flex", "items-center", "gap-0.5", "shrink-0", "ml-2")}>
+				<Button
+					variant="secondary"
+					size="sm"
+					onClick={onCancel}
+					disabled={sendingDisabled}
+					className="text-xs bg-vscode-toolbar-hoverBackground hover:bg-vscode-button-secondaryBackground text-vscode-button-secondaryForeground">
+					Cancel
+				</Button>
+				<StandardTooltip content={t("chat:addImages")}>
+					<button
+						aria-label={t("chat:addImages")}
+						disabled={shouldDisableImages}
+						onClick={!shouldDisableImages ? onSelectImages : undefined}
+						className={cn(
+							"relative inline-flex items-center justify-center",
+							"bg-transparent border-none p-1.5",
+							"rounded-md min-w-[28px] min-h-[28px]",
+							"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
+							"transition-all duration-150",
+							"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
+							"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
+							"active:bg-[rgba(255,255,255,0.1)]",
+							!shouldDisableImages && "cursor-pointer",
+							shouldDisableImages &&
+								"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
+						)}>
+						<Image className="w-4 h-4" />
+					</button>
+				</StandardTooltip>
+				<StandardTooltip content={t("chat:save.tooltip")}>
+					<button
+						aria-label={t("chat:save.tooltip")}
+						disabled={sendingDisabled}
+						onClick={!sendingDisabled ? onSend : undefined}
+						className={cn(
+							"relative inline-flex items-center justify-center",
+							"bg-transparent border-none p-1.5",
+							"rounded-md min-w-[28px] min-h-[28px]",
+							"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
+							"transition-all duration-150",
+							"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
+							"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
+							"active:bg-[rgba(255,255,255,0.1)]",
+							!sendingDisabled && "cursor-pointer",
+							sendingDisabled &&
+								"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
+						)}>
+						<SendHorizontal className="w-4 h-4" />
+					</button>
+				</StandardTooltip>
+			</div>
+		</div>
+	)
+}

+ 62 - 0
webview-ui/src/components/chat/MessageModificationConfirmationDialog.tsx

@@ -0,0 +1,62 @@
+import React from "react"
+import { useAppTranslation } from "@src/i18n/TranslationContext"
+import {
+	AlertDialog,
+	AlertDialogAction,
+	AlertDialogCancel,
+	AlertDialogContent,
+	AlertDialogDescription,
+	AlertDialogFooter,
+	AlertDialogHeader,
+	AlertDialogTitle,
+} from "@src/components/ui"
+
+interface MessageModificationConfirmationDialogProps {
+	open: boolean
+	onOpenChange: (open: boolean) => void
+	onConfirm: () => void
+	type: "edit" | "delete"
+}
+
+export const MessageModificationConfirmationDialog: React.FC<MessageModificationConfirmationDialogProps> = ({
+	open,
+	onOpenChange,
+	onConfirm,
+	type,
+}) => {
+	const { t } = useAppTranslation()
+
+	const isEdit = type === "edit"
+	const title = isEdit ? t("common:confirmation.editMessage") : t("common:confirmation.deleteMessage")
+	const description = isEdit ? t("common:confirmation.editWarning") : t("common:confirmation.deleteWarning")
+
+	return (
+		<AlertDialog open={open} onOpenChange={onOpenChange}>
+			<AlertDialogContent>
+				<AlertDialogHeader>
+					<AlertDialogTitle className="text-lg">{title}</AlertDialogTitle>
+					<AlertDialogDescription className="text-base">{description}</AlertDialogDescription>
+				</AlertDialogHeader>
+				<AlertDialogFooter className="flex-col gap-2">
+					<AlertDialogCancel className="bg-vscode-button-secondaryBackground hover:bg-vscode-button-secondaryHoverBackground text-vscode-button-secondaryForeground border-vscode-button-border">
+						{t("common:answers.cancel")}
+					</AlertDialogCancel>
+					<AlertDialogAction
+						onClick={onConfirm}
+						className="bg-vscode-button-background hover:bg-vscode-button-hoverBackground text-vscode-button-foreground border-vscode-button-border">
+						{t("common:confirmation.proceed")}
+					</AlertDialogAction>
+				</AlertDialogFooter>
+			</AlertDialogContent>
+		</AlertDialog>
+	)
+}
+
+// Export convenience components for backward compatibility
+export const EditMessageDialog: React.FC<Omit<MessageModificationConfirmationDialogProps, "type">> = (props) => (
+	<MessageModificationConfirmationDialog {...props} type="edit" />
+)
+
+export const DeleteMessageDialog: React.FC<Omit<MessageModificationConfirmationDialogProps, "type">> = (props) => (
+	<MessageModificationConfirmationDialog {...props} type="delete" />
+)

+ 50 - 0
webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx

@@ -920,4 +920,54 @@ describe("ChatTextArea", () => {
 			expect(apiConfigDropdown).toHaveAttribute("disabled")
 		})
 	})
+	describe("edit mode integration", () => {
+		it("should render edit mode UI when isEditMode is true", () => {
+			;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
+				filePaths: [],
+				openedTabs: [],
+				taskHistory: [],
+				cwd: "/test/workspace",
+				customModes: [],
+				customModePrompts: {},
+			})
+
+			render(<ChatTextArea {...defaultProps} isEditMode={true} />)
+
+			// The edit mode UI should be rendered
+			// We can verify this by checking for the presence of elements that are unique to edit mode
+			const cancelButton = screen.getByRole("button", { name: /cancel/i })
+			expect(cancelButton).toBeInTheDocument()
+
+			// Should show save button instead of send button
+			const saveButton = screen.getByRole("button", { name: /save/i })
+			expect(saveButton).toBeInTheDocument()
+
+			// Should not show send button in edit mode
+			const sendButton = screen.queryByRole("button", { name: /send.*message/i })
+			expect(sendButton).not.toBeInTheDocument()
+		})
+
+		it("should not render edit mode UI when isEditMode is false", () => {
+			;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
+				filePaths: [],
+				openedTabs: [],
+				taskHistory: [],
+				cwd: "/test/workspace",
+			})
+
+			render(<ChatTextArea {...defaultProps} isEditMode={false} />)
+
+			// The edit mode UI should not be rendered
+			const cancelButton = screen.queryByRole("button", { name: /cancel/i })
+			expect(cancelButton).not.toBeInTheDocument()
+
+			// Should show send button when not in edit mode
+			const sendButton = screen.getByRole("button", { name: /send.*message/i })
+			expect(sendButton).toBeInTheDocument()
+
+			// Should not show save button when not in edit mode
+			const saveButton = screen.queryByRole("button", { name: /save/i })
+			expect(saveButton).not.toBeInTheDocument()
+		})
+	})
 })

+ 138 - 0
webview-ui/src/components/chat/__tests__/EditModeControls.spec.tsx

@@ -0,0 +1,138 @@
+import React from "react"
+import { render, screen, fireEvent } from "@testing-library/react"
+import { describe, it, expect, vi, beforeEach } from "vitest"
+import { EditModeControls } from "../EditModeControls"
+import { Mode } from "@roo/modes"
+
+// Mock the translation hook
+vi.mock("@/i18n/TranslationContext", () => ({
+	useAppTranslation: () => ({
+		t: (key: string) => key,
+	}),
+}))
+
+// Mock the UI components
+vi.mock("@/components/ui", () => ({
+	Button: ({ children, onClick, disabled, ...props }: any) => (
+		<button onClick={onClick} disabled={disabled} {...props}>
+			{children}
+		</button>
+	),
+	StandardTooltip: ({ children, content }: any) => <div title={content}>{children}</div>,
+}))
+
+// Mock ModeSelector
+vi.mock("../ModeSelector", () => ({
+	default: ({ value, onChange, title }: any) => (
+		<select value={value} onChange={(e) => onChange(e.target.value)} title={title}>
+			<option value="code">Code</option>
+			<option value="architect">Architect</option>
+		</select>
+	),
+}))
+
+describe("EditModeControls", () => {
+	const defaultProps = {
+		mode: "code" as Mode,
+		onModeChange: vi.fn(),
+		modeShortcutText: "Ctrl+M",
+		customModes: [],
+		customModePrompts: {},
+		onCancel: vi.fn(),
+		onSend: vi.fn(),
+		onSelectImages: vi.fn(),
+		sendingDisabled: false,
+		shouldDisableImages: false,
+	}
+
+	beforeEach(() => {
+		vi.clearAllMocks()
+	})
+
+	it("renders all controls correctly", () => {
+		render(<EditModeControls {...defaultProps} />)
+
+		// Check for mode selector
+		expect(screen.getByTitle("chat:selectMode")).toBeInTheDocument()
+
+		// Check for Cancel button
+		expect(screen.getByText("Cancel")).toBeInTheDocument()
+
+		// Check for image button
+		expect(screen.getByTitle("chat:addImages")).toBeInTheDocument()
+
+		// Check for send button
+		expect(screen.getByTitle("chat:save.tooltip")).toBeInTheDocument()
+	})
+
+	it("calls onCancel when Cancel button is clicked", () => {
+		render(<EditModeControls {...defaultProps} />)
+
+		const cancelButton = screen.getByText("Cancel")
+		fireEvent.click(cancelButton)
+
+		expect(defaultProps.onCancel).toHaveBeenCalledTimes(1)
+	})
+
+	it("calls onSend when send button is clicked", () => {
+		render(<EditModeControls {...defaultProps} />)
+
+		const sendButton = screen.getByLabelText("chat:save.tooltip")
+		fireEvent.click(sendButton)
+
+		expect(defaultProps.onSend).toHaveBeenCalledTimes(1)
+	})
+
+	it("calls onSelectImages when image button is clicked", () => {
+		render(<EditModeControls {...defaultProps} />)
+
+		const imageButton = screen.getByLabelText("chat:addImages")
+		fireEvent.click(imageButton)
+
+		expect(defaultProps.onSelectImages).toHaveBeenCalledTimes(1)
+	})
+
+	it("disables buttons when sendingDisabled is true", () => {
+		render(<EditModeControls {...defaultProps} sendingDisabled={true} />)
+
+		const cancelButton = screen.getByText("Cancel")
+		const sendButton = screen.getByLabelText("chat:save.tooltip")
+
+		expect(cancelButton).toBeDisabled()
+		expect(sendButton).toBeDisabled()
+	})
+
+	it("disables image button when shouldDisableImages is true", () => {
+		render(<EditModeControls {...defaultProps} shouldDisableImages={true} />)
+
+		const imageButton = screen.getByLabelText("chat:addImages")
+		expect(imageButton).toBeDisabled()
+	})
+
+	it("does not call onSelectImages when image button is disabled", () => {
+		render(<EditModeControls {...defaultProps} shouldDisableImages={true} />)
+
+		const imageButton = screen.getByLabelText("chat:addImages")
+		fireEvent.click(imageButton)
+
+		expect(defaultProps.onSelectImages).not.toHaveBeenCalled()
+	})
+
+	it("does not call onSend when send button is disabled", () => {
+		render(<EditModeControls {...defaultProps} sendingDisabled={true} />)
+
+		const sendButton = screen.getByLabelText("chat:save.tooltip")
+		fireEvent.click(sendButton)
+
+		expect(defaultProps.onSend).not.toHaveBeenCalled()
+	})
+
+	it("calls onModeChange when mode is changed", () => {
+		render(<EditModeControls {...defaultProps} />)
+
+		const modeSelector = screen.getByTitle("chat:selectMode")
+		fireEvent.change(modeSelector, { target: { value: "architect" } })
+
+		expect(defaultProps.onModeChange).toHaveBeenCalledWith("architect")
+	})
+})

+ 4 - 1
webview-ui/src/i18n/locales/ca/chat.json

@@ -44,7 +44,7 @@
 	},
 	"save": {
 		"title": "Desar",
-		"tooltip": "Desa els canvis del fitxer"
+		"tooltip": "Desa els canvis del missatge"
 	},
 	"reject": {
 		"title": "Rebutjar",
@@ -322,5 +322,8 @@
 	},
 	"versionIndicator": {
 		"ariaLabel": "Versió {{version}} - Feu clic per veure les notes de llançament"
+	},
+	"editMessage": {
+		"placeholder": "Edita el teu missatge..."
 	}
 }

+ 7 - 0
webview-ui/src/i18n/locales/ca/common.json

@@ -51,5 +51,12 @@
 		"success": {
 			"imageDataUriCopied": "URI de dades de la imatge copiada al porta-retalls"
 		}
+	},
+	"confirmation": {
+		"deleteMessage": "Eliminar missatge",
+		"deleteWarning": "Eliminar aquest missatge eliminarà tots els missatges posteriors de la conversa. Vols continuar?",
+		"editMessage": "Editar missatge",
+		"editWarning": "Editar aquest missatge eliminarà tots els missatges posteriors de la conversa. Vols continuar?",
+		"proceed": "Continuar"
 	}
 }

+ 4 - 1
webview-ui/src/i18n/locales/de/chat.json

@@ -44,7 +44,7 @@
 	},
 	"save": {
 		"title": "Speichern",
-		"tooltip": "Dateiänderungen speichern"
+		"tooltip": "Nachrichtenänderungen speichern"
 	},
 	"reject": {
 		"title": "Ablehnen",
@@ -322,5 +322,8 @@
 	},
 	"versionIndicator": {
 		"ariaLabel": "Version {{version}} - Klicken Sie, um die Versionshinweise anzuzeigen"
+	},
+	"editMessage": {
+		"placeholder": "Bearbeite deine Nachricht..."
 	}
 }

+ 7 - 0
webview-ui/src/i18n/locales/de/common.json

@@ -51,5 +51,12 @@
 		"success": {
 			"imageDataUriCopied": "Bild-Daten-URI in die Zwischenablage kopiert"
 		}
+	},
+	"confirmation": {
+		"deleteMessage": "Nachricht löschen",
+		"deleteWarning": "Das Löschen dieser Nachricht wird alle nachfolgenden Nachrichten in der Unterhaltung löschen. Möchtest du fortfahren?",
+		"editMessage": "Nachricht bearbeiten",
+		"editWarning": "Das Bearbeiten dieser Nachricht wird alle nachfolgenden Nachrichten in der Unterhaltung löschen. Möchtest du fortfahren?",
+		"proceed": "Fortfahren"
 	}
 }

+ 4 - 1
webview-ui/src/i18n/locales/en/chat.json

@@ -39,7 +39,7 @@
 	},
 	"save": {
 		"title": "Save",
-		"tooltip": "Save the file changes"
+		"tooltip": "Save the message changes"
 	},
 	"tokenProgress": {
 		"availableSpace": "Available space: {{amount}} tokens",
@@ -87,6 +87,9 @@
 		"title": "Cancel",
 		"tooltip": "Cancel the current operation"
 	},
+	"editMessage": {
+		"placeholder": "Edit your message..."
+	},
 	"scrollToBottom": "Scroll to bottom of chat",
 	"about": "Generate, refactor, and debug code with AI assistance. Check out our <DocsLink>documentation</DocsLink> to learn more.",
 	"onboarding": "Your task list in this workspace is empty.",

+ 7 - 0
webview-ui/src/i18n/locales/en/common.json

@@ -51,5 +51,12 @@
 		"success": {
 			"imageDataUriCopied": "Image data URI copied to clipboard"
 		}
+	},
+	"confirmation": {
+		"deleteMessage": "Delete Message",
+		"deleteWarning": "Deleting this message will delete all subsequent messages in the conversation. Do you want to proceed?",
+		"editMessage": "Edit Message",
+		"editWarning": "Editing this message will delete all subsequent messages in the conversation. Do you want to proceed?",
+		"proceed": "Proceed"
 	}
 }

+ 4 - 1
webview-ui/src/i18n/locales/es/chat.json

@@ -39,7 +39,7 @@
 	},
 	"save": {
 		"title": "Guardar",
-		"tooltip": "Guardar los cambios del archivo"
+		"tooltip": "Guardar los cambios del mensaje"
 	},
 	"tokenProgress": {
 		"availableSpace": "Espacio disponible: {{amount}} tokens",
@@ -322,5 +322,8 @@
 	},
 	"versionIndicator": {
 		"ariaLabel": "Versión {{version}} - Haz clic para ver las notas de la versión"
+	},
+	"editMessage": {
+		"placeholder": "Edita tu mensaje..."
 	}
 }

+ 7 - 0
webview-ui/src/i18n/locales/es/common.json

@@ -51,5 +51,12 @@
 		"success": {
 			"imageDataUriCopied": "URI de datos de imagen copiada al portapapeles"
 		}
+	},
+	"confirmation": {
+		"deleteMessage": "Eliminar mensaje",
+		"deleteWarning": "Eliminar este mensaje eliminará todos los mensajes posteriores en la conversación. ¿Deseas continuar?",
+		"editMessage": "Editar mensaje",
+		"editWarning": "Editar este mensaje eliminará todos los mensajes posteriores en la conversación. ¿Deseas continuar?",
+		"proceed": "Continuar"
 	}
 }

+ 4 - 1
webview-ui/src/i18n/locales/fr/chat.json

@@ -44,7 +44,7 @@
 	},
 	"save": {
 		"title": "Enregistrer",
-		"tooltip": "Sauvegarder les modifications du fichier"
+		"tooltip": "Enregistrer les modifications du message"
 	},
 	"reject": {
 		"title": "Rejeter",
@@ -322,5 +322,8 @@
 	},
 	"versionIndicator": {
 		"ariaLabel": "Version {{version}} - Cliquez pour voir les notes de version"
+	},
+	"editMessage": {
+		"placeholder": "Modifiez votre message..."
 	}
 }

+ 7 - 0
webview-ui/src/i18n/locales/fr/common.json

@@ -51,5 +51,12 @@
 		"success": {
 			"imageDataUriCopied": "URI de données d'image copiée dans le presse-papiers"
 		}
+	},
+	"confirmation": {
+		"deleteMessage": "Supprimer le message",
+		"deleteWarning": "Supprimer ce message supprimera tous les messages suivants dans la conversation. Voulez-vous continuer ?",
+		"editMessage": "Modifier le message",
+		"editWarning": "Modifier ce message supprimera tous les messages suivants dans la conversation. Voulez-vous continuer ?",
+		"proceed": "Continuer"
 	}
 }

+ 4 - 1
webview-ui/src/i18n/locales/hi/chat.json

@@ -44,7 +44,7 @@
 	},
 	"save": {
 		"title": "सहेजें",
-		"tooltip": "फ़ाइल परिवर्तन सहेजें"
+		"tooltip": "संदेश के बदलाव सहेजें"
 	},
 	"reject": {
 		"title": "अस्वीकार करें",
@@ -322,5 +322,8 @@
 	},
 	"versionIndicator": {
 		"ariaLabel": "संस्करण {{version}} - रिलीज़ नोट्स देखने के लिए क्लिक करें"
+	},
+	"editMessage": {
+		"placeholder": "अपना संदेश संपादित करें..."
 	}
 }

+ 7 - 0
webview-ui/src/i18n/locales/hi/common.json

@@ -51,5 +51,12 @@
 		"success": {
 			"imageDataUriCopied": "इमेज डेटा URI क्लिपबोर्ड में कॉपी हो गया"
 		}
+	},
+	"confirmation": {
+		"deleteMessage": "संदेश हटाएं",
+		"deleteWarning": "इस संदेश को हटाने से बातचीत के सभी बाद के संदेश हट जाएंगे। क्या आप जारी रखना चाहते हैं?",
+		"editMessage": "संदेश संपादित करें",
+		"editWarning": "इस संदेश को संपादित करने से बातचीत के सभी बाद के संदेश हट जाएंगे। क्या आप जारी रखना चाहते हैं?",
+		"proceed": "जारी रखें"
 	}
 }

+ 4 - 1
webview-ui/src/i18n/locales/id/chat.json

@@ -45,7 +45,7 @@
 	},
 	"save": {
 		"title": "Simpan",
-		"tooltip": "Simpan perubahan file"
+		"tooltip": "Simpan perubahan pesan"
 	},
 	"tokenProgress": {
 		"availableSpace": "Ruang tersedia: {{amount}} token",
@@ -328,5 +328,8 @@
 	},
 	"versionIndicator": {
 		"ariaLabel": "Versi {{version}} - Klik untuk melihat catatan rilis"
+	},
+	"editMessage": {
+		"placeholder": "Edit pesan Anda..."
 	}
 }

+ 7 - 0
webview-ui/src/i18n/locales/id/common.json

@@ -51,5 +51,12 @@
 		"success": {
 			"imageDataUriCopied": "Data URI gambar disalin ke clipboard"
 		}
+	},
+	"confirmation": {
+		"deleteMessage": "Hapus Pesan",
+		"deleteWarning": "Menghapus pesan ini akan menghapus semua pesan selanjutnya dalam percakapan. Apakah kamu ingin melanjutkan?",
+		"editMessage": "Edit Pesan",
+		"editWarning": "Mengedit pesan ini akan menghapus semua pesan selanjutnya dalam percakapan. Apakah kamu ingin melanjutkan?",
+		"proceed": "Lanjutkan"
 	}
 }

+ 4 - 1
webview-ui/src/i18n/locales/it/chat.json

@@ -44,7 +44,7 @@
 	},
 	"save": {
 		"title": "Salva",
-		"tooltip": "Salva le modifiche al file"
+		"tooltip": "Salva le modifiche del messaggio"
 	},
 	"reject": {
 		"title": "Rifiuta",
@@ -322,5 +322,8 @@
 	},
 	"versionIndicator": {
 		"ariaLabel": "Versione {{version}} - Clicca per visualizzare le note di rilascio"
+	},
+	"editMessage": {
+		"placeholder": "Modifica il tuo messaggio..."
 	}
 }

+ 7 - 0
webview-ui/src/i18n/locales/it/common.json

@@ -51,5 +51,12 @@
 		"success": {
 			"imageDataUriCopied": "URI dati immagine copiato negli appunti"
 		}
+	},
+	"confirmation": {
+		"deleteMessage": "Elimina Messaggio",
+		"deleteWarning": "Eliminando questo messaggio verranno eliminati tutti i messaggi successivi nella conversazione. Vuoi procedere?",
+		"editMessage": "Modifica Messaggio",
+		"editWarning": "Modificando questo messaggio verranno eliminati tutti i messaggi successivi nella conversazione. Vuoi procedere?",
+		"proceed": "Procedi"
 	}
 }

+ 4 - 1
webview-ui/src/i18n/locales/ja/chat.json

@@ -44,7 +44,7 @@
 	},
 	"save": {
 		"title": "保存",
-		"tooltip": "ファイル変更を保存"
+		"tooltip": "メッセージの変更を保存"
 	},
 	"reject": {
 		"title": "拒否",
@@ -322,5 +322,8 @@
 	},
 	"versionIndicator": {
 		"ariaLabel": "バージョン {{version}} - クリックしてリリースノートを表示"
+	},
+	"editMessage": {
+		"placeholder": "メッセージを編集..."
 	}
 }

+ 7 - 0
webview-ui/src/i18n/locales/ja/common.json

@@ -51,5 +51,12 @@
 		"success": {
 			"imageDataUriCopied": "画像データURIをクリップボードにコピーしました"
 		}
+	},
+	"confirmation": {
+		"deleteMessage": "メッセージを削除",
+		"deleteWarning": "このメッセージを削除すると、会話内の後続のメッセージもすべて削除されます。続行しますか?",
+		"editMessage": "メッセージを編集",
+		"editWarning": "このメッセージを編集すると、会話内の後続のメッセージもすべて削除されます。続行しますか?",
+		"proceed": "続行"
 	}
 }

+ 4 - 1
webview-ui/src/i18n/locales/ko/chat.json

@@ -44,7 +44,7 @@
 	},
 	"save": {
 		"title": "저장",
-		"tooltip": "파일 변경사항 저장"
+		"tooltip": "메시지 변경사항 저장"
 	},
 	"reject": {
 		"title": "거부",
@@ -322,5 +322,8 @@
 	},
 	"versionIndicator": {
 		"ariaLabel": "버전 {{version}} - 릴리스 노트를 보려면 클릭하세요"
+	},
+	"editMessage": {
+		"placeholder": "메시지 편집..."
 	}
 }

+ 7 - 0
webview-ui/src/i18n/locales/ko/common.json

@@ -51,5 +51,12 @@
 		"success": {
 			"imageDataUriCopied": "이미지 데이터 URI가 클립보드에 복사됨"
 		}
+	},
+	"confirmation": {
+		"deleteMessage": "메시지 삭제",
+		"deleteWarning": "이 메시지를 삭제하면 대화의 모든 후속 메시지가 삭제됩니다. 계속하시겠습니까?",
+		"editMessage": "메시지 편집",
+		"editWarning": "이 메시지를 편집하면 대화의 모든 후속 메시지가 삭제됩니다. 계속하시겠습니까?",
+		"proceed": "계속"
 	}
 }

+ 4 - 1
webview-ui/src/i18n/locales/nl/chat.json

@@ -39,7 +39,7 @@
 	},
 	"save": {
 		"title": "Opslaan",
-		"tooltip": "Bestandswijzigingen opslaan"
+		"tooltip": "Berichtwijzigingen opslaan"
 	},
 	"tokenProgress": {
 		"availableSpace": "Beschikbare ruimte: {{amount}} tokens",
@@ -322,5 +322,8 @@
 	},
 	"versionIndicator": {
 		"ariaLabel": "Versie {{version}} - Klik om release notes te bekijken"
+	},
+	"editMessage": {
+		"placeholder": "Bewerk je bericht..."
 	}
 }

+ 7 - 0
webview-ui/src/i18n/locales/nl/common.json

@@ -51,5 +51,12 @@
 		"success": {
 			"imageDataUriCopied": "Afbeelding data-URI gekopieerd naar klembord"
 		}
+	},
+	"confirmation": {
+		"deleteMessage": "Bericht Verwijderen",
+		"deleteWarning": "Het verwijderen van dit bericht zal alle volgende berichten in het gesprek verwijderen. Wil je doorgaan?",
+		"editMessage": "Bericht Bewerken",
+		"editWarning": "Het bewerken van dit bericht zal alle volgende berichten in het gesprek verwijderen. Wil je doorgaan?",
+		"proceed": "Doorgaan"
 	}
 }

+ 4 - 1
webview-ui/src/i18n/locales/pl/chat.json

@@ -44,7 +44,7 @@
 	},
 	"save": {
 		"title": "Zapisz",
-		"tooltip": "Zapisz zmiany w pliku"
+		"tooltip": "Zapisz zmiany wiadomości"
 	},
 	"reject": {
 		"title": "Odrzuć",
@@ -322,5 +322,8 @@
 	},
 	"versionIndicator": {
 		"ariaLabel": "Wersja {{version}} - Kliknij, aby wyświetlić informacje o wydaniu"
+	},
+	"editMessage": {
+		"placeholder": "Edytuj swoją wiadomość..."
 	}
 }

+ 7 - 0
webview-ui/src/i18n/locales/pl/common.json

@@ -51,5 +51,12 @@
 		"success": {
 			"imageDataUriCopied": "URI danych obrazu skopiowane do schowka"
 		}
+	},
+	"confirmation": {
+		"deleteMessage": "Usuń Wiadomość",
+		"deleteWarning": "Usunięcie tej wiadomości spowoduje usunięcie wszystkich kolejnych wiadomości w rozmowie. Czy chcesz kontynuować?",
+		"editMessage": "Edytuj Wiadomość",
+		"editWarning": "Edycja tej wiadomości spowoduje usunięcie wszystkich kolejnych wiadomości w rozmowie. Czy chcesz kontynuować?",
+		"proceed": "Kontynuuj"
 	}
 }

+ 4 - 1
webview-ui/src/i18n/locales/pt-BR/chat.json

@@ -44,7 +44,7 @@
 	},
 	"save": {
 		"title": "Salvar",
-		"tooltip": "Salvar as alterações do arquivo"
+		"tooltip": "Salvar as alterações da mensagem"
 	},
 	"reject": {
 		"title": "Rejeitar",
@@ -322,5 +322,8 @@
 	},
 	"versionIndicator": {
 		"ariaLabel": "Versão {{version}} - Clique para ver as notas de lançamento"
+	},
+	"editMessage": {
+		"placeholder": "Edite sua mensagem..."
 	}
 }

+ 7 - 0
webview-ui/src/i18n/locales/pt-BR/common.json

@@ -51,5 +51,12 @@
 		"success": {
 			"imageDataUriCopied": "URI de dados da imagem copiada para a área de transferência"
 		}
+	},
+	"confirmation": {
+		"deleteMessage": "Excluir Mensagem",
+		"deleteWarning": "Excluir esta mensagem irá excluir todas as mensagens subsequentes na conversa. Deseja prosseguir?",
+		"editMessage": "Editar Mensagem",
+		"editWarning": "Editar esta mensagem irá excluir todas as mensagens subsequentes na conversa. Deseja prosseguir?",
+		"proceed": "Prosseguir"
 	}
 }

+ 4 - 1
webview-ui/src/i18n/locales/ru/chat.json

@@ -39,7 +39,7 @@
 	},
 	"save": {
 		"title": "Сохранить",
-		"tooltip": "Сохранить изменения в файле"
+		"tooltip": "Сохранить изменения сообщения"
 	},
 	"tokenProgress": {
 		"availableSpace": "Доступно места: {{amount}} токенов",
@@ -322,5 +322,8 @@
 	},
 	"versionIndicator": {
 		"ariaLabel": "Версия {{version}} - Нажмите, чтобы просмотреть примечания к выпуску"
+	},
+	"editMessage": {
+		"placeholder": "Редактировать сообщение..."
 	}
 }

+ 7 - 0
webview-ui/src/i18n/locales/ru/common.json

@@ -51,5 +51,12 @@
 		"success": {
 			"imageDataUriCopied": "URI данных изображения скопирован в буфер обмена"
 		}
+	},
+	"confirmation": {
+		"deleteMessage": "Удалить Сообщение",
+		"deleteWarning": "Удаление этого сообщения приведет к удалению всех последующих сообщений в разговоре. Хотите продолжить?",
+		"editMessage": "Редактировать Сообщение",
+		"editWarning": "Редактирование этого сообщения приведет к удалению всех последующих сообщений в разговоре. Хотите продолжить?",
+		"proceed": "Продолжить"
 	}
 }

+ 4 - 1
webview-ui/src/i18n/locales/tr/chat.json

@@ -44,7 +44,7 @@
 	},
 	"save": {
 		"title": "Kaydet",
-		"tooltip": "Dosya değişikliklerini kaydet"
+		"tooltip": "Mesaj değişikliklerini kaydet"
 	},
 	"reject": {
 		"title": "Reddet",
@@ -322,5 +322,8 @@
 	},
 	"versionIndicator": {
 		"ariaLabel": "Sürüm {{version}} - Sürüm notlarını görüntülemek için tıklayın"
+	},
+	"editMessage": {
+		"placeholder": "Mesajını düzenle..."
 	}
 }

+ 7 - 0
webview-ui/src/i18n/locales/tr/common.json

@@ -51,5 +51,12 @@
 		"success": {
 			"imageDataUriCopied": "Görsel veri URI'si panoya kopyalandı"
 		}
+	},
+	"confirmation": {
+		"deleteMessage": "Mesajı Sil",
+		"deleteWarning": "Bu mesajı silmek, konuşmadaki sonraki tüm mesajları da silecektir. Devam etmek istiyor musun?",
+		"editMessage": "Mesajı Düzenle",
+		"editWarning": "Bu mesajı düzenlemek, konuşmadaki sonraki tüm mesajları da silecektir. Devam etmek istiyor musun?",
+		"proceed": "Devam Et"
 	}
 }

+ 4 - 1
webview-ui/src/i18n/locales/vi/chat.json

@@ -44,7 +44,7 @@
 	},
 	"save": {
 		"title": "Lưu",
-		"tooltip": "Lưu các thay đổi tệp"
+		"tooltip": "Lưu các thay đổi tin nhắn"
 	},
 	"reject": {
 		"title": "Từ chối",
@@ -322,5 +322,8 @@
 	},
 	"versionIndicator": {
 		"ariaLabel": "Phiên bản {{version}} - Nhấp để xem ghi chú phát hành"
+	},
+	"editMessage": {
+		"placeholder": "Chỉnh sửa tin nhắn của bạn..."
 	}
 }

+ 7 - 0
webview-ui/src/i18n/locales/vi/common.json

@@ -51,5 +51,12 @@
 		"success": {
 			"imageDataUriCopied": "URI dữ liệu hình ảnh đã được sao chép vào clipboard"
 		}
+	},
+	"confirmation": {
+		"deleteMessage": "Xóa Tin Nhắn",
+		"deleteWarning": "Xóa tin nhắn này sẽ xóa tất cả các tin nhắn tiếp theo trong cuộc trò chuyện. Bạn có muốn tiếp tục không?",
+		"editMessage": "Chỉnh Sửa Tin Nhắn",
+		"editWarning": "Chỉnh sửa tin nhắn này sẽ xóa tất cả các tin nhắn tiếp theo trong cuộc trò chuyện. Bạn có muốn tiếp tục không?",
+		"proceed": "Tiếp Tục"
 	}
 }

+ 4 - 1
webview-ui/src/i18n/locales/zh-CN/chat.json

@@ -44,7 +44,7 @@
 	},
 	"save": {
 		"title": "保存",
-		"tooltip": "保存文件更改"
+		"tooltip": "保存消息更改"
 	},
 	"reject": {
 		"title": "拒绝",
@@ -322,5 +322,8 @@
 	},
 	"versionIndicator": {
 		"ariaLabel": "版本 {{version}} - 点击查看发布说明"
+	},
+	"editMessage": {
+		"placeholder": "编辑消息..."
 	}
 }

+ 7 - 0
webview-ui/src/i18n/locales/zh-CN/common.json

@@ -51,5 +51,12 @@
 		"success": {
 			"imageDataUriCopied": "图片数据 URI 已复制到剪贴板"
 		}
+	},
+	"confirmation": {
+		"deleteMessage": "删除消息",
+		"deleteWarning": "删除此消息将删除对话中的所有后续消息。是否继续?",
+		"editMessage": "编辑消息",
+		"editWarning": "编辑此消息将删除对话中的所有后续消息。是否继续?",
+		"proceed": "继续"
 	}
 }

+ 4 - 1
webview-ui/src/i18n/locales/zh-TW/chat.json

@@ -44,7 +44,7 @@
 	},
 	"save": {
 		"title": "儲存",
-		"tooltip": "儲存檔案變更"
+		"tooltip": "儲存訊息變更"
 	},
 	"reject": {
 		"title": "拒絕",
@@ -322,5 +322,8 @@
 	},
 	"versionIndicator": {
 		"ariaLabel": "版本 {{version}} - 點擊查看發布說明"
+	},
+	"editMessage": {
+		"placeholder": "編輯訊息..."
 	}
 }

+ 7 - 0
webview-ui/src/i18n/locales/zh-TW/common.json

@@ -51,5 +51,12 @@
 		"success": {
 			"imageDataUriCopied": "圖片資料 URI 已複製到剪貼簿"
 		}
+	},
+	"confirmation": {
+		"deleteMessage": "刪除訊息",
+		"deleteWarning": "刪除此訊息將刪除對話中的所有後續訊息。是否繼續?",
+		"editMessage": "編輯訊息",
+		"editWarning": "編輯此訊息將刪除對話中的所有後續訊息。是否繼續?",
+		"proceed": "繼續"
 	}
 }

+ 17 - 0
webview-ui/src/utils/imageUtils.ts

@@ -0,0 +1,17 @@
+/**
+ * Utility function to append new images to existing images array
+ * while respecting the maximum image limit
+ *
+ * @param currentImages - The current array of images
+ * @param newImages - The new images to append
+ * @param maxImages - The maximum number of images allowed
+ * @returns The updated images array
+ */
+export function appendImages(currentImages: string[], newImages: string[] | undefined, maxImages: number): string[] {
+	const imagesToAdd = newImages ?? []
+	if (imagesToAdd.length === 0) {
+		return currentImages
+	}
+
+	return [...currentImages, ...imagesToAdd].slice(0, maxImages)
+}