Skip to content

Commit a9a87c2

Browse files
Make the prompts editable now (#5359)
1 parent edce187 commit a9a87c2

File tree

22 files changed

+1464
-153
lines changed

22 files changed

+1464
-153
lines changed

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

Lines changed: 995 additions & 2 deletions
Large diffs are not rendered by default.

src/core/webview/webviewMessageHandler.ts

Lines changed: 211 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,16 @@ import pWaitFor from "p-wait-for"
66
import * as vscode from "vscode"
77
import * as yaml from "yaml"
88

9-
import { type Language, type ProviderSettings, type GlobalState, TelemetryEventName } from "@roo-code/types"
9+
import {
10+
type Language,
11+
type ProviderSettings,
12+
type GlobalState,
13+
type ClineMessage,
14+
TelemetryEventName,
15+
} from "@roo-code/types"
1016
import { CloudService } from "@roo-code/cloud"
1117
import { TelemetryService } from "@roo-code/telemetry"
18+
import { type ApiMessage } from "../task-persistence/apiMessages"
1219

1320
import { ClineProvider } from "./ClineProvider"
1421
import { changeLanguage, t } from "../../i18n"
@@ -58,6 +65,200 @@ export const webviewMessageHandler = async (
5865
const updateGlobalState = async <K extends keyof GlobalState>(key: K, value: GlobalState[K]) =>
5966
await provider.contextProxy.setValue(key, value)
6067

68+
/**
69+
* Shared utility to find message indices based on timestamp
70+
*/
71+
const findMessageIndices = (messageTs: number, currentCline: any) => {
72+
const timeCutoff = messageTs - 1000 // 1 second buffer before the message
73+
const messageIndex = currentCline.clineMessages.findIndex((msg: ClineMessage) => msg.ts && msg.ts >= timeCutoff)
74+
const apiConversationHistoryIndex = currentCline.apiConversationHistory.findIndex(
75+
(msg: ApiMessage) => msg.ts && msg.ts >= timeCutoff,
76+
)
77+
return { messageIndex, apiConversationHistoryIndex }
78+
}
79+
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+
129+
/**
130+
* Removes the target message and all subsequent messages
131+
*/
132+
const removeMessagesThisAndSubsequent = async (
133+
currentCline: any,
134+
messageIndex: number,
135+
apiConversationHistoryIndex: number,
136+
) => {
137+
// Delete this message and all that follow
138+
await currentCline.overwriteClineMessages(currentCline.clineMessages.slice(0, messageIndex))
139+
140+
if (apiConversationHistoryIndex !== -1) {
141+
await currentCline.overwriteApiConversationHistory(
142+
currentCline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
143+
)
144+
}
145+
}
146+
147+
/**
148+
* Handles message deletion operations with user confirmation
149+
*/
150+
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+
)
161+
162+
// Only proceed if user selected one of the options and we have a current cline
163+
if (answer && options.includes(answer) && provider.getCurrentCline()) {
164+
const currentCline = provider.getCurrentCline()!
165+
const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline)
166+
167+
if (messageIndex !== -1) {
168+
try {
169+
const { historyItem } = await provider.getTaskWithId(currentCline.taskId)
170+
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+
}
179+
180+
// Initialize with history item after deletion
181+
await provider.initClineWithHistoryItem(historyItem)
182+
} catch (error) {
183+
console.error("Error in delete message:", error)
184+
vscode.window.showErrorMessage(
185+
`Error deleting message: ${error instanceof Error ? error.message : String(error)}`,
186+
)
187+
}
188+
}
189+
}
190+
}
191+
192+
/**
193+
* Handles message editing operations with user confirmation
194+
*/
195+
const handleEditOperation = async (messageTs: number, editedContent: string): Promise<void> => {
196+
const options = [
197+
t("common:confirmation.edit_this_and_delete_subsequent"),
198+
t("common:confirmation.edit_just_this_message"),
199+
]
200+
201+
const answer = await vscode.window.showInformationMessage(
202+
t("common:confirmation.edit_message"),
203+
{ modal: true },
204+
...options,
205+
)
206+
207+
// Only proceed if user selected one of the options and we have a current cline
208+
if (answer && options.includes(answer) && provider.getCurrentCline()) {
209+
const currentCline = provider.getCurrentCline()!
210+
const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline)
211+
212+
if (messageIndex !== -1) {
213+
try {
214+
// Check which option the user selected
215+
if (answer === options[0]) {
216+
// Edit this message and delete subsequent
217+
await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex)
218+
} else if (answer === options[1]) {
219+
// Edit just this message
220+
await removeMessagesJustThis(currentCline, messageIndex, apiConversationHistoryIndex)
221+
}
222+
223+
// Process the edited message as a regular user message
224+
// This will add it to the conversation and trigger an AI response
225+
webviewMessageHandler(provider, {
226+
type: "askResponse",
227+
askResponse: "messageResponse",
228+
text: editedContent,
229+
})
230+
231+
// Don't initialize with history item for edit operations
232+
// The webviewMessageHandler will handle the conversation state
233+
} catch (error) {
234+
console.error("Error in edit message:", error)
235+
vscode.window.showErrorMessage(
236+
`Error editing message: ${error instanceof Error ? error.message : String(error)}`,
237+
)
238+
}
239+
}
240+
}
241+
}
242+
243+
/**
244+
* Handles message modification operations (delete or edit) with confirmation dialog
245+
* @param messageTs Timestamp of the message to operate on
246+
* @param operation Type of operation ('delete' or 'edit')
247+
* @param editedContent New content for edit operations
248+
* @returns Promise<void>
249+
*/
250+
const handleMessageModificationsOperation = async (
251+
messageTs: number,
252+
operation: "delete" | "edit",
253+
editedContent?: string,
254+
): Promise<void> => {
255+
if (operation === "delete") {
256+
await handleDeleteOperation(messageTs)
257+
} else if (operation === "edit" && editedContent) {
258+
await handleEditOperation(messageTs, editedContent)
259+
}
260+
}
261+
61262
switch (message.type) {
62263
case "webviewDidLaunch":
63264
// Load custom modes first
@@ -989,108 +1190,19 @@ export const webviewMessageHandler = async (
9891190
}
9901191
break
9911192
case "deleteMessage": {
992-
const answer = await vscode.window.showInformationMessage(
993-
t("common:confirmation.delete_message"),
994-
{ modal: true },
995-
t("common:confirmation.just_this_message"),
996-
t("common:confirmation.this_and_subsequent"),
997-
)
998-
1193+
if (provider.getCurrentCline() && typeof message.value === "number" && message.value) {
1194+
await handleMessageModificationsOperation(message.value, "delete")
1195+
}
1196+
break
1197+
}
1198+
case "submitEditedMessage": {
9991199
if (
1000-
(answer === t("common:confirmation.just_this_message") ||
1001-
answer === t("common:confirmation.this_and_subsequent")) &&
10021200
provider.getCurrentCline() &&
10031201
typeof message.value === "number" &&
1004-
message.value
1202+
message.value &&
1203+
message.editedMessageContent
10051204
) {
1006-
const timeCutoff = message.value - 1000 // 1 second buffer before the message to delete
1007-
1008-
const messageIndex = provider
1009-
.getCurrentCline()!
1010-
.clineMessages.findIndex((msg) => msg.ts && msg.ts >= timeCutoff)
1011-
1012-
const apiConversationHistoryIndex = provider
1013-
.getCurrentCline()
1014-
?.apiConversationHistory.findIndex((msg) => msg.ts && msg.ts >= timeCutoff)
1015-
1016-
if (messageIndex !== -1) {
1017-
const { historyItem } = await provider.getTaskWithId(provider.getCurrentCline()!.taskId)
1018-
1019-
if (answer === t("common:confirmation.just_this_message")) {
1020-
// Find the next user message first
1021-
const nextUserMessage = provider
1022-
.getCurrentCline()!
1023-
.clineMessages.slice(messageIndex + 1)
1024-
.find((msg) => msg.type === "say" && msg.say === "user_feedback")
1025-
1026-
// Handle UI messages
1027-
if (nextUserMessage) {
1028-
// Find absolute index of next user message
1029-
const nextUserMessageIndex = provider
1030-
.getCurrentCline()!
1031-
.clineMessages.findIndex((msg) => msg === nextUserMessage)
1032-
1033-
// Keep messages before current message and after next user message
1034-
await provider
1035-
.getCurrentCline()!
1036-
.overwriteClineMessages([
1037-
...provider.getCurrentCline()!.clineMessages.slice(0, messageIndex),
1038-
...provider.getCurrentCline()!.clineMessages.slice(nextUserMessageIndex),
1039-
])
1040-
} else {
1041-
// If no next user message, keep only messages before current message
1042-
await provider
1043-
.getCurrentCline()!
1044-
.overwriteClineMessages(
1045-
provider.getCurrentCline()!.clineMessages.slice(0, messageIndex),
1046-
)
1047-
}
1048-
1049-
// Handle API messages
1050-
if (apiConversationHistoryIndex !== -1) {
1051-
if (nextUserMessage && nextUserMessage.ts) {
1052-
// Keep messages before current API message and after next user message
1053-
await provider
1054-
.getCurrentCline()!
1055-
.overwriteApiConversationHistory([
1056-
...provider
1057-
.getCurrentCline()!
1058-
.apiConversationHistory.slice(0, apiConversationHistoryIndex),
1059-
...provider
1060-
.getCurrentCline()!
1061-
.apiConversationHistory.filter(
1062-
(msg) => msg.ts && msg.ts >= nextUserMessage.ts,
1063-
),
1064-
])
1065-
} else {
1066-
// If no next user message, keep only messages before current API message
1067-
await provider
1068-
.getCurrentCline()!
1069-
.overwriteApiConversationHistory(
1070-
provider
1071-
.getCurrentCline()!
1072-
.apiConversationHistory.slice(0, apiConversationHistoryIndex),
1073-
)
1074-
}
1075-
}
1076-
} else if (answer === t("common:confirmation.this_and_subsequent")) {
1077-
// Delete this message and all that follow
1078-
await provider
1079-
.getCurrentCline()!
1080-
.overwriteClineMessages(provider.getCurrentCline()!.clineMessages.slice(0, messageIndex))
1081-
if (apiConversationHistoryIndex !== -1) {
1082-
await provider
1083-
.getCurrentCline()!
1084-
.overwriteApiConversationHistory(
1085-
provider
1086-
.getCurrentCline()!
1087-
.apiConversationHistory.slice(0, apiConversationHistoryIndex),
1088-
)
1089-
}
1090-
}
1091-
1092-
await provider.initClineWithHistoryItem(historyItem)
1093-
}
1205+
await handleMessageModificationsOperation(message.value, "edit", message.editedMessageContent)
10941206
}
10951207
break
10961208
}

src/i18n/locales/ca/common.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@
2323
"delete_config_profile": "Estàs segur que vols eliminar aquest perfil de configuració?",
2424
"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}",
2525
"delete_message": "Què vols eliminar?",
26-
"just_this_message": "Només aquest missatge",
27-
"this_and_subsequent": "Aquest i tots els missatges posteriors"
26+
"edit_message": "Eliminar tots els missatges després d'aquest?",
27+
"delete_just_this_message": "Només aquest missatge",
28+
"edit_just_this_message": "No, només editar aquest",
29+
"delete_this_and_subsequent": "Aquest i tots els missatges posteriors",
30+
"edit_this_and_delete_subsequent": ""
2831
},
2932
"errors": {
3033
"invalid_data_uri": "Format d'URI de dades no vàlid",
@@ -112,6 +115,11 @@
112115
"remove": "Eliminar",
113116
"keep": "Mantenir"
114117
},
118+
"buttons": {
119+
"save": "Desar",
120+
"cancel": "Cancel·lar",
121+
"edit": "Editar"
122+
},
115123
"tasks": {
116124
"canceled": "Error de tasca: Ha estat aturada i cancel·lada per l'usuari.",
117125
"deleted": "Fallada de tasca: Ha estat aturada i eliminada per l'usuari.",

src/i18n/locales/de/common.json

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@
1919
"delete_config_profile": "Möchtest du dieses Konfigurationsprofil wirklich löschen?",
2020
"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}",
2121
"delete_message": "Was möchtest du löschen?",
22-
"just_this_message": "Nur diese Nachricht",
23-
"this_and_subsequent": "Diese und alle nachfolgenden Nachrichten"
22+
"edit_message": "Alle Nachrichten nach dieser löschen?",
23+
"delete_just_this_message": "Nur diese Nachricht",
24+
"edit_just_this_message": "Nein, nur diese bearbeiten",
25+
"delete_this_and_subsequent": "Diese und alle nachfolgenden Nachrichten",
26+
"edit_this_and_delete_subsequent": "Ja"
2427
},
2528
"errors": {
2629
"invalid_data_uri": "Ungültiges Daten-URI-Format",
@@ -108,6 +111,11 @@
108111
"remove": "Entfernen",
109112
"keep": "Behalten"
110113
},
114+
"buttons": {
115+
"save": "Speichern",
116+
"cancel": "Abbrechen",
117+
"edit": "Bearbeiten"
118+
},
111119
"tasks": {
112120
"canceled": "Aufgabenfehler: Die Aufgabe wurde vom Benutzer gestoppt und abgebrochen.",
113121
"deleted": "Aufgabenfehler: Die Aufgabe wurde vom Benutzer gestoppt und gelöscht.",

0 commit comments

Comments
 (0)