Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
353 changes: 216 additions & 137 deletions src/core/webview/__tests__/ClineProvider.spec.ts

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions src/core/webview/__tests__/webviewMessageHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,13 @@ const mockClineProvider = {
globalStorageUri: { fsPath: "/mock/global/storage" },
},
setValue: vi.fn(),
getValue: vi.fn(),
},
log: vi.fn(),
postStateToWebview: vi.fn(),
getCurrentCline: vi.fn(),
getTaskWithId: vi.fn(),
initClineWithHistoryItem: vi.fn(),
} as unknown as ClineProvider

import { t } from "../../../i18n"
Expand Down Expand Up @@ -482,3 +486,51 @@ describe("webviewMessageHandler - deleteCustomMode", () => {
expect(mockClineProvider.postMessageToWebview).not.toHaveBeenCalled()
})
})

describe("webviewMessageHandler - message dialog preferences", () => {
beforeEach(() => {
vi.clearAllMocks()
// Mock a current Cline instance
vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({
taskId: "test-task-id",
apiConversationHistory: [],
clineMessages: [],
} as any)
// Reset getValue mock
vi.mocked(mockClineProvider.contextProxy.getValue).mockReturnValue(false)
})

describe("deleteMessage", () => {
it("should always show dialog for delete confirmation", async () => {
vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({} as any) // Mock current cline exists

await webviewMessageHandler(mockClineProvider, {
type: "deleteMessage",
value: 123456789, // Changed from messageTs to value
})

expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "showDeleteMessageDialog",
messageTs: 123456789,
})
})
})

describe("submitEditedMessage", () => {
it("should always show dialog for edit confirmation", async () => {
vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({} as any) // Mock current cline exists

await webviewMessageHandler(mockClineProvider, {
type: "submitEditedMessage",
value: 123456789, // messageTs as number
editedMessageContent: "edited content", // text content in editedMessageContent field
})

expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "showEditMessageDialog",
messageTs: 123456789,
text: "edited content",
})
})
})
})
138 changes: 58 additions & 80 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,55 +77,6 @@ export const webviewMessageHandler = async (
return { messageIndex, apiConversationHistoryIndex }
}

/**
* Removes just the target message, preserving messages after the next user message
*/
const removeMessagesJustThis = async (
currentCline: any,
messageIndex: number,
apiConversationHistoryIndex: number,
) => {
// Find the next user message first
const nextUserMessage = currentCline.clineMessages
.slice(messageIndex + 1)
.find((msg: ClineMessage) => msg.type === "say" && msg.say === "user_feedback")

// Handle UI messages
if (nextUserMessage) {
// Find absolute index of next user message
const nextUserMessageIndex = currentCline.clineMessages.findIndex(
(msg: ClineMessage) => msg === nextUserMessage,
)

// Keep messages before current message and after next user message
await currentCline.overwriteClineMessages([
...currentCline.clineMessages.slice(0, messageIndex),
...currentCline.clineMessages.slice(nextUserMessageIndex),
])
} else {
// If no next user message, keep only messages before current message
await currentCline.overwriteClineMessages(currentCline.clineMessages.slice(0, messageIndex))
}

// Handle API messages
if (apiConversationHistoryIndex !== -1) {
if (nextUserMessage && nextUserMessage.ts) {
// Keep messages before current API message and after next user message
await currentCline.overwriteApiConversationHistory([
...currentCline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
...currentCline.apiConversationHistory.filter(
(msg: ApiMessage) => msg.ts && msg.ts >= nextUserMessage.ts,
),
])
} else {
// If no next user message, keep only messages before current API message
await currentCline.overwriteApiConversationHistory(
currentCline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
)
}
}
}

/**
* Removes the target message and all subsequent messages
*/
Expand All @@ -148,34 +99,28 @@ export const webviewMessageHandler = async (
* Handles message deletion operations with user confirmation
*/
const handleDeleteOperation = async (messageTs: number): Promise<void> => {
const options = [
t("common:confirmation.delete_just_this_message"),
t("common:confirmation.delete_this_and_subsequent"),
]

const answer = await vscode.window.showInformationMessage(
t("common:confirmation.delete_message"),
{ modal: true },
...options,
)
// Send message to webview to show delete confirmation dialog
await provider.postMessageToWebview({
type: "showDeleteMessageDialog",
messageTs,
})
}

// Only proceed if user selected one of the options and we have a current cline
if (answer && options.includes(answer) && provider.getCurrentCline()) {
/**
* Handles confirmed message deletion from webview dialog
*/
const handleDeleteMessageConfirm = async (messageTs: number): Promise<void> => {
// Only proceed if we have a current cline
if (provider.getCurrentCline()) {
const currentCline = provider.getCurrentCline()!
const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline)

if (messageIndex !== -1) {
try {
const { historyItem } = await provider.getTaskWithId(currentCline.taskId)

// Check which option the user selected
if (answer === options[0]) {
// Delete just this message
await removeMessagesJustThis(currentCline, messageIndex, apiConversationHistoryIndex)
} else if (answer === options[1]) {
// Delete this message and all subsequent
await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex)
}
// Delete this message and all subsequent messages
await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex)

// Initialize with history item after deletion
await provider.initClineWithHistoryItem(historyItem)
Expand All @@ -192,15 +137,26 @@ export const webviewMessageHandler = async (
/**
* Handles message editing operations with user confirmation
*/
const handleEditOperation = async (messageTs: number, editedContent: string): Promise<void> => {
const answer = await vscode.window.showWarningMessage(
t("common:confirmation.edit_warning"),
{ modal: true },
t("common:confirmation.proceed"),
)
const handleEditOperation = async (messageTs: number, editedContent: string, images?: string[]): Promise<void> => {
// Send message to webview to show edit confirmation dialog
await provider.postMessageToWebview({
type: "showEditMessageDialog",
messageTs,
text: editedContent,
images,
})
}

// Only proceed if user selected "Proceed" and we have a current cline
if (answer === t("common:confirmation.proceed") && provider.getCurrentCline()) {
/**
* Handles confirmed message editing from webview dialog
*/
const handleEditMessageConfirm = async (
messageTs: number,
editedContent: string,
images?: string[],
): Promise<void> => {
// Only proceed if we have a current cline
if (provider.getCurrentCline()) {
const currentCline = provider.getCurrentCline()!

// Use findMessageIndices to find messages based on timestamp
Expand All @@ -217,6 +173,7 @@ export const webviewMessageHandler = async (
type: "askResponse",
askResponse: "messageResponse",
text: editedContent,
images,
})

// Don't initialize with history item for edit operations
Expand All @@ -242,11 +199,12 @@ export const webviewMessageHandler = async (
messageTs: number,
operation: "delete" | "edit",
editedContent?: string,
images?: string[],
): Promise<void> => {
if (operation === "delete") {
await handleDeleteOperation(messageTs)
} else if (operation === "edit" && editedContent) {
await handleEditOperation(messageTs, editedContent)
await handleEditOperation(messageTs, editedContent, images)
}
}

Expand Down Expand Up @@ -416,7 +374,12 @@ export const webviewMessageHandler = async (
break
case "selectImages":
const images = await selectImages()
await provider.postMessageToWebview({ type: "selectedImages", images })
await provider.postMessageToWebview({
type: "selectedImages",
images,
context: message.context,
messageTs: message.messageTs,
})
break
case "exportCurrentTask":
const currentTaskId = provider.getCurrentCline()?.taskId
Expand Down Expand Up @@ -1193,7 +1156,12 @@ export const webviewMessageHandler = async (
message.value &&
message.editedMessageContent
) {
await handleMessageModificationsOperation(message.value, "edit", message.editedMessageContent)
await handleMessageModificationsOperation(
message.value,
"edit",
message.editedMessageContent,
message.images,
)
}
break
}
Expand Down Expand Up @@ -1526,6 +1494,16 @@ export const webviewMessageHandler = async (
}
}
break
case "deleteMessageConfirm":
if (message.messageTs) {
await handleDeleteMessageConfirm(message.messageTs)
}
break
case "editMessageConfirm":
if (message.messageTs && message.text) {
await handleEditMessageConfirm(message.messageTs, message.text, message.images)
}
break
case "getListApiConfiguration":
try {
const listApiConfig = await provider.providerSettingsManager.listConfig()
Expand Down
7 changes: 1 addition & 6 deletions src/i18n/locales/ca/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,7 @@
"confirmation": {
"reset_state": "Estàs segur que vols restablir tots els estats i emmagatzematge secret a l'extensió? Això no es pot desfer.",
"delete_config_profile": "Estàs segur que vols eliminar aquest perfil de configuració?",
"delete_custom_mode_with_rules": "Esteu segur que voleu suprimir aquest mode {scope}?\n\nAixò també suprimirà la carpeta de regles associada a:\n{rulesFolderPath}",
"delete_message": "Què vols eliminar?",
"edit_warning": "Editar aquest missatge eliminarà tots els missatges posteriors de la conversa. Vols continuar?",
"delete_just_this_message": "Només aquest missatge",
"delete_this_and_subsequent": "Aquest i tots els missatges posteriors",
"proceed": "Continuar"
"delete_custom_mode_with_rules": "Esteu segur que voleu suprimir aquest mode {scope}?\n\nAixò també suprimirà la carpeta de regles associada a:\n{rulesFolderPath}"
},
"errors": {
"invalid_data_uri": "Format d'URI de dades no vàlid",
Expand Down
7 changes: 1 addition & 6 deletions src/i18n/locales/de/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,7 @@
"confirmation": {
"reset_state": "Möchtest du wirklich alle Zustände und geheimen Speicher in der Erweiterung zurücksetzen? Dies kann nicht rückgängig gemacht werden.",
"delete_config_profile": "Möchtest du dieses Konfigurationsprofil wirklich löschen?",
"delete_custom_mode_with_rules": "Bist du sicher, dass du diesen {scope}-Modus löschen möchtest?\n\nDadurch wird auch der zugehörige Regelordner unter folgender Adresse gelöscht:\n{rulesFolderPath}",
"delete_message": "Was möchtest du löschen?",
"edit_warning": "Das Bearbeiten dieser Nachricht wird alle nachfolgenden Nachrichten in der Unterhaltung löschen. Möchtest du fortfahren?",
"delete_just_this_message": "Nur diese Nachricht",
"delete_this_and_subsequent": "Diese und alle nachfolgenden Nachrichten",
"proceed": "Fortfahren"
"delete_custom_mode_with_rules": "Bist du sicher, dass du diesen {scope}-Modus löschen möchtest?\n\nDadurch wird auch der zugehörige Regelordner unter folgender Adresse gelöscht:\n{rulesFolderPath}"
},
"errors": {
"invalid_data_uri": "Ungültiges Daten-URI-Format",
Expand Down
7 changes: 1 addition & 6 deletions src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,7 @@
"confirmation": {
"reset_state": "Are you sure you want to reset all state and secret storage in the extension? This cannot be undone.",
"delete_config_profile": "Are you sure you want to delete this configuration profile?",
"delete_custom_mode_with_rules": "Are you sure you want to delete this {scope} mode?\n\nThis will also delete the associated rules folder at:\n{rulesFolderPath}",
"delete_message": "What would you like to delete?",
"edit_warning": "Editing this message will delete all subsequent messages in the conversation. Do you want to proceed?",
"delete_just_this_message": "Just this message",
"delete_this_and_subsequent": "This and all subsequent messages",
"proceed": "Proceed"
"delete_custom_mode_with_rules": "Are you sure you want to delete this {scope} mode?\n\nThis will also delete the associated rules folder at:\n{rulesFolderPath}"
},
"errors": {
"invalid_data_uri": "Invalid data URI format",
Expand Down
7 changes: 1 addition & 6 deletions src/i18n/locales/es/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,7 @@
"confirmation": {
"reset_state": "¿Estás seguro de que deseas restablecer todo el estado y el almacenamiento secreto en la extensión? Esta acción no se puede deshacer.",
"delete_config_profile": "¿Estás seguro de que deseas eliminar este perfil de configuración?",
"delete_custom_mode_with_rules": "¿Estás seguro de que quieres eliminar este modo {scope}?\n\nEsto también eliminará la carpeta de reglas asociada en:\n{rulesFolderPath}",
"delete_message": "¿Qué deseas eliminar?",
"edit_warning": "Editar este mensaje eliminará todos los mensajes posteriores en la conversación. ¿Deseas continuar?",
"delete_just_this_message": "Solo este mensaje",
"delete_this_and_subsequent": "Este y todos los mensajes posteriores",
"proceed": "Continuar"
"delete_custom_mode_with_rules": "¿Estás seguro de que quieres eliminar este modo {scope}?\n\nEsto también eliminará la carpeta de reglas asociada en:\n{rulesFolderPath}"
},
"errors": {
"invalid_data_uri": "Formato de URI de datos no válido",
Expand Down
7 changes: 1 addition & 6 deletions src/i18n/locales/fr/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,7 @@
"confirmation": {
"reset_state": "Êtes-vous sûr de vouloir réinitialiser le global state et le stockage de secrets de l'extension ? Cette action est irréversible.",
"delete_config_profile": "Êtes-vous sûr de vouloir supprimer ce profil de configuration ?",
"delete_custom_mode_with_rules": "Êtes-vous sûr de vouloir supprimer ce mode {scope} ?\n\nCela supprimera également le dossier de règles associé à l'adresse :\n{rulesFolderPath}",
"delete_message": "Que souhaitez-vous supprimer ?",
"edit_warning": "Modifier ce message supprimera tous les messages suivants dans la conversation. Voulez-vous continuer ?",
"delete_just_this_message": "Uniquement ce message",
"delete_this_and_subsequent": "Ce message et tous les messages suivants",
"proceed": "Continuer"
"delete_custom_mode_with_rules": "Êtes-vous sûr de vouloir supprimer ce mode {scope} ?\n\nCela supprimera également le dossier de règles associé à l'adresse :\n{rulesFolderPath}"
},
"errors": {
"invalid_data_uri": "Format d'URI de données invalide",
Expand Down
7 changes: 1 addition & 6 deletions src/i18n/locales/hi/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,7 @@
"confirmation": {
"reset_state": "क्या आप वाकई एक्सटेंशन में सभी स्टेट और गुप्त स्टोरेज रीसेट करना चाहते हैं? इसे पूर्ववत नहीं किया जा सकता है।",
"delete_config_profile": "क्या आप वाकई इस कॉन्फ़िगरेशन प्रोफ़ाइल को हटाना चाहते हैं?",
"delete_custom_mode_with_rules": "क्या आप वाकई इस {scope} मोड को हटाना चाहते हैं?\n\nयह संबंधित नियम फ़ोल्डर को भी यहाँ हटा देगा:\n{rulesFolderPath}",
"delete_message": "आप क्या हटाना चाहते हैं?",
"edit_warning": "इस संदेश को संपादित करने से बातचीत के सभी बाद के संदेश हट जाएंगे। क्या आप जारी रखना चाहते हैं?",
"delete_just_this_message": "सिर्फ यह संदेश",
"delete_this_and_subsequent": "यह और सभी बाद के संदेश",
"proceed": "जारी रखें"
"delete_custom_mode_with_rules": "क्या आप वाकई इस {scope} मोड को हटाना चाहते हैं?\n\nयह संबंधित नियम फ़ोल्डर को भी यहाँ हटा देगा:\n{rulesFolderPath}"
},
"errors": {
"invalid_data_uri": "अमान्य डेटा URI फॉर्मेट",
Expand Down
7 changes: 1 addition & 6 deletions src/i18n/locales/id/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,7 @@
"confirmation": {
"reset_state": "Apakah kamu yakin ingin mereset semua state dan secret storage di ekstensi? Ini tidak dapat dibatalkan.",
"delete_config_profile": "Apakah kamu yakin ingin menghapus profil konfigurasi ini?",
"delete_custom_mode_with_rules": "Anda yakin ingin menghapus mode {scope} ini?\n\nIni juga akan menghapus folder aturan terkait di:\n{rulesFolderPath}",
"delete_message": "Apa yang ingin kamu hapus?",
"edit_warning": "Mengedit pesan ini akan menghapus semua pesan selanjutnya dalam percakapan. Apakah kamu ingin melanjutkan?",
"delete_just_this_message": "Hanya pesan ini",
"delete_this_and_subsequent": "Ini dan semua pesan selanjutnya",
"proceed": "Lanjutkan"
"delete_custom_mode_with_rules": "Anda yakin ingin menghapus mode {scope} ini?\n\nIni juga akan menghapus folder aturan terkait di:\n{rulesFolderPath}"
},
"errors": {
"invalid_data_uri": "Format data URI tidak valid",
Expand Down
7 changes: 1 addition & 6 deletions src/i18n/locales/it/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,7 @@
"confirmation": {
"reset_state": "Sei sicuro di voler reimpostare tutti gli stati e l'archiviazione segreta nell'estensione? Questa azione non può essere annullata.",
"delete_config_profile": "Sei sicuro di voler eliminare questo profilo di configurazione?",
"delete_custom_mode_with_rules": "Sei sicuro di voler eliminare questa modalità {scope}?\n\nQuesto eliminerà anche la cartella delle regole associata in:\n{rulesFolderPath}",
"delete_message": "Cosa desideri eliminare?",
"edit_warning": "Modificare questo messaggio eliminerà tutti i messaggi successivi nella conversazione. Vuoi continuare?",
"delete_just_this_message": "Solo questo messaggio",
"delete_this_and_subsequent": "Questo e tutti i messaggi successivi",
"proceed": "Continua"
"delete_custom_mode_with_rules": "Sei sicuro di voler eliminare questa modalità {scope}?\n\nQuesto eliminerà anche la cartella delle regole associata in:\n{rulesFolderPath}"
},
"errors": {
"invalid_data_uri": "Formato URI dati non valido",
Expand Down
7 changes: 1 addition & 6 deletions src/i18n/locales/ja/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,7 @@
"confirmation": {
"reset_state": "拡張機能のすべての状態とシークレットストレージをリセットしてもよろしいですか?この操作は元に戻せません。",
"delete_config_profile": "この設定プロファイルを削除してもよろしいですか?",
"delete_custom_mode_with_rules": "この{scope}モードを削除してもよろしいですか?\n\nこれにより、関連するルールフォルダも次の場所で削除されます:\n{rulesFolderPath}",
"delete_message": "何を削除しますか?",
"edit_warning": "このメッセージを編集すると、会話内のすべての後続メッセージが削除されます。続行しますか?",
"delete_just_this_message": "このメッセージのみ",
"delete_this_and_subsequent": "これ以降のすべてのメッセージ",
"proceed": "続行"
"delete_custom_mode_with_rules": "この{scope}モードを削除してもよろしいですか?\n\nこれにより、関連するルールフォルダも次の場所で削除されます:\n{rulesFolderPath}"
},
"errors": {
"invalid_data_uri": "データURIフォーマットが無効です",
Expand Down
Loading