diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 801c6c47747..2e987d22ecc 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -1169,7 +1169,7 @@ describe("ClineProvider", () => { test('handles "Just this message" deletion correctly', async () => { // Mock user selecting "Just this message" - ;(vscode.window.showInformationMessage as any).mockResolvedValue("confirmation.just_this_message") + ;(vscode.window.showInformationMessage as any).mockResolvedValue("confirmation.delete_just_this_message") // Setup mock messages const mockMessages = [ @@ -1224,7 +1224,7 @@ describe("ClineProvider", () => { 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.this_and_subsequent") + ;(vscode.window.showInformationMessage as any).mockResolvedValue("confirmation.delete_this_and_subsequent") // Setup mock messages const mockMessages = [ @@ -1287,6 +1287,186 @@ describe("ClineProvider", () => { }) }) + describe("editMessage", () => { + beforeEach(async () => { + // Mock window.showInformationMessage + ;(vscode.window.showInformationMessage as any) = vi.fn() + await provider.resolveWebviewView(mockWebviewView) + }) + + test('handles "No, just edit this one" edit correctly', async () => { + // Mock user selecting "No, just edit this one" + ;(vscode.window.showInformationMessage as any).mockResolvedValue("confirmation.edit_just_this_message") + + // Setup mock messages + const mockMessages = [ + { ts: 1000, type: "say", say: "user_feedback" }, // User message 1 + { ts: 2000, type: "say", say: "tool" }, // Tool message + { ts: 3000, type: "say", say: "text", value: 4000 }, // Message to edit + { ts: 4000, type: "say", say: "browser_action" }, // Response to edit + { ts: 5000, type: "say", say: "user_feedback" }, // Next user message + { ts: 6000, type: "say", say: "user_feedback" }, // Final message + ] as ClineMessage[] + + const mockApiHistory = [ + { ts: 1000 }, + { ts: 2000 }, + { ts: 3000 }, + { ts: 4000 }, + { ts: 5000 }, + { ts: 6000 }, + ] as (Anthropic.MessageParam & { ts?: number })[] + + // Setup Task instance with auto-mock from the top of the file + const mockCline = new Task(defaultTaskOptions) // Create a new mocked instance + mockCline.clineMessages = mockMessages // Set test-specific messages + mockCline.apiConversationHistory = mockApiHistory // Set API history + + // Explicitly mock the overwrite methods since they're not being called in the tests + mockCline.overwriteClineMessages = vi.fn() + mockCline.overwriteApiConversationHistory = vi.fn() + mockCline.handleWebviewAskResponse = vi.fn() + + await provider.addClineToStack(mockCline) // Add the mocked instance to the stack + + // Mock getTaskWithId + ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({ + historyItem: { id: "test-task-id" }, + }) + + // Trigger message edit + // Get the message handler function that was registered with the webview + const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] + + // Call the message handler with a submitEditedMessage message + await messageHandler({ + type: "submitEditedMessage", + value: 4000, + editedMessageContent: "Edited message content", + }) + + // 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], + ]) + + // Verify handleWebviewAskResponse was called with the edited content + expect(mockCline.handleWebviewAskResponse).toHaveBeenCalledWith( + "messageResponse", + "Edited message content", + undefined, + ) + }) + + test('handles "Yes" (edit and delete subsequent) correctly', async () => { + // Mock user selecting "Yes" + ;(vscode.window.showInformationMessage as any).mockResolvedValue( + "confirmation.edit_this_and_delete_subsequent", + ) + + // Setup mock messages + const mockMessages = [ + { ts: 1000, type: "say", say: "user_feedback" }, + { ts: 2000, type: "say", say: "text", value: 3000 }, // Message to edit + { 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 + + // Explicitly mock the overwrite methods since they're not being called in the tests + mockCline.overwriteClineMessages = vi.fn() + mockCline.overwriteApiConversationHistory = vi.fn() + mockCline.handleWebviewAskResponse = vi.fn() + + await provider.addClineToStack(mockCline) + + // Mock getTaskWithId + ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({ + historyItem: { id: "test-task-id" }, + }) + + // Trigger message edit + // Get the message handler function that was registered with the webview + const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] + + // Call the message handler with a submitEditedMessage message + await messageHandler({ + type: "submitEditedMessage", + value: 3000, + editedMessageContent: "Edited message content", + }) + + // Verify only messages before the edited message were kept + expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0]]) + + // Verify only API messages before the edited message were kept + expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([mockApiHistory[0]]) + + // Verify handleWebviewAskResponse was called with the edited content + expect(mockCline.handleWebviewAskResponse).toHaveBeenCalledWith( + "messageResponse", + "Edited message content", + undefined, + ) + }) + + test("handles Cancel correctly", async () => { + // Mock user selecting "Cancel" + ;(vscode.window.showInformationMessage as any).mockResolvedValue("Cancel") + + // 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 + })[] + + // Explicitly mock the overwrite methods since they're not being called in the tests + mockCline.overwriteClineMessages = vi.fn() + mockCline.overwriteApiConversationHistory = vi.fn() + mockCline.handleWebviewAskResponse = vi.fn() + + await provider.addClineToStack(mockCline) + + // Trigger message edit + const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as any).mock.calls[0][0] + await messageHandler({ + type: "submitEditedMessage", + value: 2000, + editedMessageContent: "Edited message content", + }) + + // Verify no messages were edited or deleted + expect(mockCline.overwriteClineMessages).not.toHaveBeenCalled() + expect(mockCline.overwriteApiConversationHistory).not.toHaveBeenCalled() + expect(mockCline.handleWebviewAskResponse).not.toHaveBeenCalled() + }) + }) + describe("getSystemPrompt", () => { beforeEach(async () => { mockPostMessage.mockClear() diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 296f23bc5fd..062f8667f91 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -55,6 +55,171 @@ export const webviewMessageHandler = async ( const updateGlobalState = async (key: K, value: GlobalState[K]) => await provider.contextProxy.setValue(key, value) + /** + * Handles message modification operations (delete or edit) with confirmation dialog + * @param messageTs Timestamp of the message to operate on + * @param operation Type of operation ('delete' or 'edit') + * @param editedContent New content for edit operations + * @returns Promise + */ + const handleMessageModificationsOperation = async ( + messageTs: number, + operation: "delete" | "edit", + editedContent?: string, + ): Promise => { + // Get current conversation state + const currentCline = provider.getCurrentCline() + if (!currentCline) return + + // Find the message index in the conversation + const timeCutoff = messageTs - 1000 // 1 second buffer before the message + const messageIndex = currentCline.clineMessages.findIndex((msg) => msg.ts && msg.ts >= timeCutoff) + + if (messageIndex === -1) return + + // Check if this is the last user message (no subsequent user messages) + // Find the index of the last user message in the conversation + const lastUserMessageIndex = [...currentCline.clineMessages] + .reverse() + .findIndex((msg) => msg.type === "say" && msg.say === "user_feedback") + + // If lastUserMessageIndex is 0, it means the last message in the array is a user message + // We need to convert this to the actual index in the original array + const actualLastUserIndex = + lastUserMessageIndex === -1 ? -1 : currentCline.clineMessages.length - 1 - lastUserMessageIndex + + // Check if the current message is the last user message + const isLastUserMessage = messageIndex === actualLastUserIndex + + // Different visual order of options based on operation type + const options = + operation === "edit" + ? [ + t("common:confirmation.edit_this_and_delete_subsequent"), + t("common:confirmation.edit_just_this_message"), + ] + : [ + t("common:confirmation.delete_just_this_message"), + t("common:confirmation.delete_this_and_subsequent"), + ] + + // Skip confirmation dialog if it's the last message + let answer: string | undefined + + if (isLastUserMessage) { + // If it's the last message, default to "just_this_message" without showing dialog + answer = + operation === "edit" + ? t("common:confirmation.edit_just_this_message") + : t("common:confirmation.delete_just_this_message") + } else { + // Otherwise show the confirmation dialog + answer = await vscode.window.showInformationMessage( + operation === "edit" ? t("common:confirmation.edit_message") : t("common:confirmation.delete_message"), + { modal: true }, + ...options, + ) + } + + // Only proceed if user selected one of the options or we're skipping the dialog for the last message + if (answer && (options.includes(answer) || isLastUserMessage)) { + // Find API conversation history index + const apiConversationHistoryIndex = currentCline.apiConversationHistory.findIndex( + (msg) => msg.ts && msg.ts >= timeCutoff, + ) + + // Process the message operation + try { + const { historyItem } = await provider.getTaskWithId(currentCline.taskId) + + // Check if user selected the "modify just this message" option + // For delete: options[0], for edit: options[1] + if ( + (operation === "delete" && answer === options[0]) || + (operation === "edit" && answer === options[1]) + ) { + // Find the next user message first + const nextUserMessage = currentCline.clineMessages + .slice(messageIndex + 1) + .find((msg) => 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) => 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) => 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), + ) + } + } + } else if ( + // Check if user selected the "modify this and subsequent" option + // For delete: options[1], for edit: options[0] + (operation === "delete" && answer === options[1]) || + (operation === "edit" && answer === options[0]) + ) { + // Delete this message and all that follow + await currentCline.overwriteClineMessages(currentCline.clineMessages.slice(0, messageIndex)) + + if (apiConversationHistoryIndex !== -1) { + await currentCline.overwriteApiConversationHistory( + currentCline.apiConversationHistory.slice(0, apiConversationHistoryIndex), + ) + } + } + + // Initialize with history item first for delete operations + if (operation === "delete") { + await provider.initClineWithHistoryItem(historyItem) + } + + // For edit operations, process the edited message + if (operation === "edit" && editedContent) { + // Process the edited message as a regular user message + // This will add it to the conversation and trigger an AI response + webviewMessageHandler(provider, { + type: "askResponse", + askResponse: "messageResponse", + text: editedContent, + }) + + // Don't initialize with history item for edit operations + // The webviewMessageHandler will handle the conversation state + } + } catch (error) { + console.error(`Error in ${operation} message:`, error) + vscode.window.showErrorMessage( + `Error ${operation === "edit" ? "editing" : "deleting"} message: ${error instanceof Error ? error.message : String(error)}`, + ) + } + } + } + switch (message.type) { case "webviewDidLaunch": // Load custom modes first @@ -982,108 +1147,19 @@ export const webviewMessageHandler = async ( } break case "deleteMessage": { - const answer = await vscode.window.showInformationMessage( - t("common:confirmation.delete_message"), - { modal: true }, - t("common:confirmation.just_this_message"), - t("common:confirmation.this_and_subsequent"), - ) - + if (provider.getCurrentCline() && typeof message.value === "number" && message.value) { + await handleMessageModificationsOperation(message.value, "delete") + } + break + } + case "submitEditedMessage": { if ( - (answer === t("common:confirmation.just_this_message") || - answer === t("common:confirmation.this_and_subsequent")) && provider.getCurrentCline() && typeof message.value === "number" && - message.value + message.value && + message.editedMessageContent ) { - const timeCutoff = message.value - 1000 // 1 second buffer before the message to delete - - const messageIndex = provider - .getCurrentCline()! - .clineMessages.findIndex((msg) => msg.ts && msg.ts >= timeCutoff) - - const apiConversationHistoryIndex = provider - .getCurrentCline() - ?.apiConversationHistory.findIndex((msg) => msg.ts && msg.ts >= timeCutoff) - - if (messageIndex !== -1) { - const { historyItem } = await provider.getTaskWithId(provider.getCurrentCline()!.taskId) - - if (answer === t("common:confirmation.just_this_message")) { - // Find the next user message first - const nextUserMessage = provider - .getCurrentCline()! - .clineMessages.slice(messageIndex + 1) - .find((msg) => msg.type === "say" && msg.say === "user_feedback") - - // Handle UI messages - if (nextUserMessage) { - // Find absolute index of next user message - const nextUserMessageIndex = provider - .getCurrentCline()! - .clineMessages.findIndex((msg) => msg === nextUserMessage) - - // Keep messages before current message and after next user message - await provider - .getCurrentCline()! - .overwriteClineMessages([ - ...provider.getCurrentCline()!.clineMessages.slice(0, messageIndex), - ...provider.getCurrentCline()!.clineMessages.slice(nextUserMessageIndex), - ]) - } else { - // If no next user message, keep only messages before current message - await provider - .getCurrentCline()! - .overwriteClineMessages( - provider.getCurrentCline()!.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 provider - .getCurrentCline()! - .overwriteApiConversationHistory([ - ...provider - .getCurrentCline()! - .apiConversationHistory.slice(0, apiConversationHistoryIndex), - ...provider - .getCurrentCline()! - .apiConversationHistory.filter( - (msg) => msg.ts && msg.ts >= nextUserMessage.ts, - ), - ]) - } else { - // If no next user message, keep only messages before current API message - await provider - .getCurrentCline()! - .overwriteApiConversationHistory( - provider - .getCurrentCline()! - .apiConversationHistory.slice(0, apiConversationHistoryIndex), - ) - } - } - } else if (answer === t("common:confirmation.this_and_subsequent")) { - // Delete this message and all that follow - await provider - .getCurrentCline()! - .overwriteClineMessages(provider.getCurrentCline()!.clineMessages.slice(0, messageIndex)) - if (apiConversationHistoryIndex !== -1) { - await provider - .getCurrentCline()! - .overwriteApiConversationHistory( - provider - .getCurrentCline()! - .apiConversationHistory.slice(0, apiConversationHistoryIndex), - ) - } - } - - await provider.initClineWithHistoryItem(historyItem) - } + await handleMessageModificationsOperation(message.value, "edit", message.editedMessageContent) } break } diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 6ebb4ef36aa..f3c651b9d7c 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -23,8 +23,11 @@ "delete_config_profile": "Estàs segur que vols eliminar aquest perfil de configuració?", "delete_custom_mode": "Estàs segur que vols eliminar aquest mode personalitzat?", "delete_message": "Què vols eliminar?", - "just_this_message": "Només aquest missatge", - "this_and_subsequent": "Aquest i tots els missatges posteriors" + "edit_message": "Eliminar tots els missatges després d'aquest?", + "delete_just_this_message": "Només aquest missatge", + "edit_just_this_message": "No, només editar aquest", + "delete_this_and_subsequent": "Aquest i tots els missatges posteriors", + "edit_this_and_delete_subsequent": "Sí" }, "errors": { "invalid_data_uri": "Format d'URI de dades no vàlid", @@ -111,6 +114,11 @@ "remove": "Eliminar", "keep": "Mantenir" }, + "buttons": { + "save": "Desar", + "cancel": "Cancel·lar", + "edit": "Editar" + }, "tasks": { "canceled": "Error de tasca: Ha estat aturada i cancel·lada per l'usuari.", "deleted": "Fallada de tasca: Ha estat aturada i eliminada per l'usuari.", diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 1fceb3bf3ff..ec8ac6a481c 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -19,8 +19,11 @@ "delete_config_profile": "Möchtest du dieses Konfigurationsprofil wirklich löschen?", "delete_custom_mode": "Möchtest du diesen benutzerdefinierten Modus wirklich löschen?", "delete_message": "Was möchtest du löschen?", - "just_this_message": "Nur diese Nachricht", - "this_and_subsequent": "Diese und alle nachfolgenden Nachrichten" + "edit_message": "Alle Nachrichten nach dieser löschen?", + "delete_just_this_message": "Nur diese Nachricht", + "edit_just_this_message": "Nein, nur diese bearbeiten", + "delete_this_and_subsequent": "Diese und alle nachfolgenden Nachrichten", + "edit_this_and_delete_subsequent": "Ja" }, "errors": { "invalid_data_uri": "Ungültiges Daten-URI-Format", @@ -107,6 +110,11 @@ "remove": "Entfernen", "keep": "Behalten" }, + "buttons": { + "save": "Speichern", + "cancel": "Abbrechen", + "edit": "Bearbeiten" + }, "tasks": { "canceled": "Aufgabenfehler: Die Aufgabe wurde vom Benutzer gestoppt und abgebrochen.", "deleted": "Aufgabenfehler: Die Aufgabe wurde vom Benutzer gestoppt und gelöscht.", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index b0779cdd892..307508f5663 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -19,8 +19,11 @@ "delete_config_profile": "Are you sure you want to delete this configuration profile?", "delete_custom_mode": "Are you sure you want to delete this custom mode?", "delete_message": "What would you like to delete?", - "just_this_message": "Just this message", - "this_and_subsequent": "This and all subsequent messages" + "edit_message": "Delete all messages after this one?", + "delete_just_this_message": "Just this message", + "edit_just_this_message": "No, just edit this one", + "delete_this_and_subsequent": "This and all subsequent messages", + "edit_this_and_delete_subsequent": "Yes" }, "errors": { "invalid_data_uri": "Invalid data URI format", @@ -107,6 +110,11 @@ "remove": "Remove", "keep": "Keep" }, + "buttons": { + "save": "Save", + "cancel": "Cancel", + "edit": "Edit" + }, "tasks": { "canceled": "Task error: It was stopped and canceled by the user.", "deleted": "Task failure: It was stopped and deleted by the user.", diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 316da8d6cc9..3e54265a4a3 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -19,8 +19,11 @@ "delete_config_profile": "¿Estás seguro de que deseas eliminar este perfil de configuración?", "delete_custom_mode": "¿Estás seguro de que deseas eliminar este modo personalizado?", "delete_message": "¿Qué deseas eliminar?", - "just_this_message": "Solo este mensaje", - "this_and_subsequent": "Este y todos los mensajes posteriores" + "edit_message": "¿Eliminar todos los mensajes posteriores a este?", + "delete_just_this_message": "Solo este mensaje", + "edit_just_this_message": "No, solo editar este", + "delete_this_and_subsequent": "Este y todos los mensajes posteriores", + "edit_this_and_delete_subsequent": "Sí" }, "errors": { "invalid_data_uri": "Formato de URI de datos no válido", @@ -107,6 +110,11 @@ "remove": "Eliminar", "keep": "Mantener" }, + "buttons": { + "save": "Guardar", + "cancel": "Cancelar", + "edit": "Editar" + }, "tasks": { "canceled": "Error de tarea: Fue detenida y cancelada por el usuario.", "deleted": "Fallo de tarea: Fue detenida y eliminada por el usuario.", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 99c511d26a3..a073484f6ff 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -19,8 +19,11 @@ "delete_config_profile": "Êtes-vous sûr de vouloir supprimer ce profil de configuration ?", "delete_custom_mode": "Êtes-vous sûr de vouloir supprimer ce mode personnalisé ?", "delete_message": "Que souhaitez-vous supprimer ?", - "just_this_message": "Uniquement ce message", - "this_and_subsequent": "Ce message et tous les messages suivants" + "edit_message": "Supprimer tous les messages après celui-ci ?", + "delete_just_this_message": "Uniquement ce message", + "edit_just_this_message": "Non, modifier uniquement celui-ci", + "delete_this_and_subsequent": "Ce message et tous les messages suivants", + "edit_this_and_delete_subsequent": "Oui" }, "errors": { "invalid_data_uri": "Format d'URI de données invalide", @@ -107,6 +110,11 @@ "remove": "Supprimer", "keep": "Conserver" }, + "buttons": { + "save": "Enregistrer", + "cancel": "Annuler", + "edit": "Modifier" + }, "tasks": { "canceled": "Erreur de tâche : Elle a été arrêtée et annulée par l'utilisateur.", "deleted": "Échec de la tâche : Elle a été arrêtée et supprimée par l'utilisateur.", diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index fb92bb8f8d5..7381e2b1782 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -19,8 +19,11 @@ "delete_config_profile": "क्या आप वाकई इस कॉन्फ़िगरेशन प्रोफ़ाइल को हटाना चाहते हैं?", "delete_custom_mode": "क्या आप वाकई इस कस्टम मोड को हटाना चाहते हैं?", "delete_message": "आप क्या हटाना चाहते हैं?", - "just_this_message": "सिर्फ यह संदेश", - "this_and_subsequent": "यह और सभी बाद के संदेश" + "edit_message": "इसके बाद के सभी संदेशों को हटाएं?", + "delete_just_this_message": "सिर्फ यह संदेश", + "edit_just_this_message": "नहीं, केवल इसे संपादित करें", + "delete_this_and_subsequent": "यह और सभी बाद के संदेश", + "edit_this_and_delete_subsequent": "हां" }, "errors": { "invalid_data_uri": "अमान्य डेटा URI फॉर्मेट", @@ -107,6 +110,11 @@ "remove": "हटाएं", "keep": "रखें" }, + "buttons": { + "save": "सहेजें", + "cancel": "रद्द करें", + "edit": "संपादित करें" + }, "tasks": { "canceled": "टास्क त्रुटि: इसे उपयोगकर्ता द्वारा रोका और रद्द किया गया था।", "deleted": "टास्क विफलता: इसे उपयोगकर्ता द्वारा रोका और हटाया गया था।", diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index fec0bb863e0..8ea7f9f5e2a 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -19,8 +19,11 @@ "delete_config_profile": "Apakah kamu yakin ingin menghapus profil konfigurasi ini?", "delete_custom_mode": "Apakah kamu yakin ingin menghapus mode kustom ini?", "delete_message": "Apa yang ingin kamu hapus?", - "just_this_message": "Hanya pesan ini", - "this_and_subsequent": "Ini dan semua pesan selanjutnya" + "edit_message": "Hapus semua pesan setelah ini?", + "delete_just_this_message": "Hanya pesan ini", + "edit_just_this_message": "Tidak, hanya edit yang ini", + "delete_this_and_subsequent": "Ini dan semua pesan selanjutnya", + "edit_this_and_delete_subsequent": "Ya" }, "errors": { "invalid_data_uri": "Format data URI tidak valid", @@ -107,6 +110,11 @@ "remove": "Hapus", "keep": "Simpan" }, + "buttons": { + "save": "Simpan", + "cancel": "Batal", + "edit": "Edit" + }, "tasks": { "canceled": "Error tugas: Dihentikan dan dibatalkan oleh pengguna.", "deleted": "Kegagalan tugas: Dihentikan dan dihapus oleh pengguna.", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index 119b4a6d7aa..c0d2bb7b1c1 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -19,8 +19,11 @@ "delete_config_profile": "Sei sicuro di voler eliminare questo profilo di configurazione?", "delete_custom_mode": "Sei sicuro di voler eliminare questa modalità personalizzata?", "delete_message": "Cosa desideri eliminare?", - "just_this_message": "Solo questo messaggio", - "this_and_subsequent": "Questo e tutti i messaggi successivi" + "edit_message": "Eliminare tutti i messaggi dopo questo?", + "delete_just_this_message": "Solo questo messaggio", + "edit_just_this_message": "No, modifica solo questo", + "delete_this_and_subsequent": "Questo e tutti i messaggi successivi", + "edit_this_and_delete_subsequent": "Sì" }, "errors": { "invalid_data_uri": "Formato URI dati non valido", @@ -107,6 +110,11 @@ "remove": "Rimuovi", "keep": "Mantieni" }, + "buttons": { + "save": "Salva", + "cancel": "Annulla", + "edit": "Modifica" + }, "tasks": { "canceled": "Errore attività: È stata interrotta e annullata dall'utente.", "deleted": "Fallimento attività: È stata interrotta ed eliminata dall'utente.", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 39b8948b184..d3a38191b2b 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -19,8 +19,11 @@ "delete_config_profile": "この設定プロファイルを削除してもよろしいですか?", "delete_custom_mode": "このカスタムモードを削除してもよろしいですか?", "delete_message": "何を削除しますか?", - "just_this_message": "このメッセージのみ", - "this_and_subsequent": "これ以降のすべてのメッセージ" + "edit_message": "これ以降のメッセージをすべて削除しますか?", + "delete_just_this_message": "このメッセージのみ", + "edit_just_this_message": "いいえ、これだけを編集", + "delete_this_and_subsequent": "これ以降のすべてのメッセージ", + "edit_this_and_delete_subsequent": "はい" }, "errors": { "invalid_data_uri": "データURIフォーマットが無効です", @@ -107,6 +110,11 @@ "remove": "削除", "keep": "保持" }, + "buttons": { + "save": "保存", + "cancel": "キャンセル", + "edit": "編集" + }, "tasks": { "canceled": "タスクエラー:ユーザーによって停止およびキャンセルされました。", "deleted": "タスク失敗:ユーザーによって停止および削除されました。", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index f73eb22c2de..8e8c87d760a 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -19,8 +19,11 @@ "delete_config_profile": "이 구성 프로필을 삭제하시겠습니까?", "delete_custom_mode": "이 사용자 지정 모드를 삭제하시겠습니까?", "delete_message": "무엇을 삭제하시겠습니까?", - "just_this_message": "이 메시지만", - "this_and_subsequent": "이 메시지와 모든 후속 메시지" + "edit_message": "이 메시지 이후의 모든 메시지를 삭제하시겠습니까?", + "delete_just_this_message": "이 메시지만", + "edit_just_this_message": "아니요, 이것만 편집", + "delete_this_and_subsequent": "이 메시지와 모든 후속 메시지", + "edit_this_and_delete_subsequent": "예" }, "errors": { "invalid_data_uri": "잘못된 데이터 URI 형식", @@ -107,6 +110,11 @@ "remove": "제거", "keep": "유지" }, + "buttons": { + "save": "저장", + "cancel": "취소", + "edit": "편집" + }, "tasks": { "canceled": "작업 오류: 사용자에 의해 중지 및 취소되었습니다.", "deleted": "작업 실패: 사용자에 의해 중지 및 삭제되었습니다.", diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index 6958ace2888..48bf7b6fe3f 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -19,8 +19,11 @@ "delete_config_profile": "Weet je zeker dat je dit configuratieprofiel wilt verwijderen?", "delete_custom_mode": "Weet je zeker dat je deze aangepaste modus wilt verwijderen?", "delete_message": "Wat wil je verwijderen?", - "just_this_message": "Alleen dit bericht", - "this_and_subsequent": "Dit en alle volgende berichten" + "delete_just_this_message": "Alleen dit bericht", + "delete_this_and_subsequent": "Dit en alle volgende berichten", + "edit_message": "Alle berichten na dit bericht verwijderen?", + "edit_just_this_message": "Nee, alleen dit bericht bewerken", + "edit_this_and_delete_subsequent": "Ja" }, "errors": { "invalid_data_uri": "Ongeldig data-URI-formaat", @@ -107,6 +110,11 @@ "remove": "Verwijderen", "keep": "Behouden" }, + "buttons": { + "save": "Opslaan", + "cancel": "Annuleren", + "edit": "Bewerken" + }, "tasks": { "canceled": "Taakfout: gestopt en geannuleerd door gebruiker.", "deleted": "Taakfout: gestopt en verwijderd door gebruiker.", diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index d9f244cbe38..95a8a5f96fd 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -19,8 +19,11 @@ "delete_config_profile": "Czy na pewno chcesz usunąć ten profil konfiguracyjny?", "delete_custom_mode": "Czy na pewno chcesz usunąć ten niestandardowy tryb?", "delete_message": "Co chcesz usunąć?", - "just_this_message": "Tylko tę wiadomość", - "this_and_subsequent": "Tę i wszystkie kolejne wiadomości" + "delete_just_this_message": "Tylko tę wiadomość", + "delete_this_and_subsequent": "Tę i wszystkie kolejne wiadomości", + "edit_message": "Usunąć wszystkie wiadomości po tej?", + "edit_just_this_message": "Nie, tylko edytuj tę wiadomość", + "edit_this_and_delete_subsequent": "Tak" }, "errors": { "invalid_data_uri": "Nieprawidłowy format URI danych", @@ -107,6 +110,11 @@ "remove": "Usuń", "keep": "Zachowaj" }, + "buttons": { + "save": "Zapisz", + "cancel": "Anuluj", + "edit": "Edytuj" + }, "tasks": { "canceled": "Błąd zadania: Zostało zatrzymane i anulowane przez użytkownika.", "deleted": "Niepowodzenie zadania: Zostało zatrzymane i usunięte przez użytkownika.", diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index 9a9c3de30f1..87ecaf5359a 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -23,8 +23,11 @@ "delete_config_profile": "Tem certeza de que deseja excluir este perfil de configuração?", "delete_custom_mode": "Tem certeza de que deseja excluir este modo personalizado?", "delete_message": "O que você gostaria de excluir?", - "just_this_message": "Apenas esta mensagem", - "this_and_subsequent": "Esta e todas as mensagens subsequentes" + "delete_just_this_message": "Apenas esta mensagem", + "delete_this_and_subsequent": "Esta e todas as mensagens subsequentes", + "edit_message": "Excluir todas as mensagens após esta?", + "edit_just_this_message": "Não, apenas editar esta", + "edit_this_and_delete_subsequent": "Sim" }, "errors": { "invalid_data_uri": "Formato de URI de dados inválido", @@ -111,6 +114,11 @@ "remove": "Remover", "keep": "Manter" }, + "buttons": { + "save": "Salvar", + "cancel": "Cancelar", + "edit": "Editar" + }, "tasks": { "canceled": "Erro na tarefa: Foi interrompida e cancelada pelo usuário.", "deleted": "Falha na tarefa: Foi interrompida e excluída pelo usuário.", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index d598d832fb8..ab9a796b87e 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -19,8 +19,11 @@ "delete_config_profile": "Вы уверены, что хотите удалить этот профиль конфигурации?", "delete_custom_mode": "Вы уверены, что хотите удалить этот пользовательский режим?", "delete_message": "Что вы хотите удалить?", - "just_this_message": "Только это сообщение", - "this_and_subsequent": "Это и все последующие сообщения" + "delete_just_this_message": "Только это сообщение", + "delete_this_and_subsequent": "Это и все последующие сообщения", + "edit_message": "Удалить все сообщения после этого?", + "edit_just_this_message": "Нет, только редактировать это", + "edit_this_and_delete_subsequent": "Да" }, "errors": { "invalid_data_uri": "Неверный формат URI данных", @@ -107,6 +110,11 @@ "remove": "Удалить", "keep": "Оставить" }, + "buttons": { + "save": "Сохранить", + "cancel": "Отмена", + "edit": "Редактировать" + }, "tasks": { "canceled": "Ошибка задачи: Она была остановлена и отменена пользователем.", "deleted": "Сбой задачи: Она была остановлена и удалена пользователем.", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 5fa89fbba53..dc2f5c00724 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -19,8 +19,11 @@ "delete_config_profile": "Bu yapılandırma profilini silmek istediğinizden emin misiniz?", "delete_custom_mode": "Bu özel modu silmek istediğinizden emin misiniz?", "delete_message": "Neyi silmek istersiniz?", - "just_this_message": "Sadece bu mesajı", - "this_and_subsequent": "Bu ve sonraki tüm mesajları" + "delete_just_this_message": "Sadece bu mesajı", + "delete_this_and_subsequent": "Bu ve sonraki tüm mesajları", + "edit_message": "Bu mesajdan sonraki tüm mesajlar silinsin mi?", + "edit_just_this_message": "Hayır, sadece bunu düzenle", + "edit_this_and_delete_subsequent": "Evet" }, "errors": { "invalid_data_uri": "Geçersiz veri URI formatı", @@ -107,6 +110,11 @@ "remove": "Kaldır", "keep": "Koru" }, + "buttons": { + "save": "Kaydet", + "cancel": "İptal", + "edit": "Düzenle" + }, "tasks": { "canceled": "Görev hatası: Kullanıcı tarafından durduruldu ve iptal edildi.", "deleted": "Görev başarısız: Kullanıcı tarafından durduruldu ve silindi.", diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 7f9ec5dc20c..ab73225c5fb 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -19,8 +19,11 @@ "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": "Bạn có chắc chắn muốn xóa chế độ tùy chỉnh này không?", "delete_message": "Bạn muốn xóa gì?", - "just_this_message": "Chỉ tin nhắn này", - "this_and_subsequent": "Tin nhắn này và tất cả tin nhắn tiếp theo" + "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_message": "Xóa tất cả tin nhắn sau tin nhắn này?", + "edit_just_this_message": "Không, chỉ chỉnh sửa tin nhắn này", + "edit_this_and_delete_subsequent": "Có" }, "errors": { "invalid_data_uri": "Định dạng URI dữ liệu không hợp lệ", @@ -107,6 +110,11 @@ "remove": "Xóa", "keep": "Giữ" }, + "buttons": { + "save": "Lưu", + "cancel": "Hủy", + "edit": "Chỉnh sửa" + }, "tasks": { "canceled": "Lỗi nhiệm vụ: Nó đã bị dừng và hủy bởi người dùng.", "deleted": "Nhiệm vụ thất bại: Nó đã bị dừng và xóa bởi người dùng.", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 1b0a2729933..e4c2a150712 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -19,8 +19,11 @@ "delete_config_profile": "您确定要删除此配置文件吗?", "delete_custom_mode": "您确定要删除此自定义模式吗?", "delete_message": "您想删除什么?", - "just_this_message": "仅此消息", - "this_and_subsequent": "此消息及所有后续消息" + "edit_message": "删除此消息后的所有消息?", + "delete_just_this_message": "仅此消息", + "edit_just_this_message": "不,仅编辑此消息", + "delete_this_and_subsequent": "此消息及所有后续消息", + "edit_this_and_delete_subsequent": "是" }, "errors": { "invalid_mcp_config": "项目MCP配置格式无效", @@ -112,6 +115,11 @@ "remove": "删除", "keep": "保留" }, + "buttons": { + "save": "保存", + "cancel": "取消", + "edit": "编辑" + }, "tasks": { "canceled": "任务错误:它已被用户停止并取消。", "deleted": "任务失败:它已被用户停止并删除。", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index c4b90ef1a57..8a8d512deb5 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -19,8 +19,11 @@ "delete_config_profile": "您確定要刪除此設定檔案嗎?", "delete_custom_mode": "您確定要刪除此自訂模式嗎?", "delete_message": "您想刪除哪些內容?", - "just_this_message": "僅這則訊息", - "this_and_subsequent": "這則訊息及所有後續訊息" + "edit_message": "刪除此訊息後的所有訊息?", + "delete_just_this_message": "僅這則訊息", + "edit_just_this_message": "否,僅編輯此訊息", + "delete_this_and_subsequent": "這則訊息及所有後續訊息", + "edit_this_and_delete_subsequent": "是" }, "errors": { "invalid_data_uri": "資料 URI 格式無效", @@ -107,6 +110,11 @@ "remove": "刪除", "keep": "保留" }, + "buttons": { + "save": "儲存", + "cancel": "取消", + "edit": "編輯" + }, "tasks": { "canceled": "工作錯誤:它已被使用者停止並取消。", "deleted": "工作失敗:它已被使用者停止並刪除。", diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 0f1d22c3c73..2621980fd00 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -103,6 +103,7 @@ export interface WebviewMessage { | "enhancedPrompt" | "draggedImages" | "deleteMessage" + | "submitEditedMessage" | "terminalOutputLineLimit" | "terminalShellIntegrationTimeout" | "terminalShellIntegrationDisabled" @@ -184,6 +185,7 @@ export interface WebviewMessage { | "checkRulesDirectory" | "checkRulesDirectoryResult" text?: string + editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account" disabled?: boolean dataUri?: string diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index f61eb8c5e02..f2803b6b54d 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -108,6 +108,8 @@ export const ChatRowContent = ({ 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 { copyWithFeedback } = useCopyToClipboard() // Memoized callback to prevent re-renders caused by inline arrow functions @@ -115,6 +117,31 @@ export const ChatRowContent = ({ onToggleExpand(message.ts) }, [onToggleExpand, message.ts]) + // Handle edit button click + const handleEditClick = useCallback(() => { + setIsEditing(true) + setEditedContent(message.text || "") + // Edit mode is now handled entirely in the frontend + // No need to notify the backend + }, [message.text]) + + // Handle cancel edit + const handleCancelEdit = useCallback(() => { + setIsEditing(false) + setEditedContent(message.text || "") + }, [message.text]) + + // Handle save edit + const handleSaveEdit = useCallback(() => { + setIsEditing(false) + // Send edited message to backend + vscode.postMessage({ + type: "submitEditedMessage", + value: message.ts, + editedMessageContent: editedContent, + }) + }, [message.ts, editedContent]) + const [cost, apiReqCancelReason, apiReqStreamingFailedMessage] = useMemo(() => { if (message.text !== null && message.text !== undefined && message.say === "api_req_started") { const info = safeJsonParse(message.text) @@ -983,23 +1010,56 @@ export const ChatRowContent = ({ case "user_feedback": return (
-
-
- + {isEditing ? ( +
+