Skip to content
This repository was archived by the owner on May 15, 2026. It is now read-only.

Commit f0f80f7

Browse files
committed
feat: add DeleteQueuedMessage IPC command for queue removal
1 parent 6cfa82f commit f0f80f7

6 files changed

Lines changed: 140 additions & 1 deletion

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"roo-cline": minor
3+
"@roo-code/types": minor
4+
"@roo-code/ipc": minor
5+
---
6+
7+
Add DeleteQueuedMessage IPC command to allow removing queued messages from the extension's message queue via the IPC protocol

packages/ipc/src/ipc-client.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,13 @@ export class IpcClient extends EventEmitter<IpcClientEvents> {
108108
})
109109
}
110110

111+
public deleteQueuedMessage(messageId: string) {
112+
this.sendCommand({
113+
commandName: TaskCommandName.DeleteQueuedMessage,
114+
data: messageId,
115+
})
116+
}
117+
111118
public sendMessage(message: IpcMessage) {
112119
ipc.of[this._id]?.emit("message", message)
113120
}

packages/types/src/__tests__/ipc.test.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,19 @@ describe("IPC Types", () => {
66
expect(TaskCommandName.ResumeTask).toBe("ResumeTask")
77
})
88

9+
it("should include DeleteQueuedMessage command", () => {
10+
expect(TaskCommandName.DeleteQueuedMessage).toBe("DeleteQueuedMessage")
11+
})
12+
913
it("should have all expected task commands", () => {
10-
const expectedCommands = ["StartNewTask", "CancelTask", "CloseTask", "ResumeTask"]
14+
const expectedCommands = [
15+
"StartNewTask",
16+
"CancelTask",
17+
"CloseTask",
18+
"ResumeTask",
19+
"SendMessage",
20+
"DeleteQueuedMessage",
21+
]
1122
const actualCommands = Object.values(TaskCommandName)
1223

1324
expectedCommands.forEach((command) => {
@@ -70,5 +81,40 @@ describe("IPC Types", () => {
7081
const result = taskCommandSchema.safeParse(invalidCommand)
7182
expect(result.success).toBe(false)
7283
})
84+
85+
it("should validate DeleteQueuedMessage command with messageId", () => {
86+
const command = {
87+
commandName: TaskCommandName.DeleteQueuedMessage,
88+
data: "msg-abc-123",
89+
}
90+
91+
const result = taskCommandSchema.safeParse(command)
92+
expect(result.success).toBe(true)
93+
94+
if (result.success && result.data.commandName === TaskCommandName.DeleteQueuedMessage) {
95+
expect(result.data.commandName).toBe("DeleteQueuedMessage")
96+
expect(result.data.data).toBe("msg-abc-123")
97+
}
98+
})
99+
100+
it("should reject DeleteQueuedMessage command with invalid data", () => {
101+
const invalidCommand = {
102+
commandName: TaskCommandName.DeleteQueuedMessage,
103+
data: 123, // Should be string
104+
}
105+
106+
const result = taskCommandSchema.safeParse(invalidCommand)
107+
expect(result.success).toBe(false)
108+
})
109+
110+
it("should reject DeleteQueuedMessage command without data", () => {
111+
const invalidCommand = {
112+
commandName: TaskCommandName.DeleteQueuedMessage,
113+
// Missing data field
114+
}
115+
116+
const result = taskCommandSchema.safeParse(invalidCommand)
117+
expect(result.success).toBe(false)
118+
})
73119
})
74120
})

packages/types/src/ipc.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export enum TaskCommandName {
4646
CloseTask = "CloseTask",
4747
ResumeTask = "ResumeTask",
4848
SendMessage = "SendMessage",
49+
DeleteQueuedMessage = "DeleteQueuedMessage",
4950
}
5051

5152
/**
@@ -81,6 +82,10 @@ export const taskCommandSchema = z.discriminatedUnion("commandName", [
8182
images: z.array(z.string()).optional(),
8283
}),
8384
}),
85+
z.object({
86+
commandName: z.literal(TaskCommandName.DeleteQueuedMessage),
87+
data: z.string(), // messageId
88+
}),
8489
])
8590

8691
export type TaskCommand = z.infer<typeof taskCommandSchema>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest"
2+
import * as vscode from "vscode"
3+
4+
import { API } from "../api"
5+
import { ClineProvider } from "../../core/webview/ClineProvider"
6+
7+
vi.mock("vscode")
8+
vi.mock("../../core/webview/ClineProvider")
9+
10+
describe("API - DeleteQueuedMessage Command", () => {
11+
let api: API
12+
let mockOutputChannel: vscode.OutputChannel
13+
let mockProvider: ClineProvider
14+
let mockRemoveMessage: ReturnType<typeof vi.fn>
15+
let mockLog: ReturnType<typeof vi.fn>
16+
17+
beforeEach(() => {
18+
mockOutputChannel = {
19+
appendLine: vi.fn(),
20+
} as unknown as vscode.OutputChannel
21+
22+
mockRemoveMessage = vi.fn().mockReturnValue(true)
23+
24+
mockProvider = {
25+
context: {} as vscode.ExtensionContext,
26+
postMessageToWebview: vi.fn().mockResolvedValue(undefined),
27+
on: vi.fn(),
28+
getCurrentTaskStack: vi.fn().mockReturnValue([]),
29+
getCurrentTask: vi.fn().mockReturnValue({
30+
messageQueueService: {
31+
removeMessage: mockRemoveMessage,
32+
},
33+
}),
34+
viewLaunched: true,
35+
} as unknown as ClineProvider
36+
37+
mockLog = vi.fn()
38+
39+
api = new API(mockOutputChannel, mockProvider, undefined, true)
40+
;(api as any).log = mockLog
41+
})
42+
43+
it("should remove a queued message by id", () => {
44+
const messageId = "msg-abc-123"
45+
46+
api.deleteQueuedMessage(messageId)
47+
48+
expect(mockRemoveMessage).toHaveBeenCalledWith(messageId)
49+
expect(mockRemoveMessage).toHaveBeenCalledTimes(1)
50+
})
51+
52+
it("should handle missing current task gracefully", () => {
53+
;(mockProvider.getCurrentTask as ReturnType<typeof vi.fn>).mockReturnValue(undefined)
54+
55+
// Should not throw
56+
expect(() => api.deleteQueuedMessage("msg-abc-123")).not.toThrow()
57+
})
58+
59+
it("should handle non-existent message id gracefully", () => {
60+
mockRemoveMessage.mockReturnValue(false)
61+
62+
// Should not throw even when removeMessage returns false
63+
expect(() => api.deleteQueuedMessage("non-existent-id")).not.toThrow()
64+
expect(mockRemoveMessage).toHaveBeenCalledWith("non-existent-id")
65+
})
66+
})

src/extension/api.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
9595
this.log(`[API] SendMessage -> ${data.text}`)
9696
await this.sendMessage(data.text, data.images)
9797
break
98+
case TaskCommandName.DeleteQueuedMessage:
99+
this.log(`[API] DeleteQueuedMessage -> ${data}`)
100+
this.deleteQueuedMessage(data)
101+
break
98102
}
99103
})
100104
}
@@ -194,6 +198,10 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
194198
await this.sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "sendMessage", text, images })
195199
}
196200

201+
public deleteQueuedMessage(messageId: string) {
202+
this.sidebarProvider.getCurrentTask()?.messageQueueService.removeMessage(messageId)
203+
}
204+
197205
public async pressPrimaryButton() {
198206
await this.sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "primaryButtonClick" })
199207
}

0 commit comments

Comments
 (0)