Skip to content

Commit fb374b3

Browse files
Message edit/delete overhaul (#5538)
* improved chat row first pass * big UI improvements * working functionality * tests working * ok finally tests working for real! * translations * add back hidden flag * remove option to skip notif * fixed image issue * ui fix * put back edit flag * oops test fix * reduce margins * code review
1 parent 6cf376f commit fb374b3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+1460
-710
lines changed

src/core/webview/__tests__/ClineProvider.spec.ts

Lines changed: 216 additions & 137 deletions
Large diffs are not rendered by default.

src/core/webview/__tests__/webviewMessageHandler.spec.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,13 @@ const mockClineProvider = {
2828
globalStorageUri: { fsPath: "/mock/global/storage" },
2929
},
3030
setValue: vi.fn(),
31+
getValue: vi.fn(),
3132
},
3233
log: vi.fn(),
3334
postStateToWebview: vi.fn(),
35+
getCurrentCline: vi.fn(),
36+
getTaskWithId: vi.fn(),
37+
initClineWithHistoryItem: vi.fn(),
3438
} as unknown as ClineProvider
3539

3640
import { t } from "../../../i18n"
@@ -482,3 +486,51 @@ describe("webviewMessageHandler - deleteCustomMode", () => {
482486
expect(mockClineProvider.postMessageToWebview).not.toHaveBeenCalled()
483487
})
484488
})
489+
490+
describe("webviewMessageHandler - message dialog preferences", () => {
491+
beforeEach(() => {
492+
vi.clearAllMocks()
493+
// Mock a current Cline instance
494+
vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({
495+
taskId: "test-task-id",
496+
apiConversationHistory: [],
497+
clineMessages: [],
498+
} as any)
499+
// Reset getValue mock
500+
vi.mocked(mockClineProvider.contextProxy.getValue).mockReturnValue(false)
501+
})
502+
503+
describe("deleteMessage", () => {
504+
it("should always show dialog for delete confirmation", async () => {
505+
vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({} as any) // Mock current cline exists
506+
507+
await webviewMessageHandler(mockClineProvider, {
508+
type: "deleteMessage",
509+
value: 123456789, // Changed from messageTs to value
510+
})
511+
512+
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
513+
type: "showDeleteMessageDialog",
514+
messageTs: 123456789,
515+
})
516+
})
517+
})
518+
519+
describe("submitEditedMessage", () => {
520+
it("should always show dialog for edit confirmation", async () => {
521+
vi.mocked(mockClineProvider.getCurrentCline).mockReturnValue({} as any) // Mock current cline exists
522+
523+
await webviewMessageHandler(mockClineProvider, {
524+
type: "submitEditedMessage",
525+
value: 123456789, // messageTs as number
526+
editedMessageContent: "edited content", // text content in editedMessageContent field
527+
})
528+
529+
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
530+
type: "showEditMessageDialog",
531+
messageTs: 123456789,
532+
text: "edited content",
533+
})
534+
})
535+
})
536+
})

src/core/webview/webviewMessageHandler.ts

Lines changed: 58 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -77,55 +77,6 @@ export const webviewMessageHandler = async (
7777
return { messageIndex, apiConversationHistoryIndex }
7878
}
7979

80-
/**
81-
* Removes just the target message, preserving messages after the next user message
82-
*/
83-
const removeMessagesJustThis = async (
84-
currentCline: any,
85-
messageIndex: number,
86-
apiConversationHistoryIndex: number,
87-
) => {
88-
// Find the next user message first
89-
const nextUserMessage = currentCline.clineMessages
90-
.slice(messageIndex + 1)
91-
.find((msg: ClineMessage) => msg.type === "say" && msg.say === "user_feedback")
92-
93-
// Handle UI messages
94-
if (nextUserMessage) {
95-
// Find absolute index of next user message
96-
const nextUserMessageIndex = currentCline.clineMessages.findIndex(
97-
(msg: ClineMessage) => msg === nextUserMessage,
98-
)
99-
100-
// Keep messages before current message and after next user message
101-
await currentCline.overwriteClineMessages([
102-
...currentCline.clineMessages.slice(0, messageIndex),
103-
...currentCline.clineMessages.slice(nextUserMessageIndex),
104-
])
105-
} else {
106-
// If no next user message, keep only messages before current message
107-
await currentCline.overwriteClineMessages(currentCline.clineMessages.slice(0, messageIndex))
108-
}
109-
110-
// Handle API messages
111-
if (apiConversationHistoryIndex !== -1) {
112-
if (nextUserMessage && nextUserMessage.ts) {
113-
// Keep messages before current API message and after next user message
114-
await currentCline.overwriteApiConversationHistory([
115-
...currentCline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
116-
...currentCline.apiConversationHistory.filter(
117-
(msg: ApiMessage) => msg.ts && msg.ts >= nextUserMessage.ts,
118-
),
119-
])
120-
} else {
121-
// If no next user message, keep only messages before current API message
122-
await currentCline.overwriteApiConversationHistory(
123-
currentCline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
124-
)
125-
}
126-
}
127-
}
128-
12980
/**
13081
* Removes the target message and all subsequent messages
13182
*/
@@ -148,34 +99,28 @@ export const webviewMessageHandler = async (
14899
* Handles message deletion operations with user confirmation
149100
*/
150101
const handleDeleteOperation = async (messageTs: number): Promise<void> => {
151-
const options = [
152-
t("common:confirmation.delete_just_this_message"),
153-
t("common:confirmation.delete_this_and_subsequent"),
154-
]
155-
156-
const answer = await vscode.window.showInformationMessage(
157-
t("common:confirmation.delete_message"),
158-
{ modal: true },
159-
...options,
160-
)
102+
// Send message to webview to show delete confirmation dialog
103+
await provider.postMessageToWebview({
104+
type: "showDeleteMessageDialog",
105+
messageTs,
106+
})
107+
}
161108

162-
// Only proceed if user selected one of the options and we have a current cline
163-
if (answer && options.includes(answer) && provider.getCurrentCline()) {
109+
/**
110+
* Handles confirmed message deletion from webview dialog
111+
*/
112+
const handleDeleteMessageConfirm = async (messageTs: number): Promise<void> => {
113+
// Only proceed if we have a current cline
114+
if (provider.getCurrentCline()) {
164115
const currentCline = provider.getCurrentCline()!
165116
const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline)
166117

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

171-
// Check which option the user selected
172-
if (answer === options[0]) {
173-
// Delete just this message
174-
await removeMessagesJustThis(currentCline, messageIndex, apiConversationHistoryIndex)
175-
} else if (answer === options[1]) {
176-
// Delete this message and all subsequent
177-
await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex)
178-
}
122+
// Delete this message and all subsequent messages
123+
await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex)
179124

180125
// Initialize with history item after deletion
181126
await provider.initClineWithHistoryItem(historyItem)
@@ -192,15 +137,26 @@ export const webviewMessageHandler = async (
192137
/**
193138
* Handles message editing operations with user confirmation
194139
*/
195-
const handleEditOperation = async (messageTs: number, editedContent: string): Promise<void> => {
196-
const answer = await vscode.window.showWarningMessage(
197-
t("common:confirmation.edit_warning"),
198-
{ modal: true },
199-
t("common:confirmation.proceed"),
200-
)
140+
const handleEditOperation = async (messageTs: number, editedContent: string, images?: string[]): Promise<void> => {
141+
// Send message to webview to show edit confirmation dialog
142+
await provider.postMessageToWebview({
143+
type: "showEditMessageDialog",
144+
messageTs,
145+
text: editedContent,
146+
images,
147+
})
148+
}
201149

202-
// Only proceed if user selected "Proceed" and we have a current cline
203-
if (answer === t("common:confirmation.proceed") && provider.getCurrentCline()) {
150+
/**
151+
* Handles confirmed message editing from webview dialog
152+
*/
153+
const handleEditMessageConfirm = async (
154+
messageTs: number,
155+
editedContent: string,
156+
images?: string[],
157+
): Promise<void> => {
158+
// Only proceed if we have a current cline
159+
if (provider.getCurrentCline()) {
204160
const currentCline = provider.getCurrentCline()!
205161

206162
// Use findMessageIndices to find messages based on timestamp
@@ -217,6 +173,7 @@ export const webviewMessageHandler = async (
217173
type: "askResponse",
218174
askResponse: "messageResponse",
219175
text: editedContent,
176+
images,
220177
})
221178

222179
// Don't initialize with history item for edit operations
@@ -242,11 +199,12 @@ export const webviewMessageHandler = async (
242199
messageTs: number,
243200
operation: "delete" | "edit",
244201
editedContent?: string,
202+
images?: string[],
245203
): Promise<void> => {
246204
if (operation === "delete") {
247205
await handleDeleteOperation(messageTs)
248206
} else if (operation === "edit" && editedContent) {
249-
await handleEditOperation(messageTs, editedContent)
207+
await handleEditOperation(messageTs, editedContent, images)
250208
}
251209
}
252210

@@ -416,7 +374,12 @@ export const webviewMessageHandler = async (
416374
break
417375
case "selectImages":
418376
const images = await selectImages()
419-
await provider.postMessageToWebview({ type: "selectedImages", images })
377+
await provider.postMessageToWebview({
378+
type: "selectedImages",
379+
images,
380+
context: message.context,
381+
messageTs: message.messageTs,
382+
})
420383
break
421384
case "exportCurrentTask":
422385
const currentTaskId = provider.getCurrentCline()?.taskId
@@ -1209,7 +1172,12 @@ export const webviewMessageHandler = async (
12091172
message.value &&
12101173
message.editedMessageContent
12111174
) {
1212-
await handleMessageModificationsOperation(message.value, "edit", message.editedMessageContent)
1175+
await handleMessageModificationsOperation(
1176+
message.value,
1177+
"edit",
1178+
message.editedMessageContent,
1179+
message.images,
1180+
)
12131181
}
12141182
break
12151183
}
@@ -1542,6 +1510,16 @@ export const webviewMessageHandler = async (
15421510
}
15431511
}
15441512
break
1513+
case "deleteMessageConfirm":
1514+
if (message.messageTs) {
1515+
await handleDeleteMessageConfirm(message.messageTs)
1516+
}
1517+
break
1518+
case "editMessageConfirm":
1519+
if (message.messageTs && message.text) {
1520+
await handleEditMessageConfirm(message.messageTs, message.text, message.images)
1521+
}
1522+
break
15451523
case "getListApiConfiguration":
15461524
try {
15471525
const listApiConfig = await provider.providerSettingsManager.listConfig()

src/i18n/locales/ca/common.json

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,7 @@
2121
"confirmation": {
2222
"reset_state": "Estàs segur que vols restablir tots els estats i emmagatzematge secret a l'extensió? Això no es pot desfer.",
2323
"delete_config_profile": "Estàs segur que vols eliminar aquest perfil de configuració?",
24-
"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}",
25-
"delete_message": "Què vols eliminar?",
26-
"edit_warning": "Editar aquest missatge eliminarà tots els missatges posteriors de la conversa. Vols continuar?",
27-
"delete_just_this_message": "Només aquest missatge",
28-
"delete_this_and_subsequent": "Aquest i tots els missatges posteriors",
29-
"proceed": "Continuar"
24+
"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}"
3025
},
3126
"errors": {
3227
"invalid_data_uri": "Format d'URI de dades no vàlid",

src/i18n/locales/de/common.json

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,7 @@
1717
"confirmation": {
1818
"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.",
1919
"delete_config_profile": "Möchtest du dieses Konfigurationsprofil wirklich löschen?",
20-
"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}",
21-
"delete_message": "Was möchtest du löschen?",
22-
"edit_warning": "Das Bearbeiten dieser Nachricht wird alle nachfolgenden Nachrichten in der Unterhaltung löschen. Möchtest du fortfahren?",
23-
"delete_just_this_message": "Nur diese Nachricht",
24-
"delete_this_and_subsequent": "Diese und alle nachfolgenden Nachrichten",
25-
"proceed": "Fortfahren"
20+
"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}"
2621
},
2722
"errors": {
2823
"invalid_data_uri": "Ungültiges Daten-URI-Format",

src/i18n/locales/en/common.json

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,7 @@
1717
"confirmation": {
1818
"reset_state": "Are you sure you want to reset all state and secret storage in the extension? This cannot be undone.",
1919
"delete_config_profile": "Are you sure you want to delete this configuration profile?",
20-
"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}",
21-
"delete_message": "What would you like to delete?",
22-
"edit_warning": "Editing this message will delete all subsequent messages in the conversation. Do you want to proceed?",
23-
"delete_just_this_message": "Just this message",
24-
"delete_this_and_subsequent": "This and all subsequent messages",
25-
"proceed": "Proceed"
20+
"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}"
2621
},
2722
"errors": {
2823
"invalid_data_uri": "Invalid data URI format",

src/i18n/locales/es/common.json

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,7 @@
1717
"confirmation": {
1818
"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.",
1919
"delete_config_profile": "¿Estás seguro de que deseas eliminar este perfil de configuración?",
20-
"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}",
21-
"delete_message": "¿Qué deseas eliminar?",
22-
"edit_warning": "Editar este mensaje eliminará todos los mensajes posteriores en la conversación. ¿Deseas continuar?",
23-
"delete_just_this_message": "Solo este mensaje",
24-
"delete_this_and_subsequent": "Este y todos los mensajes posteriores",
25-
"proceed": "Continuar"
20+
"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}"
2621
},
2722
"errors": {
2823
"invalid_data_uri": "Formato de URI de datos no válido",

src/i18n/locales/fr/common.json

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,7 @@
1717
"confirmation": {
1818
"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.",
1919
"delete_config_profile": "Êtes-vous sûr de vouloir supprimer ce profil de configuration ?",
20-
"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}",
21-
"delete_message": "Que souhaitez-vous supprimer ?",
22-
"edit_warning": "Modifier ce message supprimera tous les messages suivants dans la conversation. Voulez-vous continuer ?",
23-
"delete_just_this_message": "Uniquement ce message",
24-
"delete_this_and_subsequent": "Ce message et tous les messages suivants",
25-
"proceed": "Continuer"
20+
"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}"
2621
},
2722
"errors": {
2823
"invalid_data_uri": "Format d'URI de données invalide",

src/i18n/locales/hi/common.json

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,7 @@
1717
"confirmation": {
1818
"reset_state": "क्या आप वाकई एक्सटेंशन में सभी स्टेट और गुप्त स्टोरेज रीसेट करना चाहते हैं? इसे पूर्ववत नहीं किया जा सकता है।",
1919
"delete_config_profile": "क्या आप वाकई इस कॉन्फ़िगरेशन प्रोफ़ाइल को हटाना चाहते हैं?",
20-
"delete_custom_mode_with_rules": "क्या आप वाकई इस {scope} मोड को हटाना चाहते हैं?\n\nयह संबंधित नियम फ़ोल्डर को भी यहाँ हटा देगा:\n{rulesFolderPath}",
21-
"delete_message": "आप क्या हटाना चाहते हैं?",
22-
"edit_warning": "इस संदेश को संपादित करने से बातचीत के सभी बाद के संदेश हट जाएंगे। क्या आप जारी रखना चाहते हैं?",
23-
"delete_just_this_message": "सिर्फ यह संदेश",
24-
"delete_this_and_subsequent": "यह और सभी बाद के संदेश",
25-
"proceed": "जारी रखें"
20+
"delete_custom_mode_with_rules": "क्या आप वाकई इस {scope} मोड को हटाना चाहते हैं?\n\nयह संबंधित नियम फ़ोल्डर को भी यहाँ हटा देगा:\n{rulesFolderPath}"
2621
},
2722
"errors": {
2823
"invalid_data_uri": "अमान्य डेटा URI फॉर्मेट",

src/i18n/locales/id/common.json

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,7 @@
1717
"confirmation": {
1818
"reset_state": "Apakah kamu yakin ingin mereset semua state dan secret storage di ekstensi? Ini tidak dapat dibatalkan.",
1919
"delete_config_profile": "Apakah kamu yakin ingin menghapus profil konfigurasi ini?",
20-
"delete_custom_mode_with_rules": "Anda yakin ingin menghapus mode {scope} ini?\n\nIni juga akan menghapus folder aturan terkait di:\n{rulesFolderPath}",
21-
"delete_message": "Apa yang ingin kamu hapus?",
22-
"edit_warning": "Mengedit pesan ini akan menghapus semua pesan selanjutnya dalam percakapan. Apakah kamu ingin melanjutkan?",
23-
"delete_just_this_message": "Hanya pesan ini",
24-
"delete_this_and_subsequent": "Ini dan semua pesan selanjutnya",
25-
"proceed": "Lanjutkan"
20+
"delete_custom_mode_with_rules": "Anda yakin ingin menghapus mode {scope} ini?\n\nIni juga akan menghapus folder aturan terkait di:\n{rulesFolderPath}"
2621
},
2722
"errors": {
2823
"invalid_data_uri": "Format data URI tidak valid",

0 commit comments

Comments
 (0)