diff --git a/src/core/webview/__tests__/webviewMessageHandler.test.ts b/src/core/webview/__tests__/webviewMessageHandler.test.ts index 7f3bc49654..24d786a5ea 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.test.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.test.ts @@ -2,9 +2,20 @@ import { webviewMessageHandler } from "../webviewMessageHandler" import { ClineProvider } from "../ClineProvider" import { getModels } from "../../../api/providers/fetchers/modelCache" import { ModelRecord } from "../../../shared/api" +import type { ClineMessage } from "@roo-code/types" +import * as vscode from "vscode" // Mock dependencies jest.mock("../../../api/providers/fetchers/modelCache") +jest.mock("vscode", () => ({ + window: { + showWarningMessage: jest.fn(), + }, +})) +jest.mock("../../checkpoints", () => ({ + checkpointRestore: jest.fn(), +})) + const mockGetModels = getModels as jest.MockedFunction // Mock ClineProvider @@ -272,3 +283,93 @@ describe("webviewMessageHandler - requestRouterModels", () => { }) }) }) + +describe("webviewMessageHandler - editMessage", () => { + let mockCline: any + + beforeEach(() => { + jest.clearAllMocks() + + // Mock Cline instance + mockCline = { + taskId: "test-task-id", + clineMessages: [ + { ts: 1000, type: "say", say: "user_feedback", text: "First message" }, + { ts: 2000, type: "say", say: "user_feedback", text: "Second message" }, + { ts: 3000, type: "say", say: "checkpoint_saved", text: "Checkpoint saved" }, + { ts: 4000, type: "say", say: "user_feedback", text: "Third message" }, + ] as ClineMessage[], + apiConversationHistory: [ + { ts: 1000, role: "user", content: "First message" }, + { ts: 2000, role: "user", content: "Second message" }, + { ts: 4000, role: "user", content: "Third message" }, + ], + overwriteClineMessages: jest.fn(), + overwriteApiConversationHistory: jest.fn(), + } + + mockClineProvider.getCurrentCline = jest.fn().mockReturnValue(mockCline) + mockClineProvider.getState = jest.fn().mockResolvedValue({ enableCheckpoints: true }) + mockClineProvider.getTaskWithId = jest.fn().mockResolvedValue({ + historyItem: { clineMessages: mockCline.clineMessages }, + }) + mockClineProvider.postStateToWebview = jest.fn() + mockClineProvider.initClineWithHistoryItem = jest.fn() + }) + + it("handles basic message editing without confirmation", async () => { + // Mock no subsequent messages and no checkpoints + mockCline.clineMessages = [{ ts: 1000, type: "say", say: "user_feedback", text: "Only message" }] + mockClineProvider.getState = jest.fn().mockResolvedValue({ enableCheckpoints: false }) + + await webviewMessageHandler(mockClineProvider, { + type: "editMessage", + value: 1000, + text: "Edited message", + }) + + expect(mockClineProvider.initClineWithHistoryItem).toHaveBeenCalled() + }) + + it("shows confirmation dialog when editing affects subsequent messages", async () => { + const mockShowWarning = vscode.window.showWarningMessage as jest.Mock + mockShowWarning.mockResolvedValue("Edit Message") + + await webviewMessageHandler(mockClineProvider, { + type: "editMessage", + value: 2000, // Edit second message, affecting third message + text: "Edited second message", + }) + + expect(mockShowWarning).toHaveBeenCalledWith( + "Edit and delete subsequent messages?\n\n• 1 checkpoint(s) will be removed", + { modal: true }, + "Edit Message", + ) + expect(mockClineProvider.initClineWithHistoryItem).toHaveBeenCalled() + }) + + it("cancels edit when user declines confirmation", async () => { + const mockShowWarning = vscode.window.showWarningMessage as jest.Mock + mockShowWarning.mockResolvedValue(undefined) // User cancelled + + await webviewMessageHandler(mockClineProvider, { + type: "editMessage", + value: 2000, + text: "This edit should be cancelled", + }) + + expect(mockClineProvider.postStateToWebview).toHaveBeenCalled() + expect(mockClineProvider.initClineWithHistoryItem).not.toHaveBeenCalled() + }) + + it("handles invalid message parameters gracefully", async () => { + await webviewMessageHandler(mockClineProvider, { + type: "editMessage", + value: undefined, // Invalid value + text: "Should not process", + }) + + expect(mockClineProvider.initClineWithHistoryItem).not.toHaveBeenCalled() + }) +}) diff --git a/webview-ui/src/components/chat/__tests__/ChatView.test.tsx b/webview-ui/src/components/chat/__tests__/ChatView.test.tsx index 5b618378c4..0948e4de99 100644 --- a/webview-ui/src/components/chat/__tests__/ChatView.test.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatView.test.tsx @@ -1072,3 +1072,33 @@ describe("ChatView - Focus Grabbing Tests", () => { expect(mockFocus).toHaveBeenCalledTimes(FOCUS_CALLS_ON_INIT) }) }) + +describe("ChatView - Message Edit Tests", () => { + beforeEach(() => jest.clearAllMocks()) + + it("renders ChatView component without errors", () => { + const { container } = renderChatView() + + // Verify the component renders + expect(container).toBeInTheDocument() + }) + + it("accepts message state updates", () => { + renderChatView() + + // Mock a user message state + mockPostMessage({ + clineMessages: [ + { + type: "say", + say: "user_feedback", + ts: 1234567890, + text: "Test message", + }, + ], + }) + + // Verify no errors occurred during state update + expect(vscode.postMessage).toHaveBeenCalled() + }) +}) diff --git a/webview-ui/src/i18n/locales/ca/common.json b/webview-ui/src/i18n/locales/ca/common.json index 267e0a62d7..3d66568729 100644 --- a/webview-ui/src/i18n/locales/ca/common.json +++ b/webview-ui/src/i18n/locales/ca/common.json @@ -6,6 +6,10 @@ "remove": "Eliminar", "keep": "Mantenir" }, + "actions": { + "edit": "Editar", + "delete": "Suprimir" + }, "number_format": { "thousand_suffix": "k", "million_suffix": "m", diff --git a/webview-ui/src/i18n/locales/de/common.json b/webview-ui/src/i18n/locales/de/common.json index 76b9064bc3..08153f3fa0 100644 --- a/webview-ui/src/i18n/locales/de/common.json +++ b/webview-ui/src/i18n/locales/de/common.json @@ -6,6 +6,10 @@ "remove": "Entfernen", "keep": "Behalten" }, + "actions": { + "edit": "Bearbeiten", + "delete": "Löschen" + }, "number_format": { "thousand_suffix": "k", "million_suffix": "m", diff --git a/webview-ui/src/i18n/locales/es/common.json b/webview-ui/src/i18n/locales/es/common.json index 5fe624372f..9f78f9d6ef 100644 --- a/webview-ui/src/i18n/locales/es/common.json +++ b/webview-ui/src/i18n/locales/es/common.json @@ -6,6 +6,10 @@ "remove": "Eliminar", "keep": "Mantener" }, + "actions": { + "edit": "Editar", + "delete": "Eliminar" + }, "number_format": { "thousand_suffix": "k", "million_suffix": "m", diff --git a/webview-ui/src/i18n/locales/fr/common.json b/webview-ui/src/i18n/locales/fr/common.json index 677116ff2a..e52808d885 100644 --- a/webview-ui/src/i18n/locales/fr/common.json +++ b/webview-ui/src/i18n/locales/fr/common.json @@ -6,6 +6,10 @@ "remove": "Supprimer", "keep": "Conserver" }, + "actions": { + "edit": "Modifier", + "delete": "Supprimer" + }, "number_format": { "thousand_suffix": "k", "million_suffix": "m", diff --git a/webview-ui/src/i18n/locales/hi/common.json b/webview-ui/src/i18n/locales/hi/common.json index 77876eb274..e3c4a31573 100644 --- a/webview-ui/src/i18n/locales/hi/common.json +++ b/webview-ui/src/i18n/locales/hi/common.json @@ -6,6 +6,10 @@ "remove": "हटाएं", "keep": "रखें" }, + "actions": { + "edit": "संपादित करें", + "delete": "हटाएं" + }, "number_format": { "thousand_suffix": "k", "million_suffix": "m", diff --git a/webview-ui/src/i18n/locales/id/common.json b/webview-ui/src/i18n/locales/id/common.json index d50246ced2..c6b5a601c3 100644 --- a/webview-ui/src/i18n/locales/id/common.json +++ b/webview-ui/src/i18n/locales/id/common.json @@ -6,6 +6,10 @@ "remove": "Hapus", "keep": "Simpan" }, + "actions": { + "edit": "Ubah", + "delete": "Hapus" + }, "number_format": { "thousand_suffix": "rb", "million_suffix": "jt", diff --git a/webview-ui/src/i18n/locales/it/common.json b/webview-ui/src/i18n/locales/it/common.json index 9d5426aa0e..807f95cd02 100644 --- a/webview-ui/src/i18n/locales/it/common.json +++ b/webview-ui/src/i18n/locales/it/common.json @@ -6,6 +6,10 @@ "remove": "Rimuovi", "keep": "Mantieni" }, + "actions": { + "edit": "Modifica", + "delete": "Elimina" + }, "number_format": { "thousand_suffix": "k", "million_suffix": "m", diff --git a/webview-ui/src/i18n/locales/ja/common.json b/webview-ui/src/i18n/locales/ja/common.json index 975ea67834..713e301371 100644 --- a/webview-ui/src/i18n/locales/ja/common.json +++ b/webview-ui/src/i18n/locales/ja/common.json @@ -6,6 +6,10 @@ "remove": "削除", "keep": "保持" }, + "actions": { + "edit": "編集", + "delete": "削除" + }, "number_format": { "thousand_suffix": "k", "million_suffix": "m", diff --git a/webview-ui/src/i18n/locales/ko/common.json b/webview-ui/src/i18n/locales/ko/common.json index 276f2cb20b..ee7991ed67 100644 --- a/webview-ui/src/i18n/locales/ko/common.json +++ b/webview-ui/src/i18n/locales/ko/common.json @@ -6,6 +6,10 @@ "remove": "삭제", "keep": "유지" }, + "actions": { + "edit": "편집", + "delete": "삭제" + }, "number_format": { "thousand_suffix": "k", "million_suffix": "m", diff --git a/webview-ui/src/i18n/locales/nl/common.json b/webview-ui/src/i18n/locales/nl/common.json index 012808e51a..9068482916 100644 --- a/webview-ui/src/i18n/locales/nl/common.json +++ b/webview-ui/src/i18n/locales/nl/common.json @@ -6,6 +6,10 @@ "remove": "Verwijderen", "keep": "Behouden" }, + "actions": { + "edit": "Bewerken", + "delete": "Verwijderen" + }, "number_format": { "thousand_suffix": "k", "million_suffix": "m", diff --git a/webview-ui/src/i18n/locales/pl/common.json b/webview-ui/src/i18n/locales/pl/common.json index c72b046c42..fd0c981366 100644 --- a/webview-ui/src/i18n/locales/pl/common.json +++ b/webview-ui/src/i18n/locales/pl/common.json @@ -11,6 +11,10 @@ "remove": "Usuń", "keep": "Zachowaj" }, + "actions": { + "edit": "Edytuj", + "delete": "Usuń" + }, "ui": { "search_placeholder": "Szukaj..." }, diff --git a/webview-ui/src/i18n/locales/pt-BR/common.json b/webview-ui/src/i18n/locales/pt-BR/common.json index a911b2366f..a0f8a3ffd1 100644 --- a/webview-ui/src/i18n/locales/pt-BR/common.json +++ b/webview-ui/src/i18n/locales/pt-BR/common.json @@ -11,6 +11,10 @@ "remove": "Remover", "keep": "Manter" }, + "actions": { + "edit": "Editar", + "delete": "Excluir" + }, "ui": { "search_placeholder": "Pesquisar..." }, diff --git a/webview-ui/src/i18n/locales/ru/common.json b/webview-ui/src/i18n/locales/ru/common.json index e68899a2db..24a9e64d7f 100644 --- a/webview-ui/src/i18n/locales/ru/common.json +++ b/webview-ui/src/i18n/locales/ru/common.json @@ -11,6 +11,10 @@ "remove": "Удалить", "keep": "Оставить" }, + "actions": { + "edit": "Редактировать", + "delete": "Удалить" + }, "ui": { "search_placeholder": "Поиск..." }, diff --git a/webview-ui/src/i18n/locales/tr/common.json b/webview-ui/src/i18n/locales/tr/common.json index 23344ca966..f32207ad1d 100644 --- a/webview-ui/src/i18n/locales/tr/common.json +++ b/webview-ui/src/i18n/locales/tr/common.json @@ -6,6 +6,10 @@ "remove": "Kaldır", "keep": "Tut" }, + "actions": { + "edit": "Düzenle", + "delete": "Sil" + }, "number_format": { "thousand_suffix": "k", "million_suffix": "m", diff --git a/webview-ui/src/i18n/locales/vi/common.json b/webview-ui/src/i18n/locales/vi/common.json index 16952117ef..90b9b009cc 100644 --- a/webview-ui/src/i18n/locales/vi/common.json +++ b/webview-ui/src/i18n/locales/vi/common.json @@ -6,6 +6,10 @@ "remove": "Xóa", "keep": "Giữ" }, + "actions": { + "edit": "Chỉnh sửa", + "delete": "Xóa" + }, "number_format": { "thousand_suffix": "k", "million_suffix": "m", diff --git a/webview-ui/src/i18n/locales/zh-CN/common.json b/webview-ui/src/i18n/locales/zh-CN/common.json index 29f11c7f2f..4f55d38fcd 100644 --- a/webview-ui/src/i18n/locales/zh-CN/common.json +++ b/webview-ui/src/i18n/locales/zh-CN/common.json @@ -6,6 +6,10 @@ "remove": "移除", "keep": "保留" }, + "actions": { + "edit": "编辑", + "delete": "删除" + }, "number_format": { "thousand_suffix": "k", "million_suffix": "m", diff --git a/webview-ui/src/i18n/locales/zh-TW/common.json b/webview-ui/src/i18n/locales/zh-TW/common.json index b8ec7f998e..db292b98e4 100644 --- a/webview-ui/src/i18n/locales/zh-TW/common.json +++ b/webview-ui/src/i18n/locales/zh-TW/common.json @@ -6,6 +6,10 @@ "remove": "移除", "keep": "保留" }, + "actions": { + "edit": "編輯", + "delete": "刪除" + }, "number_format": { "thousand_suffix": "k", "million_suffix": "m",