mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
Allow deleting single messages
This commit is contained in:
5
.changeset/funny-candles-exist.md
Normal file
5
.changeset/funny-candles-exist.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Allow deleting single messages or all subsequent messages
|
||||
@@ -689,21 +689,65 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
||||
break
|
||||
case "deleteMessage": {
|
||||
const answer = await vscode.window.showInformationMessage(
|
||||
"Are you sure you want to delete this message and all subsequent messages?",
|
||||
"What would you like to delete?",
|
||||
{ modal: true },
|
||||
"Yes",
|
||||
"No"
|
||||
"Just this message",
|
||||
"This and all subsequent messages",
|
||||
)
|
||||
if (answer === "Yes" && this.cline && typeof message.value === 'number' && message.value) {
|
||||
if ((answer === "Just this message" || answer === "This and all subsequent messages") &&
|
||||
this.cline && typeof message.value === 'number' && message.value) {
|
||||
const timeCutoff = message.value - 1000; // 1 second buffer before the message to delete
|
||||
const messageIndex = this.cline.clineMessages.findIndex(msg => msg.ts && msg.ts >= timeCutoff)
|
||||
const apiConversationHistoryIndex = this.cline.apiConversationHistory.findIndex(msg => msg.ts && msg.ts >= timeCutoff)
|
||||
|
||||
if (messageIndex !== -1) {
|
||||
const { historyItem } = await this.getTaskWithId(this.cline.taskId)
|
||||
|
||||
if (answer === "Just this message") {
|
||||
// Find the next user message first
|
||||
const nextUserMessage = this.cline.clineMessages
|
||||
.slice(messageIndex + 1)
|
||||
.find(msg => msg.type === "say" && msg.say === "user_feedback")
|
||||
|
||||
// Handle UI messages
|
||||
if (nextUserMessage) {
|
||||
// Find absolute index of next user message
|
||||
const nextUserMessageIndex = this.cline.clineMessages.findIndex(msg => msg === nextUserMessage)
|
||||
// Keep messages before current message and after next user message
|
||||
await this.cline.overwriteClineMessages([
|
||||
...this.cline.clineMessages.slice(0, messageIndex),
|
||||
...this.cline.clineMessages.slice(nextUserMessageIndex)
|
||||
])
|
||||
} else {
|
||||
// If no next user message, keep only messages before current message
|
||||
await this.cline.overwriteClineMessages(
|
||||
this.cline.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 this.cline.overwriteApiConversationHistory([
|
||||
...this.cline.apiConversationHistory.slice(0, apiConversationHistoryIndex),
|
||||
...this.cline.apiConversationHistory.filter(msg => msg.ts && msg.ts >= nextUserMessage.ts)
|
||||
])
|
||||
} else {
|
||||
// If no next user message, keep only messages before current API message
|
||||
await this.cline.overwriteApiConversationHistory(
|
||||
this.cline.apiConversationHistory.slice(0, apiConversationHistoryIndex)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else if (answer === "This and all subsequent messages") {
|
||||
// Delete this message and all that follow
|
||||
await this.cline.overwriteClineMessages(this.cline.clineMessages.slice(0, messageIndex))
|
||||
if (apiConversationHistoryIndex !== -1) {
|
||||
await this.cline.overwriteApiConversationHistory(this.cline.apiConversationHistory.slice(0, apiConversationHistoryIndex))
|
||||
}
|
||||
}
|
||||
|
||||
await this.initClineWithHistoryItem(historyItem)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,9 @@ jest.mock('vscode', () => ({
|
||||
joinPath: jest.fn(),
|
||||
file: jest.fn()
|
||||
},
|
||||
window: {
|
||||
showInformationMessage: jest.fn(),
|
||||
},
|
||||
workspace: {
|
||||
getConfiguration: jest.fn().mockReturnValue({
|
||||
get: jest.fn().mockReturnValue([]),
|
||||
@@ -123,7 +126,11 @@ jest.mock('../../Cline', () => {
|
||||
Cline: jest.fn().mockImplementation(() => ({
|
||||
abortTask: jest.fn(),
|
||||
handleWebviewAskResponse: jest.fn(),
|
||||
clineMessages: []
|
||||
clineMessages: [],
|
||||
apiConversationHistory: [],
|
||||
overwriteClineMessages: jest.fn(),
|
||||
overwriteApiConversationHistory: jest.fn(),
|
||||
taskId: 'test-task-id'
|
||||
}))
|
||||
}
|
||||
})
|
||||
@@ -380,4 +387,149 @@ describe('ClineProvider', () => {
|
||||
const result = await extractTextFromFile('test.js')
|
||||
expect(result).toBe('1 | const x = 1;\n2 | const y = 2;\n3 | const z = 3;')
|
||||
})
|
||||
|
||||
describe('deleteMessage', () => {
|
||||
beforeEach(() => {
|
||||
// Mock window.showInformationMessage
|
||||
;(vscode.window.showInformationMessage as jest.Mock) = jest.fn()
|
||||
provider.resolveWebviewView(mockWebviewView)
|
||||
})
|
||||
|
||||
test('handles "Just this message" deletion correctly', async () => {
|
||||
// Mock user selecting "Just this message"
|
||||
;(vscode.window.showInformationMessage as jest.Mock).mockResolvedValue('Just this message')
|
||||
|
||||
// Setup mock messages
|
||||
const mockMessages = [
|
||||
{ ts: 1000, type: 'say', say: 'user_feedback' }, // User message 1
|
||||
{ ts: 2000, type: 'say', say: 'tool' }, // Tool message
|
||||
{ ts: 3000, type: 'say', say: 'text', value: 4000 }, // Message to delete
|
||||
{ ts: 4000, type: 'say', say: 'browser_action' }, // Response to delete
|
||||
{ ts: 5000, type: 'say', say: 'user_feedback' }, // Next user message
|
||||
{ ts: 6000, type: 'say', say: 'user_feedback' } // Final message
|
||||
]
|
||||
|
||||
const mockApiHistory = [
|
||||
{ ts: 1000 },
|
||||
{ ts: 2000 },
|
||||
{ ts: 3000 },
|
||||
{ ts: 4000 },
|
||||
{ ts: 5000 },
|
||||
{ ts: 6000 }
|
||||
]
|
||||
|
||||
// Setup Cline instance with mock data
|
||||
const mockCline = {
|
||||
clineMessages: mockMessages,
|
||||
apiConversationHistory: mockApiHistory,
|
||||
overwriteClineMessages: jest.fn(),
|
||||
overwriteApiConversationHistory: jest.fn(),
|
||||
taskId: 'test-task-id',
|
||||
abortTask: jest.fn(),
|
||||
handleWebviewAskResponse: jest.fn()
|
||||
}
|
||||
// @ts-ignore - accessing private property for testing
|
||||
provider.cline = mockCline
|
||||
|
||||
// Mock getTaskWithId
|
||||
;(provider as any).getTaskWithId = jest.fn().mockResolvedValue({
|
||||
historyItem: { id: 'test-task-id' }
|
||||
})
|
||||
|
||||
// Trigger message deletion
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
await messageHandler({ type: 'deleteMessage', value: 4000 })
|
||||
|
||||
// Verify correct messages were kept
|
||||
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([
|
||||
mockMessages[0],
|
||||
mockMessages[1],
|
||||
mockMessages[4],
|
||||
mockMessages[5]
|
||||
])
|
||||
|
||||
// Verify correct API messages were kept
|
||||
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([
|
||||
mockApiHistory[0],
|
||||
mockApiHistory[1],
|
||||
mockApiHistory[4],
|
||||
mockApiHistory[5]
|
||||
])
|
||||
})
|
||||
|
||||
test('handles "This and all subsequent messages" deletion correctly', async () => {
|
||||
// Mock user selecting "This and all subsequent messages"
|
||||
;(vscode.window.showInformationMessage as jest.Mock).mockResolvedValue('This and all subsequent messages')
|
||||
|
||||
// Setup mock messages
|
||||
const mockMessages = [
|
||||
{ ts: 1000, type: 'say', say: 'user_feedback' },
|
||||
{ ts: 2000, type: 'say', say: 'text', value: 3000 }, // Message to delete
|
||||
{ ts: 3000, type: 'say', say: 'user_feedback' },
|
||||
{ ts: 4000, type: 'say', say: 'user_feedback' }
|
||||
]
|
||||
|
||||
const mockApiHistory = [
|
||||
{ ts: 1000 },
|
||||
{ ts: 2000 },
|
||||
{ ts: 3000 },
|
||||
{ ts: 4000 }
|
||||
]
|
||||
|
||||
// Setup Cline instance with mock data
|
||||
const mockCline = {
|
||||
clineMessages: mockMessages,
|
||||
apiConversationHistory: mockApiHistory,
|
||||
overwriteClineMessages: jest.fn(),
|
||||
overwriteApiConversationHistory: jest.fn(),
|
||||
taskId: 'test-task-id',
|
||||
abortTask: jest.fn(),
|
||||
handleWebviewAskResponse: jest.fn()
|
||||
}
|
||||
// @ts-ignore - accessing private property for testing
|
||||
provider.cline = mockCline
|
||||
|
||||
// Mock getTaskWithId
|
||||
;(provider as any).getTaskWithId = jest.fn().mockResolvedValue({
|
||||
historyItem: { id: 'test-task-id' }
|
||||
})
|
||||
|
||||
// Trigger message deletion
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
await messageHandler({ type: 'deleteMessage', value: 3000 })
|
||||
|
||||
// Verify only messages before the deleted message were kept
|
||||
expect(mockCline.overwriteClineMessages).toHaveBeenCalledWith([
|
||||
mockMessages[0]
|
||||
])
|
||||
|
||||
// Verify only API messages before the deleted message were kept
|
||||
expect(mockCline.overwriteApiConversationHistory).toHaveBeenCalledWith([
|
||||
mockApiHistory[0]
|
||||
])
|
||||
})
|
||||
|
||||
test('handles Cancel correctly', async () => {
|
||||
// Mock user selecting "Cancel"
|
||||
;(vscode.window.showInformationMessage as jest.Mock).mockResolvedValue('Cancel')
|
||||
|
||||
const mockCline = {
|
||||
clineMessages: [{ ts: 1000 }, { ts: 2000 }],
|
||||
apiConversationHistory: [{ ts: 1000 }, { ts: 2000 }],
|
||||
overwriteClineMessages: jest.fn(),
|
||||
overwriteApiConversationHistory: jest.fn(),
|
||||
taskId: 'test-task-id'
|
||||
}
|
||||
// @ts-ignore - accessing private property for testing
|
||||
provider.cline = mockCline
|
||||
|
||||
// Trigger message deletion
|
||||
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
|
||||
await messageHandler({ type: 'deleteMessage', value: 2000 })
|
||||
|
||||
// Verify no messages were deleted
|
||||
expect(mockCline.overwriteClineMessages).not.toHaveBeenCalled()
|
||||
expect(mockCline.overwriteApiConversationHistory).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user