Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
32 changes: 25 additions & 7 deletions src/core/webview/__tests__/ClineProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1222,12 +1222,19 @@ describe("ClineProvider", () => {
await messageHandler({ type: "deleteMessageConfirm", messageTs: 4000 })

// Verify only messages before the deleted message were kept
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0], mockMessages[1]])
// With exact timestamp matching, we find the message at index 3 (ts: 4000)
// and keep messages 0, 1, and 2 (all before timestamp 4000)
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job updating the test expectations to match the new exact timestamp behavior. Consider adding a dedicated test case specifically for the function to ensure it handles edge cases like duplicate timestamps correctly.

mockMessages[0],
mockMessages[1],
mockMessages[2],
])

// Verify only API messages before the deleted message were kept
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([
mockApiHistory[0],
mockApiHistory[1],
mockApiHistory[2],
])

// Verify createTaskWithHistoryItem was called
Expand Down Expand Up @@ -1319,12 +1326,19 @@ describe("ClineProvider", () => {
})

// Verify correct messages were kept (only messages before the edited one)
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0], mockMessages[1]])
// With exact timestamp matching, we find the message at index 3 (ts: 4000)
// and keep messages 0, 1, and 2 (all before timestamp 4000)
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([
mockMessages[0],
mockMessages[1],
mockMessages[2],
])

// Verify correct API messages were kept (only messages before the edited one)
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([
mockApiHistory[0],
mockApiHistory[1],
mockApiHistory[2],
])

// The new flow calls webviewMessageHandler recursively with askResponse
Expand Down Expand Up @@ -3016,9 +3030,11 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
text: "Edited message with preserved images",
})

// Verify messages were edited correctly - only the first message should remain
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0]])
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([{ ts: 1000 }])
// Verify messages were edited correctly
// With exact timestamp matching, we find the message at index 2 (ts: 3000)
// and keep messages 0 and 1 (all before timestamp 3000)
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0], mockMessages[1]])
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([{ ts: 1000 }, { ts: 2000 }])
})

test("handles editing messages with file attachments", async () => {
Expand Down Expand Up @@ -3632,8 +3648,10 @@ describe("ClineProvider - Comprehensive Edit/Delete Edge Cases", () => {
await messageHandler({ type: "deleteMessageConfirm", messageTs: 3000 })

// Should handle large payloads without issues
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0]])
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([{ ts: 1000 }])
// With exact timestamp matching, we find the message at index 2 (ts: 3000)
// and keep messages 0 and 1 (all before timestamp 3000)
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([mockMessages[0], mockMessages[1]])
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([{ ts: 1000 }, { ts: 2000 }])
})
})

Expand Down
63 changes: 54 additions & 9 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,32 +67,53 @@ export const webviewMessageHandler = async (
await provider.contextProxy.setValue(key, value)

/**
* Shared utility to find message indices based on timestamp
* Shared utility to find message indices based on exact timestamp matching
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good addition of validation! Consider adding a comment explaining why we use exact timestamp matching instead of the previous buffer approach, perhaps referencing issue #6932 for future maintainers?

* This prevents accidental deletion of unrelated messages
*/
const findMessageIndices = (messageTs: number, currentCline: any) => {
const timeCutoff = messageTs - 1000 // 1 second buffer before the message
const messageIndex = currentCline.clineMessages.findIndex((msg: ClineMessage) => msg.ts && msg.ts >= timeCutoff)
// Use exact timestamp matching to prevent unintended message deletion
const messageIndex = currentCline.clineMessages.findIndex((msg: ClineMessage) => msg.ts === messageTs)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While this exact timestamp matching fixes the immediate issue, consider what happens if multiple messages share the same timestamp. Would adding a secondary check using message content or index as a tiebreaker make this more robust?

const apiConversationHistoryIndex = currentCline.apiConversationHistory.findIndex(
(msg: ApiMessage) => msg.ts && msg.ts >= timeCutoff,
(msg: ApiMessage) => msg.ts === messageTs,
)
return { messageIndex, apiConversationHistoryIndex }
}

/**
* Removes the target message and all subsequent messages
* Includes validation to prevent accidental data loss
*/
const removeMessagesThisAndSubsequent = async (
currentCline: any,
messageIndex: number,
apiConversationHistoryIndex: number,
) => {
// Validate indices before deletion
if (messageIndex < 0 || messageIndex >= currentCline.clineMessages.length) {
console.error(
`[Chat History] Invalid message index: ${messageIndex}, total messages: ${currentCline.clineMessages.length}`,
)
throw new Error("Invalid message index for deletion")
}

// Log deletion for debugging
const messagesToDelete = currentCline.clineMessages.length - messageIndex
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice defensive programming with the validation checks. For consistency, could we standardize all console logs in this file to use the prefix? Some logs have it, others don't.

console.log(`[Chat History] Deleting ${messagesToDelete} messages starting from index ${messageIndex}`)

// Delete this message and all that follow
await currentCline.overwriteClineMessages(currentCline.clineMessages.slice(0, messageIndex))

if (apiConversationHistoryIndex !== -1) {
await currentCline.overwriteApiConversationHistory(
currentCline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
)
if (
apiConversationHistoryIndex >= 0 &&
apiConversationHistoryIndex < currentCline.apiConversationHistory.length
) {
await currentCline.overwriteApiConversationHistory(
currentCline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
)
} else {
console.warn(`[Chat History] Invalid API conversation history index: ${apiConversationHistoryIndex}`)
}
}
}

Expand All @@ -118,6 +139,16 @@ export const webviewMessageHandler = async (

if (messageIndex !== -1) {
try {
// Additional validation: ensure we're not deleting critical messages
const messageToDelete = currentCline.clineMessages[messageIndex]
if (!messageToDelete) {
console.error(`[Chat History] Message not found at index ${messageIndex}`)
throw new Error("Message not found for deletion")
}

// Log the message being deleted for debugging
console.log(`[Chat History] Deleting message with timestamp ${messageTs} at index ${messageIndex}`)

const { historyItem } = await provider.getTaskWithId(currentCline.taskId)

// Delete this message and all subsequent messages
Expand All @@ -126,11 +157,13 @@ export const webviewMessageHandler = async (
// Initialize with history item after deletion
await provider.createTaskWithHistoryItem(historyItem)
} catch (error) {
console.error("Error in delete message:", error)
console.error("[Chat History] Error in delete message:", error)
vscode.window.showErrorMessage(
`Error deleting message: ${error instanceof Error ? error.message : String(error)}`,
)
}
} else {
console.warn(`[Chat History] Message with timestamp ${messageTs} not found for deletion`)
}
}
}
Expand Down Expand Up @@ -165,6 +198,16 @@ export const webviewMessageHandler = async (

if (messageIndex !== -1) {
try {
// Additional validation: ensure we're not editing critical messages
const messageToEdit = currentCline.clineMessages[messageIndex]
if (!messageToEdit) {
console.error(`[Chat History] Message not found at index ${messageIndex}`)
throw new Error("Message not found for editing")
}

// Log the message being edited for debugging
console.log(`[Chat History] Editing message with timestamp ${messageTs} at index ${messageIndex}`)

// Edit this message and delete subsequent
await removeMessagesThisAndSubsequent(currentCline, messageIndex, apiConversationHistoryIndex)

Expand All @@ -180,11 +223,13 @@ export const webviewMessageHandler = async (
// Don't initialize with history item for edit operations
// The webviewMessageHandler will handle the conversation state
} catch (error) {
console.error("Error in edit message:", error)
console.error("[Chat History] Error in edit message:", error)
vscode.window.showErrorMessage(
`Error editing message: ${error instanceof Error ? error.message : String(error)}`,
)
}
} else {
console.warn(`[Chat History] Message with timestamp ${messageTs} not found for editing`)
}
}
}
Expand Down
Loading