diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 375de1cd891..cd41ce09ce9 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -1332,19 +1332,11 @@ describe("ClineProvider", () => { text: "Edited message content", }) - // Verify correct messages were kept (only messages before the edited one) - expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([ - mockMessages[0], - mockMessages[1], - mockMessages[2], - ]) + // Verify correct messages were kept - delete from the preceding user message to truly replace it + expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([]) - // Verify correct API messages were kept (only messages before the edited one) - expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([ - mockApiHistory[0], - mockApiHistory[1], - mockApiHistory[2], - ]) + // Verify correct API messages were kept + expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([]) // The new flow calls webviewMessageHandler recursively with askResponse // We need to verify the recursive call happened by checking if the handler was called again @@ -3016,7 +3008,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => { mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }, { ts: 3000 }] as any[] mockCline.overwriteClineMessages = vi.fn() mockCline.overwriteApiConversationHistory = vi.fn() - mockCline.handleWebviewAskResponse = vi.fn() + mockCline.submitUserMessage = vi.fn() await provider.addClineToStack(mockCline) ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({ @@ -3046,9 +3038,11 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => { text: "Edited message with preserved images", }) - // Verify messages were edited correctly - messages up to the edited message should remain - expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0], mockMessages[1]]) - expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([{ ts: 1000 }, { ts: 2000 }]) + // Verify messages were edited correctly - the ORIGINAL user message and all subsequent messages are removed + expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0]]) + expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([{ ts: 1000 }]) + // Verify submitUserMessage was called with the edited content + expect(mockCline.submitUserMessage).toHaveBeenCalledWith("Edited message with preserved images", undefined) }) test("handles editing messages with file attachments", async () => { @@ -3070,7 +3064,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => { mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }, { ts: 3000 }] as any[] mockCline.overwriteClineMessages = vi.fn() mockCline.overwriteApiConversationHistory = vi.fn() - mockCline.handleWebviewAskResponse = vi.fn() + mockCline.submitUserMessage = vi.fn() await provider.addClineToStack(mockCline) ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({ @@ -3101,11 +3095,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => { }) expect(mockCline.overwriteClineMessages).toHaveBeenCalled() - expect(mockCline.handleWebviewAskResponse).toHaveBeenCalledWith( - "messageResponse", - "Edited message with file attachment", - undefined, - ) + expect(mockCline.submitUserMessage).toHaveBeenCalledWith("Edited message with file attachment", undefined) }) }) @@ -3197,7 +3187,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => { 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") + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.message.error_editing_message") }) }) @@ -3320,7 +3310,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => { text: "Edited message", }) - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Error editing message: Unauthorized") + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.message.error_editing_message") }) describe("Malformed Requests and Invalid Formats", () => { @@ -3544,7 +3534,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => { // Verify cleanup was attempted before failure expect(cleanupSpy).toHaveBeenCalled() - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("Error editing message: Operation failed") + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.message.error_editing_message") }) test("validates proper cleanup during failed delete operations", async () => { @@ -3584,9 +3574,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => { // Verify cleanup was attempted before failure expect(cleanupSpy).toHaveBeenCalled() - expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( - "Error deleting message: Delete operation failed", - ) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("errors.message.error_deleting_message") }) }) @@ -3609,7 +3597,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => { mockCline.apiConversationHistory = [{ ts: 1000 }, { ts: 2000 }] as any[] mockCline.overwriteClineMessages = vi.fn() mockCline.overwriteApiConversationHistory = vi.fn() - mockCline.handleWebviewAskResponse = vi.fn() + mockCline.submitUserMessage = vi.fn() await provider.addClineToStack(mockCline) ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({ @@ -3638,11 +3626,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => { await messageHandler({ type: "editMessageConfirm", messageTs: 2000, text: largeEditedContent }) expect(mockCline.overwriteClineMessages).toHaveBeenCalled() - expect(mockCline.handleWebviewAskResponse).toHaveBeenCalledWith( - "messageResponse", - largeEditedContent, - undefined, - ) + expect(mockCline.submitUserMessage).toHaveBeenCalledWith(largeEditedContent, undefined) }) test("handles deleting messages with large payloads", async () => { @@ -3822,7 +3806,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => { ] as any[] mockCline.overwriteClineMessages = vi.fn() mockCline.overwriteApiConversationHistory = vi.fn() - mockCline.handleWebviewAskResponse = vi.fn() + mockCline.submitUserMessage = vi.fn() await provider.addClineToStack(mockCline) ;(provider as any).getTaskWithId = vi.fn().mockResolvedValue({ @@ -3855,7 +3839,7 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => { // Should handle future timestamps correctly expect(mockCline.overwriteClineMessages).toHaveBeenCalled() - expect(mockCline.handleWebviewAskResponse).toHaveBeenCalled() + expect(mockCline.submitUserMessage).toHaveBeenCalled() }) }) }) diff --git a/src/core/webview/__tests__/webviewMessageHandler.delete.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.delete.spec.ts new file mode 100644 index 00000000000..28f6ba9cf88 --- /dev/null +++ b/src/core/webview/__tests__/webviewMessageHandler.delete.spec.ts @@ -0,0 +1,245 @@ +import { describe, it, expect, beforeEach, vi } from "vitest" +import { webviewMessageHandler } from "../webviewMessageHandler" +import * as vscode from "vscode" +import { ClineProvider } from "../ClineProvider" + +// Mock the saveTaskMessages function +vi.mock("../../task-persistence", () => ({ + saveTaskMessages: vi.fn(), +})) + +// Mock the i18n module +vi.mock("../../../i18n", () => ({ + t: vi.fn((key: string) => key), + changeLanguage: vi.fn(), +})) + +vi.mock("vscode", () => ({ + window: { + showErrorMessage: vi.fn(), + showWarningMessage: vi.fn(), + showInformationMessage: vi.fn(), + }, + workspace: { + workspaceFolders: undefined, + getConfiguration: vi.fn(() => ({ + get: vi.fn(), + update: vi.fn(), + })), + }, + ConfigurationTarget: { + Global: 1, + Workspace: 2, + WorkspaceFolder: 3, + }, + Uri: { + parse: vi.fn((str) => ({ toString: () => str })), + file: vi.fn((path) => ({ fsPath: path })), + }, + env: { + openExternal: vi.fn(), + clipboard: { + writeText: vi.fn(), + }, + }, + commands: { + executeCommand: vi.fn(), + }, +})) + +describe("webviewMessageHandler delete functionality", () => { + let provider: any + let getCurrentTaskMock: any + + beforeEach(() => { + // Reset all mocks + vi.clearAllMocks() + + // Create mock task + getCurrentTaskMock = { + clineMessages: [], + apiConversationHistory: [], + overwriteClineMessages: vi.fn(async () => {}), + overwriteApiConversationHistory: vi.fn(async () => {}), + taskId: "test-task-id", + } + + // Create mock provider + provider = { + getCurrentTask: vi.fn(() => getCurrentTaskMock), + postMessageToWebview: vi.fn(), + contextProxy: { + getValue: vi.fn(), + setValue: vi.fn(async () => {}), + globalStorageUri: { fsPath: "/test/path" }, + }, + log: vi.fn(), + cwd: "/test/cwd", + } + }) + + describe("handleDeleteMessageConfirm", () => { + it("should handle deletion when apiConversationHistoryIndex is -1 (message not in API history)", async () => { + // Setup test data with a user message and assistant response + const userMessageTs = 1000 + const assistantMessageTs = 1001 + + getCurrentTaskMock.clineMessages = [ + { ts: userMessageTs, say: "user", text: "Hello" }, + { ts: assistantMessageTs, say: "assistant", text: "Hi there" }, + ] + + // API history has the assistant message but not the user message + // This simulates the case where the user message wasn't in API history + getCurrentTaskMock.apiConversationHistory = [ + { ts: assistantMessageTs, role: "assistant", content: { type: "text", text: "Hi there" } }, + { + ts: 1002, + role: "assistant", + content: { type: "text", text: "attempt_completion" }, + name: "attempt_completion", + }, + ] + + // Call delete for the user message + await webviewMessageHandler(provider, { + type: "deleteMessageConfirm", + messageTs: userMessageTs, + }) + + // Verify that clineMessages was truncated at the correct index + expect(getCurrentTaskMock.overwriteClineMessages).toHaveBeenCalledWith([]) + + // When message is not found in API history (index is -1), + // API history should be truncated from the first API message at/after the deleted timestamp (fallback) + expect(getCurrentTaskMock.overwriteApiConversationHistory).toHaveBeenCalledWith([]) + }) + + it("should handle deletion when exact apiConversationHistoryIndex is found", async () => { + // Setup test data where message exists in both arrays + const messageTs = 1000 + + getCurrentTaskMock.clineMessages = [ + { ts: 900, say: "user", text: "Previous message" }, + { ts: messageTs, say: "user", text: "Delete this" }, + { ts: 1100, say: "assistant", text: "Response" }, + ] + + getCurrentTaskMock.apiConversationHistory = [ + { ts: 900, role: "user", content: { type: "text", text: "Previous message" } }, + { ts: messageTs, role: "user", content: { type: "text", text: "Delete this" } }, + { ts: 1100, role: "assistant", content: { type: "text", text: "Response" } }, + ] + + // Call delete + await webviewMessageHandler(provider, { + type: "deleteMessageConfirm", + messageTs: messageTs, + }) + + // Verify truncation at correct indices + expect(getCurrentTaskMock.overwriteClineMessages).toHaveBeenCalledWith([ + { ts: 900, say: "user", text: "Previous message" }, + ]) + + expect(getCurrentTaskMock.overwriteApiConversationHistory).toHaveBeenCalledWith([ + { ts: 900, role: "user", content: { type: "text", text: "Previous message" } }, + ]) + }) + + it("should handle deletion when message not found in clineMessages", async () => { + getCurrentTaskMock.clineMessages = [{ ts: 1000, say: "user", text: "Some message" }] + + getCurrentTaskMock.apiConversationHistory = [] + + // Call delete with non-existent timestamp + await webviewMessageHandler(provider, { + type: "deleteMessageConfirm", + messageTs: 9999, + }) + + // Verify error message was shown (expecting translation key since t() is mocked to return the key) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.message.message_not_found") + + // Verify no truncation occurred + expect(getCurrentTaskMock.overwriteClineMessages).not.toHaveBeenCalled() + expect(getCurrentTaskMock.overwriteApiConversationHistory).not.toHaveBeenCalled() + }) + + it("should handle deletion with attempt_completion in API history", async () => { + // Setup test data with attempt_completion + const userMessageTs = 1000 + const attemptCompletionTs = 1001 + + getCurrentTaskMock.clineMessages = [ + { ts: userMessageTs, say: "user", text: "Fix the bug" }, + { ts: attemptCompletionTs, say: "assistant", text: "I've fixed the bug" }, + ] + + // API history has attempt_completion but user message is missing + getCurrentTaskMock.apiConversationHistory = [ + { + ts: attemptCompletionTs, + role: "assistant", + content: { + type: "text", + text: "I've fixed the bug in the code", + }, + name: "attempt_completion", + }, + { + ts: 1002, + role: "user", + content: { type: "text", text: "Looks good, but..." }, + }, + ] + + // Call delete for the user message + await webviewMessageHandler(provider, { + type: "deleteMessageConfirm", + messageTs: userMessageTs, + }) + + // Verify that clineMessages was truncated + expect(getCurrentTaskMock.overwriteClineMessages).toHaveBeenCalledWith([]) + + // API history should be truncated from first message at/after deleted timestamp (fallback) + expect(getCurrentTaskMock.overwriteApiConversationHistory).toHaveBeenCalledWith([]) + }) + + it("should preserve messages before the deleted one", async () => { + const messageTs = 2000 + + getCurrentTaskMock.clineMessages = [ + { ts: 1000, say: "user", text: "First message" }, + { ts: 1500, say: "assistant", text: "First response" }, + { ts: messageTs, say: "user", text: "Delete this" }, + { ts: 2500, say: "assistant", text: "Response to delete" }, + ] + + getCurrentTaskMock.apiConversationHistory = [ + { ts: 1000, role: "user", content: { type: "text", text: "First message" } }, + { ts: 1500, role: "assistant", content: { type: "text", text: "First response" } }, + { ts: messageTs, role: "user", content: { type: "text", text: "Delete this" } }, + { ts: 2500, role: "assistant", content: { type: "text", text: "Response to delete" } }, + ] + + await webviewMessageHandler(provider, { + type: "deleteMessageConfirm", + messageTs: messageTs, + }) + + // Should preserve messages before the deleted one + expect(getCurrentTaskMock.overwriteClineMessages).toHaveBeenCalledWith([ + { ts: 1000, say: "user", text: "First message" }, + { ts: 1500, say: "assistant", text: "First response" }, + ]) + + // API history should be truncated at the exact index + expect(getCurrentTaskMock.overwriteApiConversationHistory).toHaveBeenCalledWith([ + { ts: 1000, role: "user", content: { type: "text", text: "First message" } }, + { ts: 1500, role: "assistant", content: { type: "text", text: "First response" } }, + ]) + }) + }) +}) diff --git a/src/core/webview/__tests__/webviewMessageHandler.edit.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.edit.spec.ts new file mode 100644 index 00000000000..d467f5cd92d --- /dev/null +++ b/src/core/webview/__tests__/webviewMessageHandler.edit.spec.ts @@ -0,0 +1,390 @@ +import type { Mock } from "vitest" +import { describe, it, expect, vi, beforeEach } from "vitest" + +// Mock dependencies first +vi.mock("vscode", () => ({ + window: { + showWarningMessage: vi.fn(), + showErrorMessage: vi.fn(), + }, + workspace: { + workspaceFolders: [{ uri: { fsPath: "/mock/workspace" } }], + getConfiguration: vi.fn().mockReturnValue({ + get: vi.fn(), + update: vi.fn(), + }), + }, + Uri: { + file: vi.fn((path) => ({ fsPath: path })), + }, + env: { + uriScheme: "vscode", + }, +})) + +vi.mock("../../task-persistence", () => ({ + saveTaskMessages: vi.fn(), +})) + +vi.mock("../../../api/providers/fetchers/modelCache", () => ({ + getModels: vi.fn(), + flushModels: vi.fn(), +})) + +vi.mock("../checkpointRestoreHandler", () => ({ + handleCheckpointRestoreOperation: vi.fn(), +})) + +// Import after mocks +import { webviewMessageHandler } from "../webviewMessageHandler" +import type { ClineProvider } from "../ClineProvider" +import type { ClineMessage } from "@roo-code/types" +import type { ApiMessage } from "../../task-persistence/apiMessages" + +describe("webviewMessageHandler - Edit Message with Timestamp Fallback", () => { + let mockClineProvider: ClineProvider + let mockCurrentTask: any + + beforeEach(() => { + vi.clearAllMocks() + + // Create a mock task with messages + mockCurrentTask = { + taskId: "test-task-id", + clineMessages: [] as ClineMessage[], + apiConversationHistory: [] as ApiMessage[], + overwriteClineMessages: vi.fn(), + overwriteApiConversationHistory: vi.fn(), + handleWebviewAskResponse: vi.fn(), + } + + // Create mock provider + mockClineProvider = { + getCurrentTask: vi.fn().mockReturnValue(mockCurrentTask), + postMessageToWebview: vi.fn(), + contextProxy: { + getValue: vi.fn(), + setValue: vi.fn(), + globalStorageUri: { fsPath: "/mock/storage" }, + }, + log: vi.fn(), + } as unknown as ClineProvider + }) + + it("should not modify API history when apiConversationHistoryIndex is -1", async () => { + // Setup: User message followed by attempt_completion + const userMessageTs = 1000 + const assistantMessageTs = 2000 + const completionMessageTs = 3000 + + // UI messages (clineMessages) + mockCurrentTask.clineMessages = [ + { + ts: userMessageTs, + type: "say", + say: "user_feedback", + text: "Hello", + } as ClineMessage, + { + ts: completionMessageTs, + type: "say", + say: "completion_result", + text: "Task Completed!", + } as ClineMessage, + ] + + // API conversation history - note the user message is missing (common scenario after condense) + mockCurrentTask.apiConversationHistory = [ + { + ts: assistantMessageTs, + role: "assistant", + content: [ + { + type: "text", + text: "I'll help you with that.", + }, + ], + }, + { + ts: completionMessageTs, + role: "assistant", + content: [ + { + type: "tool_use", + name: "attempt_completion", + id: "tool-1", + input: { + result: "Task Completed!", + }, + }, + ], + }, + ] as ApiMessage[] + + // Trigger edit confirmation + await webviewMessageHandler(mockClineProvider, { + type: "editMessageConfirm", + messageTs: userMessageTs, + text: "Hello World", // edited content + restoreCheckpoint: false, + }) + + // Verify that UI messages were truncated at the correct index + expect(mockCurrentTask.overwriteClineMessages).toHaveBeenCalledWith( + [], // All messages before index 0 (empty array) + ) + + // API history should be truncated from first message at/after edited timestamp (fallback) + expect(mockCurrentTask.overwriteApiConversationHistory).toHaveBeenCalledWith([]) + }) + + it("should preserve messages before the edited message when message not in API history", async () => { + const earlierMessageTs = 500 + const userMessageTs = 1000 + const assistantMessageTs = 2000 + + // UI messages + mockCurrentTask.clineMessages = [ + { + ts: earlierMessageTs, + type: "say", + say: "user_feedback", + text: "Earlier message", + } as ClineMessage, + { + ts: userMessageTs, + type: "say", + say: "user_feedback", + text: "Hello", + } as ClineMessage, + { + ts: assistantMessageTs, + type: "say", + say: "text", + text: "Response", + } as ClineMessage, + ] + + // API history - missing the exact user message at ts=1000 + mockCurrentTask.apiConversationHistory = [ + { + ts: earlierMessageTs, + role: "user", + content: [{ type: "text", text: "Earlier message" }], + }, + { + ts: assistantMessageTs, + role: "assistant", + content: [{ type: "text", text: "Response" }], + }, + ] as ApiMessage[] + + await webviewMessageHandler(mockClineProvider, { + type: "editMessageConfirm", + messageTs: userMessageTs, + text: "Hello World", + restoreCheckpoint: false, + }) + + // Verify UI messages were truncated to preserve earlier message + expect(mockCurrentTask.overwriteClineMessages).toHaveBeenCalledWith([ + { + ts: earlierMessageTs, + type: "say", + say: "user_feedback", + text: "Earlier message", + }, + ]) + + // API history should be truncated from the first API message at/after the edited timestamp (fallback) + expect(mockCurrentTask.overwriteApiConversationHistory).toHaveBeenCalledWith([ + { + ts: earlierMessageTs, + role: "user", + content: [{ type: "text", text: "Earlier message" }], + }, + ]) + }) + + it("should not use fallback when exact apiConversationHistoryIndex is found", async () => { + const userMessageTs = 1000 + const assistantMessageTs = 2000 + + // Both UI and API have the message at the same timestamp + mockCurrentTask.clineMessages = [ + { + ts: userMessageTs, + type: "say", + say: "user_feedback", + text: "Hello", + } as ClineMessage, + { + ts: assistantMessageTs, + type: "say", + say: "text", + text: "Response", + } as ClineMessage, + ] + + mockCurrentTask.apiConversationHistory = [ + { + ts: userMessageTs, + role: "user", + content: [{ type: "text", text: "Hello" }], + }, + { + ts: assistantMessageTs, + role: "assistant", + content: [{ type: "text", text: "Response" }], + }, + ] as ApiMessage[] + + await webviewMessageHandler(mockClineProvider, { + type: "editMessageConfirm", + messageTs: userMessageTs, + text: "Hello World", + restoreCheckpoint: false, + }) + + // Both should be truncated at index 0 + expect(mockCurrentTask.overwriteClineMessages).toHaveBeenCalledWith([]) + expect(mockCurrentTask.overwriteApiConversationHistory).toHaveBeenCalledWith([]) + }) + + it("should handle case where no API messages match timestamp criteria", async () => { + const userMessageTs = 3000 + + mockCurrentTask.clineMessages = [ + { + ts: userMessageTs, + type: "say", + say: "user_feedback", + text: "Hello", + } as ClineMessage, + ] + + // All API messages have timestamps before the edited message + mockCurrentTask.apiConversationHistory = [ + { + ts: 1000, + role: "assistant", + content: [{ type: "text", text: "Old message 1" }], + }, + { + ts: 2000, + role: "assistant", + content: [{ type: "text", text: "Old message 2" }], + }, + ] as ApiMessage[] + + await webviewMessageHandler(mockClineProvider, { + type: "editMessageConfirm", + messageTs: userMessageTs, + text: "Hello World", + restoreCheckpoint: false, + }) + + // UI messages truncated + expect(mockCurrentTask.overwriteClineMessages).toHaveBeenCalledWith([]) + + // API history should not be modified when no API messages meet the timestamp criteria + expect(mockCurrentTask.overwriteApiConversationHistory).not.toHaveBeenCalled() + }) + + it("should handle empty API conversation history gracefully", async () => { + const userMessageTs = 1000 + + mockCurrentTask.clineMessages = [ + { + ts: userMessageTs, + type: "say", + say: "user_feedback", + text: "Hello", + } as ClineMessage, + ] + + mockCurrentTask.apiConversationHistory = [] + + await webviewMessageHandler(mockClineProvider, { + type: "editMessageConfirm", + messageTs: userMessageTs, + text: "Hello World", + restoreCheckpoint: false, + }) + + // UI messages should be truncated + expect(mockCurrentTask.overwriteClineMessages).toHaveBeenCalledWith([]) + + // API history should not be modified when message not found + expect(mockCurrentTask.overwriteApiConversationHistory).not.toHaveBeenCalled() + }) + + it("should correctly handle attempt_completion in API history", async () => { + const userMessageTs = 1000 + const completionTs = 2000 + const feedbackTs = 3000 + + mockCurrentTask.clineMessages = [ + { + ts: userMessageTs, + type: "say", + say: "user_feedback", + text: "Do something", + } as ClineMessage, + { + ts: completionTs, + type: "say", + say: "completion_result", + text: "Task Completed!", + } as ClineMessage, + { + ts: feedbackTs, + type: "say", + say: "user_feedback", + text: "Thanks", + } as ClineMessage, + ] + + // API history with attempt_completion tool use (user message missing) + mockCurrentTask.apiConversationHistory = [ + { + ts: completionTs, + role: "assistant", + content: [ + { + type: "tool_use", + name: "attempt_completion", + id: "tool-1", + input: { + result: "Task Completed!", + }, + }, + ], + }, + { + ts: feedbackTs, + role: "user", + content: [ + { + type: "text", + text: "Thanks", + }, + ], + }, + ] as ApiMessage[] + + // Edit the first user message + await webviewMessageHandler(mockClineProvider, { + type: "editMessageConfirm", + messageTs: userMessageTs, + text: "Do something else", + restoreCheckpoint: false, + }) + + // UI messages truncated at edited message + expect(mockCurrentTask.overwriteClineMessages).toHaveBeenCalledWith([]) + + // API history should be truncated from first message at/after edited timestamp (fallback) + expect(mockCurrentTask.overwriteApiConversationHistory).toHaveBeenCalledWith([]) + }) +}) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 080fbbcd943..fec0b48c5c4 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -84,6 +84,17 @@ export const webviewMessageHandler = async ( return { messageIndex, apiConversationHistoryIndex } } + /** + * Fallback: find first API history index at or after a timestamp. + * Used when the exact user message isn't present in apiConversationHistory (e.g., after condense). + */ + const findFirstApiIndexAtOrAfter = (ts: number, currentCline: any) => { + if (typeof ts !== "number") return -1 + return currentCline.apiConversationHistory.findIndex( + (msg: ApiMessage) => typeof msg?.ts === "number" && (msg.ts as number) >= ts, + ) + } + /** * Removes the target message and all subsequent messages */ @@ -109,18 +120,20 @@ export const webviewMessageHandler = async ( // Check if there's a checkpoint before this message const currentCline = provider.getCurrentTask() let hasCheckpoint = false - if (currentCline) { - const { messageIndex } = findMessageIndices(messageTs, currentCline) - if (messageIndex !== -1) { - // Find the last checkpoint before this message - const checkpoints = currentCline.clineMessages.filter( - (msg) => msg.say === "checkpoint_saved" && msg.ts > messageTs, - ) - hasCheckpoint = checkpoints.length > 0 - } else { - console.log("[webviewMessageHandler] Message not found! Looking for ts:", messageTs) - } + if (!currentCline) { + await vscode.window.showErrorMessage(t("common:errors.message.no_active_task_to_delete")) + return + } + + const { messageIndex } = findMessageIndices(messageTs, currentCline) + + if (messageIndex !== -1) { + // Find the last checkpoint before this message + const checkpoints = currentCline.clineMessages.filter( + (msg) => msg.say === "checkpoint_saved" && msg.ts > messageTs, + ) + hasCheckpoint = checkpoints.length > 0 } // Send message to webview to show delete confirmation dialog @@ -142,11 +155,15 @@ export const webviewMessageHandler = async ( } const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline) + // Determine API truncation index with timestamp fallback if exact match not found + let apiIndexToUse = apiConversationHistoryIndex + const tsThreshold = currentCline.clineMessages[messageIndex]?.ts + if (apiIndexToUse === -1 && typeof tsThreshold === "number") { + apiIndexToUse = findFirstApiIndexAtOrAfter(tsThreshold, currentCline) + } if (messageIndex === -1) { - const errorMessage = `Message with timestamp ${messageTs} not found` - console.error("[handleDeleteMessageConfirm]", errorMessage) - await vscode.window.showErrorMessage(errorMessage) + await vscode.window.showErrorMessage(t("common:errors.message.message_not_found", { messageTs })) return } @@ -188,7 +205,7 @@ export const webviewMessageHandler = async ( } // Delete this message and all subsequent messages - await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex) + await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiIndexToUse) // Restore checkpoint associations for preserved messages for (const [ts, checkpoint] of preservedCheckpoints) { @@ -204,11 +221,16 @@ export const webviewMessageHandler = async ( taskId: currentCline.taskId, globalStoragePath: provider.contextProxy.globalStorageUri.fsPath, }) + + // Update the UI to reflect the deletion + await provider.postStateToWebview() } } catch (error) { console.error("Error in delete message:", error) vscode.window.showErrorMessage( - `Error deleting message: ${error instanceof Error ? error.message : String(error)}`, + t("common:errors.message.error_deleting_message", { + error: error instanceof Error ? error.message : String(error), + }), ) } } @@ -265,7 +287,7 @@ export const webviewMessageHandler = async ( const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline) if (messageIndex === -1) { - const errorMessage = `Message with timestamp ${messageTs} not found` + const errorMessage = t("common:errors.message.message_not_found", { messageTs }) console.error("[handleEditMessageConfirm]", errorMessage) await vscode.window.showErrorMessage(errorMessage) return @@ -308,18 +330,49 @@ export const webviewMessageHandler = async ( } } - // For non-checkpoint edits, preserve checkpoint associations for remaining messages + // For non-checkpoint edits, remove the ORIGINAL user message being edited and all subsequent messages + // Determine the correct starting index to delete from (prefer the last preceding user_feedback message) + let deleteFromMessageIndex = messageIndex + let deleteFromApiIndex = apiConversationHistoryIndex + + // Find the nearest preceding user message to ensure we replace the original, not just the assistant reply + for (let i = messageIndex; i >= 0; i--) { + const m = currentCline.clineMessages[i] + if (m?.say === "user_feedback") { + deleteFromMessageIndex = i + // Align API history truncation to the same user message timestamp if present + const userTs = m.ts + if (typeof userTs === "number") { + const apiIdx = currentCline.apiConversationHistory.findIndex( + (am: ApiMessage) => am.ts === userTs, + ) + if (apiIdx !== -1) { + deleteFromApiIndex = apiIdx + } + } + break + } + } + + // Timestamp fallback for API history when exact user message isn't present + if (deleteFromApiIndex === -1) { + const tsThresholdForEdit = currentCline.clineMessages[deleteFromMessageIndex]?.ts + if (typeof tsThresholdForEdit === "number") { + deleteFromApiIndex = findFirstApiIndexAtOrAfter(tsThresholdForEdit, currentCline) + } + } + // Store checkpoints from messages that will be preserved const preservedCheckpoints = new Map() - for (let i = 0; i < messageIndex; i++) { + for (let i = 0; i < deleteFromMessageIndex; i++) { const msg = currentCline.clineMessages[i] if (msg?.checkpoint && msg.ts) { preservedCheckpoints.set(msg.ts, msg.checkpoint) } } - // Edit this message and delete subsequent - await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex) + // Delete the original (user) message and all subsequent messages + await removeMessagesThisAndSubsequent(currentCline, deleteFromMessageIndex, deleteFromApiIndex) // Restore checkpoint associations for preserved messages for (const [ts, checkpoint] of preservedCheckpoints) { @@ -336,20 +389,16 @@ export const webviewMessageHandler = async ( globalStoragePath: provider.contextProxy.globalStorageUri.fsPath, }) - // Process the edited message as a regular user message - webviewMessageHandler(provider, { - type: "askResponse", - askResponse: "messageResponse", - text: editedContent, - images, - }) + // Update the UI to reflect the deletion + await provider.postStateToWebview() - // Don't initialize with history item for edit operations - // The webviewMessageHandler will handle the conversation state + await currentCline.submitUserMessage(editedContent, images) } catch (error) { console.error("Error in edit message:", error) vscode.window.showErrorMessage( - `Error editing message: ${error instanceof Error ? error.message : String(error)}`, + t("common:errors.message.error_editing_message", { + error: error instanceof Error ? error.message : String(error), + }), ) } } @@ -1451,9 +1500,17 @@ export const webviewMessageHandler = async ( } break case "deleteMessage": { - if (provider.getCurrentTask() && typeof message.value === "number" && message.value) { - await handleMessageModificationsOperation(message.value, "delete") + if (!provider.getCurrentTask()) { + await vscode.window.showErrorMessage(t("common:errors.message.no_active_task_to_delete")) + break + } + + if (typeof message.value !== "number" || !message.value) { + await vscode.window.showErrorMessage(t("common:errors.message.invalid_timestamp_for_deletion")) + break } + + await handleMessageModificationsOperation(message.value, "delete") break } case "submitEditedMessage": { @@ -1841,9 +1898,17 @@ export const webviewMessageHandler = async ( } break case "deleteMessageConfirm": - if (message.messageTs) { - await handleDeleteMessageConfirm(message.messageTs, message.restoreCheckpoint) + if (!message.messageTs) { + await vscode.window.showErrorMessage(t("common:errors.message.cannot_delete_missing_timestamp")) + break + } + + if (typeof message.messageTs !== "number") { + await vscode.window.showErrorMessage(t("common:errors.message.cannot_delete_invalid_timestamp")) + break } + + await handleDeleteMessageConfirm(message.messageTs, message.restoreCheckpoint) break case "editMessageConfirm": if (message.messageTs && message.text) { diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 583f82693a6..bfc86d83f3a 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -90,6 +90,15 @@ "apiKeyModelPlanMismatch": "Les claus API i els plans de subscripció permeten models diferents. Assegura't que el model seleccionat estigui inclòs al teu pla.", "notFound": "No s'ha trobat l'executable Claude Code '{{claudePath}}'.\n\nInstal·la Claude Code CLI:\n1. Visita {{installationUrl}} per descarregar Claude Code\n2. Segueix les instruccions d'instal·lació per al teu sistema operatiu\n3. Assegura't que la comanda 'claude' estigui disponible al teu PATH\n4. Alternativament, configura una ruta personalitzada a la configuració de Roo sota 'Ruta de Claude Code'\n\nError original: {{originalError}}" }, + "message": { + "no_active_task_to_delete": "No hi ha cap tasca activa de la qual eliminar missatges", + "invalid_timestamp_for_deletion": "Marca de temps del missatge no vàlida per a l'eliminació", + "cannot_delete_missing_timestamp": "No es pot eliminar el missatge: falta la marca de temps", + "cannot_delete_invalid_timestamp": "No es pot eliminar el missatge: marca de temps no vàlida", + "message_not_found": "Missatge amb marca de temps {{messageTs}} no trobat", + "error_deleting_message": "Error eliminant missatge: {{error}}", + "error_editing_message": "Error editant missatge: {{error}}" + }, "gemini": { "generate_stream": "Error del flux de context de generació de Gemini: {{error}}", "generate_complete_prompt": "Error de finalització de Gemini: {{error}}", diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 2d1a2778edc..d522bc540ca 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -87,6 +87,15 @@ "apiKeyModelPlanMismatch": "API-Schlüssel und Abonnement-Pläne erlauben verschiedene Modelle. Stelle sicher, dass das ausgewählte Modell in deinem Plan enthalten ist.", "notFound": "Claude Code ausführbare Datei '{{claudePath}}' nicht gefunden.\n\nBitte installiere Claude Code CLI:\n1. Besuche {{installationUrl}} um Claude Code herunterzuladen\n2. Folge den Installationsanweisungen für dein Betriebssystem\n3. Stelle sicher, dass der 'claude' Befehl in deinem PATH verfügbar ist\n4. Alternativ konfiguriere einen benutzerdefinierten Pfad in den Roo-Einstellungen unter 'Claude Code Pfad'\n\nUrsprünglicher Fehler: {{originalError}}" }, + "message": { + "no_active_task_to_delete": "Keine aktive Aufgabe, aus der Nachrichten gelöscht werden können", + "invalid_timestamp_for_deletion": "Ungültiger Nachrichten-Zeitstempel zum Löschen", + "cannot_delete_missing_timestamp": "Nachricht kann nicht gelöscht werden: fehlender Zeitstempel", + "cannot_delete_invalid_timestamp": "Nachricht kann nicht gelöscht werden: ungültiger Zeitstempel", + "message_not_found": "Nachricht mit Zeitstempel {{messageTs}} nicht gefunden", + "error_deleting_message": "Fehler beim Löschen der Nachricht: {{error}}", + "error_editing_message": "Fehler beim Bearbeiten der Nachricht: {{error}}" + }, "gemini": { "generate_stream": "Fehler beim Generieren des Kontext-Streams von Gemini: {{error}}", "generate_complete_prompt": "Fehler bei der Vervollständigung durch Gemini: {{error}}", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 40a897ceb31..de36a11a8a4 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -87,6 +87,15 @@ "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.", "notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}" }, + "message": { + "no_active_task_to_delete": "No active task to delete messages from", + "invalid_timestamp_for_deletion": "Invalid message timestamp for deletion", + "cannot_delete_missing_timestamp": "Cannot delete message: missing timestamp", + "cannot_delete_invalid_timestamp": "Cannot delete message: invalid timestamp", + "message_not_found": "Message with timestamp {{messageTs}} not found", + "error_deleting_message": "Error deleting message: {{error}}", + "error_editing_message": "Error editing message: {{error}}" + }, "gemini": { "generate_stream": "Gemini generate context stream error: {{error}}", "generate_complete_prompt": "Gemini completion error: {{error}}", diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 0b7c7e12ee8..79190378542 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -87,6 +87,15 @@ "apiKeyModelPlanMismatch": "Las claves API y los planes de suscripción permiten diferentes modelos. Asegúrate de que el modelo seleccionado esté incluido en tu plan.", "notFound": "Ejecutable de Claude Code '{{claudePath}}' no encontrado.\n\nPor favor instala Claude Code CLI:\n1. Visita {{installationUrl}} para descargar Claude Code\n2. Sigue las instrucciones de instalación para tu sistema operativo\n3. Asegúrate de que el comando 'claude' esté disponible en tu PATH\n4. Alternativamente, configura una ruta personalizada en la configuración de Roo bajo 'Ruta de Claude Code'\n\nError original: {{originalError}}" }, + "message": { + "no_active_task_to_delete": "No hay tarea activa de la cual eliminar mensajes", + "invalid_timestamp_for_deletion": "Marca de tiempo del mensaje no válida para eliminación", + "cannot_delete_missing_timestamp": "No se puede eliminar el mensaje: falta marca de tiempo", + "cannot_delete_invalid_timestamp": "No se puede eliminar el mensaje: marca de tiempo no válida", + "message_not_found": "Mensaje con marca de tiempo {{messageTs}} no encontrado", + "error_deleting_message": "Error eliminando mensaje: {{error}}", + "error_editing_message": "Error editando mensaje: {{error}}" + }, "gemini": { "generate_stream": "Error del stream de contexto de generación de Gemini: {{error}}", "generate_complete_prompt": "Error de finalización de Gemini: {{error}}", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 9b92cf7240c..5fe6f61d2aa 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -87,6 +87,15 @@ "apiKeyModelPlanMismatch": "Les clés API et les plans d'abonnement permettent différents modèles. Assurez-vous que le modèle sélectionné est inclus dans votre plan.", "notFound": "Exécutable Claude Code '{{claudePath}}' introuvable.\n\nVeuillez installer Claude Code CLI :\n1. Visitez {{installationUrl}} pour télécharger Claude Code\n2. Suivez les instructions d'installation pour votre système d'exploitation\n3. Assurez-vous que la commande 'claude' est disponible dans votre PATH\n4. Alternativement, configurez un chemin personnalisé dans les paramètres Roo sous 'Chemin de Claude Code'\n\nErreur originale : {{originalError}}" }, + "message": { + "no_active_task_to_delete": "Aucune tâche active pour supprimer des messages", + "invalid_timestamp_for_deletion": "Horodatage du message invalide pour la suppression", + "cannot_delete_missing_timestamp": "Impossible de supprimer le message : horodatage manquant", + "cannot_delete_invalid_timestamp": "Impossible de supprimer le message : horodatage invalide", + "message_not_found": "Message avec horodatage {{messageTs}} introuvable", + "error_deleting_message": "Erreur lors de la suppression du message : {{error}}", + "error_editing_message": "Erreur lors de la modification du message : {{error}}" + }, "gemini": { "generate_stream": "Erreur du flux de contexte de génération Gemini : {{error}}", "generate_complete_prompt": "Erreur d'achèvement de Gemini : {{error}}", diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index f9bbed0dfca..f9c2dfbf80b 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -87,6 +87,15 @@ "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.", "notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}" }, + "message": { + "no_active_task_to_delete": "संदेशों को हटाने के लिए कोई सक्रिय कार्य नहीं", + "invalid_timestamp_for_deletion": "हटाने के लिए अमान्य संदेश टाइमस्टैम्प", + "cannot_delete_missing_timestamp": "संदेश हटाया नहीं जा सकता: टाइमस्टैम्प गुम है", + "cannot_delete_invalid_timestamp": "संदेश हटाया नहीं जा सकता: अमान्य टाइमस्टैम्प", + "message_not_found": "टाइमस्टैम्प {{messageTs}} वाला संदेश नहीं मिला", + "error_deleting_message": "संदेश हटाने में त्रुटि: {{error}}", + "error_editing_message": "संदेश संपादित करने में त्रुटि: {{error}}" + }, "gemini": { "generate_stream": "जेमिनी जनरेट कॉन्टेक्स्ट स्ट्रीम त्रुटि: {{error}}", "generate_complete_prompt": "जेमिनी समापन त्रुटि: {{error}}", diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index 147d88c4e74..f29c88e6379 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -87,6 +87,15 @@ "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.", "notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}" }, + "message": { + "no_active_task_to_delete": "Tidak ada tugas aktif untuk menghapus pesan", + "invalid_timestamp_for_deletion": "Timestamp pesan tidak valid untuk penghapusan", + "cannot_delete_missing_timestamp": "Tidak dapat menghapus pesan: timestamp tidak ada", + "cannot_delete_invalid_timestamp": "Tidak dapat menghapus pesan: timestamp tidak valid", + "message_not_found": "Pesan dengan timestamp {{messageTs}} tidak ditemukan", + "error_deleting_message": "Error menghapus pesan: {{error}}", + "error_editing_message": "Error mengedit pesan: {{error}}" + }, "gemini": { "generate_stream": "Kesalahan aliran konteks pembuatan Gemini: {{error}}", "generate_complete_prompt": "Kesalahan penyelesaian Gemini: {{error}}", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index c304896163e..2a19e777b5e 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -87,6 +87,15 @@ "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.", "notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}" }, + "message": { + "no_active_task_to_delete": "Nessuna attività attiva da cui eliminare messaggi", + "invalid_timestamp_for_deletion": "Timestamp del messaggio non valido per l'eliminazione", + "cannot_delete_missing_timestamp": "Impossibile eliminare il messaggio: timestamp mancante", + "cannot_delete_invalid_timestamp": "Impossibile eliminare il messaggio: timestamp non valido", + "message_not_found": "Messaggio con timestamp {{messageTs}} non trovato", + "error_deleting_message": "Errore durante l'eliminazione del messaggio: {{error}}", + "error_editing_message": "Errore durante la modifica del messaggio: {{error}}" + }, "gemini": { "generate_stream": "Errore del flusso di contesto di generazione Gemini: {{error}}", "generate_complete_prompt": "Errore di completamento Gemini: {{error}}", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 3f286351081..bb39932aa79 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -87,6 +87,15 @@ "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.", "notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}" }, + "message": { + "no_active_task_to_delete": "メッセージを削除するアクティブなタスクがありません", + "invalid_timestamp_for_deletion": "削除用のメッセージタイムスタンプが無効です", + "cannot_delete_missing_timestamp": "メッセージを削除できません:タイムスタンプがありません", + "cannot_delete_invalid_timestamp": "メッセージを削除できません:タイムスタンプが無効です", + "message_not_found": "タイムスタンプ {{messageTs}} のメッセージが見つかりません", + "error_deleting_message": "メッセージ削除エラー:{{error}}", + "error_editing_message": "メッセージ編集エラー:{{error}}" + }, "gemini": { "generate_stream": "Gemini 生成コンテキスト ストリーム エラー: {{error}}", "generate_complete_prompt": "Gemini 完了エラー: {{error}}", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index d1f2ef9c44a..a8aaec1aed7 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -87,6 +87,15 @@ "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.", "notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}" }, + "message": { + "no_active_task_to_delete": "메시지를 삭제할 활성 작업이 없습니다", + "invalid_timestamp_for_deletion": "삭제를 위한 메시지 타임스탬프가 유효하지 않습니다", + "cannot_delete_missing_timestamp": "메시지를 삭제할 수 없습니다: 타임스탬프가 없습니다", + "cannot_delete_invalid_timestamp": "메시지를 삭제할 수 없습니다: 타임스탬프가 유효하지 않습니다", + "message_not_found": "타임스탬프 {{messageTs}}인 메시지를 찾을 수 없습니다", + "error_deleting_message": "메시지 삭제 오류: {{error}}", + "error_editing_message": "메시지 편집 오류: {{error}}" + }, "gemini": { "generate_stream": "Gemini 생성 컨텍스트 스트림 오류: {{error}}", "generate_complete_prompt": "Gemini 완료 오류: {{error}}", diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index a2b29d8df03..889e378eeb7 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -87,6 +87,15 @@ "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.", "notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}" }, + "message": { + "no_active_task_to_delete": "Geen actieve taak om berichten uit te verwijderen", + "invalid_timestamp_for_deletion": "Ongeldig bericht tijdstempel voor verwijdering", + "cannot_delete_missing_timestamp": "Kan bericht niet verwijderen: tijdstempel ontbreekt", + "cannot_delete_invalid_timestamp": "Kan bericht niet verwijderen: ongeldig tijdstempel", + "message_not_found": "Bericht met tijdstempel {{messageTs}} niet gevonden", + "error_deleting_message": "Fout bij verwijderen van bericht: {{error}}", + "error_editing_message": "Fout bij bewerken van bericht: {{error}}" + }, "gemini": { "generate_stream": "Fout bij het genereren van contextstream door Gemini: {{error}}", "generate_complete_prompt": "Fout bij het voltooien door Gemini: {{error}}", diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index 45e0651fac6..d447b36ae96 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -87,6 +87,15 @@ "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.", "notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}" }, + "message": { + "no_active_task_to_delete": "Brak aktywnego zadania do usunięcia wiadomości", + "invalid_timestamp_for_deletion": "Nieprawidłowy znacznik czasu wiadomości do usunięcia", + "cannot_delete_missing_timestamp": "Nie można usunąć wiadomości: brak znacznika czasu", + "cannot_delete_invalid_timestamp": "Nie można usunąć wiadomości: nieprawidłowy znacznik czasu", + "message_not_found": "Wiadomość ze znacznikiem czasu {{messageTs}} nie została znaleziona", + "error_deleting_message": "Błąd usuwania wiadomości: {{error}}", + "error_editing_message": "Błąd edytowania wiadomości: {{error}}" + }, "gemini": { "generate_stream": "Błąd strumienia kontekstu generowania Gemini: {{error}}", "generate_complete_prompt": "Błąd uzupełniania Gemini: {{error}}", diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index 001457707e0..5e6317cf0cd 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -91,6 +91,15 @@ "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.", "notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}" }, + "message": { + "no_active_task_to_delete": "Nenhuma tarefa ativa para excluir mensagens", + "invalid_timestamp_for_deletion": "Timestamp da mensagem inválido para exclusão", + "cannot_delete_missing_timestamp": "Não é possível excluir mensagem: timestamp ausente", + "cannot_delete_invalid_timestamp": "Não é possível excluir mensagem: timestamp inválido", + "message_not_found": "Mensagem com timestamp {{messageTs}} não encontrada", + "error_deleting_message": "Erro ao excluir mensagem: {{error}}", + "error_editing_message": "Erro ao editar mensagem: {{error}}" + }, "gemini": { "generate_stream": "Erro de fluxo de contexto de geração do Gemini: {{error}}", "generate_complete_prompt": "Erro de conclusão do Gemini: {{error}}", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 3500a9a5add..c7e80c310ec 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -87,6 +87,15 @@ "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.", "notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}" }, + "message": { + "no_active_task_to_delete": "Нет активной задачи для удаления сообщений", + "invalid_timestamp_for_deletion": "Недействительная временная метка сообщения для удаления", + "cannot_delete_missing_timestamp": "Невозможно удалить сообщение: отсутствует временная метка", + "cannot_delete_invalid_timestamp": "Невозможно удалить сообщение: недействительная временная метка", + "message_not_found": "Сообщение с временной меткой {{messageTs}} не найдено", + "error_deleting_message": "Ошибка удаления сообщения: {{error}}", + "error_editing_message": "Ошибка редактирования сообщения: {{error}}" + }, "gemini": { "generate_stream": "Ошибка потока контекста генерации Gemini: {{error}}", "generate_complete_prompt": "Ошибка завершения Gemini: {{error}}", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 4089cff2176..c8ef3ca13d7 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -87,6 +87,15 @@ "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.", "notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}" }, + "message": { + "no_active_task_to_delete": "Mesaj silinecek aktif görev yok", + "invalid_timestamp_for_deletion": "Silme için geçersiz mesaj zaman damgası", + "cannot_delete_missing_timestamp": "Mesaj silinemiyor: zaman damgası eksik", + "cannot_delete_invalid_timestamp": "Mesaj silinemiyor: geçersiz zaman damgası", + "message_not_found": "{{messageTs}} zaman damgalı mesaj bulunamadı", + "error_deleting_message": "Mesaj silme hatası: {{error}}", + "error_editing_message": "Mesaj düzenleme hatası: {{error}}" + }, "gemini": { "generate_stream": "Gemini oluşturma bağlam akışı hatası: {{error}}", "generate_complete_prompt": "Gemini tamamlama hatası: {{error}}", diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index ecf686f520e..2f8e05e8188 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -87,6 +87,15 @@ "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.", "notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}" }, + "message": { + "no_active_task_to_delete": "Không có nhiệm vụ hoạt động để xóa tin nhắn", + "invalid_timestamp_for_deletion": "Dấu thời gian tin nhắn không hợp lệ để xóa", + "cannot_delete_missing_timestamp": "Không thể xóa tin nhắn: thiếu dấu thời gian", + "cannot_delete_invalid_timestamp": "Không thể xóa tin nhắn: dấu thời gian không hợp lệ", + "message_not_found": "Không tìm thấy tin nhắn có dấu thời gian {{messageTs}}", + "error_deleting_message": "Lỗi xóa tin nhắn: {{error}}", + "error_editing_message": "Lỗi chỉnh sửa tin nhắn: {{error}}" + }, "gemini": { "generate_stream": "Lỗi luồng ngữ cảnh tạo Gemini: {{error}}", "generate_complete_prompt": "Lỗi hoàn thành Gemini: {{error}}", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 9f4d24f6ebf..74b1b153b2b 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -92,6 +92,15 @@ "apiKeyModelPlanMismatch": "API keys and subscription plans allow different models. Make sure the selected model is included in your plan.", "notFound": "Claude Code executable '{{claudePath}}' not found.\n\nPlease install Claude Code CLI:\n1. Visit {{installationUrl}} to download Claude Code\n2. Follow the installation instructions for your operating system\n3. Ensure the 'claude' command is available in your PATH\n4. Alternatively, configure a custom path in Roo settings under 'Claude Code Path'\n\nOriginal error: {{originalError}}" }, + "message": { + "no_active_task_to_delete": "没有可删除消息的活跃任务", + "invalid_timestamp_for_deletion": "删除操作的消息时间戳无效", + "cannot_delete_missing_timestamp": "无法删除消息:缺少时间戳", + "cannot_delete_invalid_timestamp": "无法删除消息:时间戳无效", + "message_not_found": "未找到时间戳为 {{messageTs}} 的消息", + "error_deleting_message": "删除消息时出错:{{error}}", + "error_editing_message": "编辑消息时出错:{{error}}" + }, "gemini": { "generate_stream": "Gemini 生成上下文流错误:{{error}}", "generate_complete_prompt": "Gemini 完成错误:{{error}}", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index d40b3e094f5..a2f2bfc4c69 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -86,6 +86,15 @@ "apiKeyModelPlanMismatch": "API 金鑰和訂閱方案允許不同的模型。請確保所選模型包含在您的方案中。", "notFound": "找不到 Claude Code 可執行檔案 '{{claudePath}}'。\n\n請安裝 Claude Code CLI:\n1. 造訪 {{installationUrl}} 下載 Claude Code\n2. 依照作業系統的安裝說明進行操作\n3. 確保 'claude' 指令在 PATH 中可用\n4. 或者在 Roo 設定中的 'Claude Code 路徑' 下設定自訂路徑\n\n原始錯誤:{{originalError}}" }, + "message": { + "no_active_task_to_delete": "沒有可刪除訊息的活躍工作", + "invalid_timestamp_for_deletion": "刪除操作的訊息時間戳無效", + "cannot_delete_missing_timestamp": "無法刪除訊息:缺少時間戳", + "cannot_delete_invalid_timestamp": "無法刪除訊息:時間戳無效", + "message_not_found": "未找到時間戳為 {{messageTs}} 的訊息", + "error_deleting_message": "刪除訊息時出錯:{{error}}", + "error_editing_message": "編輯訊息時出錯:{{error}}" + }, "gemini": { "generate_stream": "Gemini 產生內容串流錯誤:{{error}}", "generate_complete_prompt": "Gemini 完成錯誤:{{error}}",