Skip to content

Commit b40a5e8

Browse files
committed
fix: prevent edited messages from being linked to pending tool calls
1 parent d909a50 commit b40a5e8

File tree

3 files changed

+44
-15
lines changed

3 files changed

+44
-15
lines changed

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,8 @@ describe("webviewMessageHandler delete functionality", () => {
111111
expect(getCurrentTaskMock.overwriteClineMessages).toHaveBeenCalledWith([])
112112

113113
// When message is not found in API history (index is -1),
114-
// API history should not be modified
115-
expect(getCurrentTaskMock.overwriteApiConversationHistory).not.toHaveBeenCalled()
114+
// API history should be truncated from the first API message at/after the deleted timestamp (fallback)
115+
expect(getCurrentTaskMock.overwriteApiConversationHistory).toHaveBeenCalledWith([])
116116
})
117117

118118
it("should handle deletion when exact apiConversationHistoryIndex is found", async () => {
@@ -203,8 +203,8 @@ describe("webviewMessageHandler delete functionality", () => {
203203
// Verify that clineMessages was truncated
204204
expect(getCurrentTaskMock.overwriteClineMessages).toHaveBeenCalledWith([])
205205

206-
// API history should not be modified when message not found
207-
expect(getCurrentTaskMock.overwriteApiConversationHistory).not.toHaveBeenCalled()
206+
// API history should be truncated from first message at/after deleted timestamp (fallback)
207+
expect(getCurrentTaskMock.overwriteApiConversationHistory).toHaveBeenCalledWith([])
208208
})
209209

210210
it("should preserve messages before the deleted one", async () => {

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

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,8 @@ describe("webviewMessageHandler - Edit Message with Timestamp Fallback", () => {
134134
[], // All messages before index 0 (empty array)
135135
)
136136

137-
// API history should not be modified when message not found
138-
expect(mockCurrentTask.overwriteApiConversationHistory).not.toHaveBeenCalled()
137+
// API history should be truncated from first message at/after edited timestamp (fallback)
138+
expect(mockCurrentTask.overwriteApiConversationHistory).toHaveBeenCalledWith([])
139139
})
140140

141141
it("should preserve messages before the edited message when message not in API history", async () => {
@@ -196,8 +196,14 @@ describe("webviewMessageHandler - Edit Message with Timestamp Fallback", () => {
196196
},
197197
])
198198

199-
// API history should not be modified when message not found
200-
expect(mockCurrentTask.overwriteApiConversationHistory).not.toHaveBeenCalled()
199+
// API history should be truncated from the first API message at/after the edited timestamp (fallback)
200+
expect(mockCurrentTask.overwriteApiConversationHistory).toHaveBeenCalledWith([
201+
{
202+
ts: earlierMessageTs,
203+
role: "user",
204+
content: [{ type: "text", text: "Earlier message" }],
205+
},
206+
])
201207
})
202208

203209
it("should not use fallback when exact apiConversationHistoryIndex is found", async () => {
@@ -281,7 +287,7 @@ describe("webviewMessageHandler - Edit Message with Timestamp Fallback", () => {
281287
// UI messages truncated
282288
expect(mockCurrentTask.overwriteClineMessages).toHaveBeenCalledWith([])
283289

284-
// API history should not be modified when no matching messages found
290+
// API history should not be modified when no API messages meet the timestamp criteria
285291
expect(mockCurrentTask.overwriteApiConversationHistory).not.toHaveBeenCalled()
286292
})
287293

@@ -378,7 +384,7 @@ describe("webviewMessageHandler - Edit Message with Timestamp Fallback", () => {
378384
// UI messages truncated at edited message
379385
expect(mockCurrentTask.overwriteClineMessages).toHaveBeenCalledWith([])
380386

381-
// API history should not be modified when message not found
382-
expect(mockCurrentTask.overwriteApiConversationHistory).not.toHaveBeenCalled()
387+
// API history should be truncated from first message at/after edited timestamp (fallback)
388+
expect(mockCurrentTask.overwriteApiConversationHistory).toHaveBeenCalledWith([])
383389
})
384390
})

src/core/webview/webviewMessageHandler.ts

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,17 @@ export const webviewMessageHandler = async (
8484
return { messageIndex, apiConversationHistoryIndex }
8585
}
8686

87+
/**
88+
* Fallback: find first API history index at or after a timestamp.
89+
* Used when the exact user message isn't present in apiConversationHistory (e.g., after condense).
90+
*/
91+
const findFirstApiIndexAtOrAfter = (ts: number, currentCline: any) => {
92+
if (typeof ts !== "number") return -1
93+
return currentCline.apiConversationHistory.findIndex(
94+
(msg: ApiMessage) => typeof msg?.ts === "number" && (msg.ts as number) >= ts,
95+
)
96+
}
97+
8798
/**
8899
* Removes the target message and all subsequent messages
89100
*/
@@ -144,6 +155,12 @@ export const webviewMessageHandler = async (
144155
}
145156

146157
const { messageIndex, apiConversationHistoryIndex } = findMessageIndices(messageTs, currentCline)
158+
// Determine API truncation index with timestamp fallback if exact match not found
159+
let apiIndexToUse = apiConversationHistoryIndex
160+
const tsThreshold = currentCline.clineMessages[messageIndex]?.ts
161+
if (apiIndexToUse === -1 && typeof tsThreshold === "number") {
162+
apiIndexToUse = findFirstApiIndexAtOrAfter(tsThreshold, currentCline)
163+
}
147164

148165
if (messageIndex === -1) {
149166
const errorMessage = `Message with timestamp ${messageTs} not found`
@@ -189,7 +206,7 @@ export const webviewMessageHandler = async (
189206
}
190207

191208
// Delete this message and all subsequent messages
192-
await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex)
209+
await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiIndexToUse)
193210

194211
// Restore checkpoint associations for preserved messages
195212
for (const [ts, checkpoint] of preservedCheckpoints) {
@@ -336,6 +353,14 @@ export const webviewMessageHandler = async (
336353
}
337354
}
338355

356+
// Timestamp fallback for API history when exact user message isn't present
357+
if (deleteFromApiIndex === -1) {
358+
const tsThresholdForEdit = currentCline.clineMessages[deleteFromMessageIndex]?.ts
359+
if (typeof tsThresholdForEdit === "number") {
360+
deleteFromApiIndex = findFirstApiIndexAtOrAfter(tsThresholdForEdit, currentCline)
361+
}
362+
}
363+
339364
// Store checkpoints from messages that will be preserved
340365
const preservedCheckpoints = new Map<number, any>()
341366
for (let i = 0; i < deleteFromMessageIndex; i++) {
@@ -366,9 +391,7 @@ export const webviewMessageHandler = async (
366391
// Update the UI to reflect the deletion
367392
await provider.postStateToWebview()
368393

369-
// Immediately send the edited message as a new user message
370-
// This restarts the conversation from the edited point
371-
await currentCline.handleWebviewAskResponse("messageResponse", editedContent, images)
394+
await currentCline.submitUserMessage(editedContent, images)
372395
} catch (error) {
373396
console.error("Error in edit message:", error)
374397
vscode.window.showErrorMessage(

0 commit comments

Comments
 (0)