From 0fc4684920e4db5f74b44b6bbcf01745ccfc6a36 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 13 Feb 2026 21:33:08 +0000 Subject: [PATCH 1/3] feat: add DeleteQueuedMessage IPC command for queue removal --- .changeset/delete-queued-message-ipc.md | 7 ++ packages/ipc/src/ipc-client.ts | 7 ++ packages/types/src/__tests__/ipc.test.ts | 48 +++++++++++++- packages/types/src/ipc.ts | 5 ++ .../api-delete-queued-message.spec.ts | 66 +++++++++++++++++++ src/extension/api.ts | 8 +++ 6 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 .changeset/delete-queued-message-ipc.md create mode 100644 src/extension/__tests__/api-delete-queued-message.spec.ts diff --git a/.changeset/delete-queued-message-ipc.md b/.changeset/delete-queued-message-ipc.md new file mode 100644 index 00000000000..93d095de701 --- /dev/null +++ b/.changeset/delete-queued-message-ipc.md @@ -0,0 +1,7 @@ +--- +"roo-cline": minor +"@roo-code/types": minor +"@roo-code/ipc": minor +--- + +Add DeleteQueuedMessage IPC command to allow removing queued messages from the extension's message queue via the IPC protocol diff --git a/packages/ipc/src/ipc-client.ts b/packages/ipc/src/ipc-client.ts index da96ab90f32..d374cb186a3 100644 --- a/packages/ipc/src/ipc-client.ts +++ b/packages/ipc/src/ipc-client.ts @@ -108,6 +108,13 @@ export class IpcClient extends EventEmitter { }) } + public deleteQueuedMessage(messageId: string) { + this.sendCommand({ + commandName: TaskCommandName.DeleteQueuedMessage, + data: messageId, + }) + } + public sendMessage(message: IpcMessage) { ipc.of[this._id]?.emit("message", message) } diff --git a/packages/types/src/__tests__/ipc.test.ts b/packages/types/src/__tests__/ipc.test.ts index 856b3f2cc18..a843354a559 100644 --- a/packages/types/src/__tests__/ipc.test.ts +++ b/packages/types/src/__tests__/ipc.test.ts @@ -6,8 +6,19 @@ describe("IPC Types", () => { expect(TaskCommandName.ResumeTask).toBe("ResumeTask") }) + it("should include DeleteQueuedMessage command", () => { + expect(TaskCommandName.DeleteQueuedMessage).toBe("DeleteQueuedMessage") + }) + it("should have all expected task commands", () => { - const expectedCommands = ["StartNewTask", "CancelTask", "CloseTask", "ResumeTask"] + const expectedCommands = [ + "StartNewTask", + "CancelTask", + "CloseTask", + "ResumeTask", + "SendMessage", + "DeleteQueuedMessage", + ] const actualCommands = Object.values(TaskCommandName) expectedCommands.forEach((command) => { @@ -70,5 +81,40 @@ describe("IPC Types", () => { const result = taskCommandSchema.safeParse(invalidCommand) expect(result.success).toBe(false) }) + + it("should validate DeleteQueuedMessage command with messageId", () => { + const command = { + commandName: TaskCommandName.DeleteQueuedMessage, + data: "msg-abc-123", + } + + const result = taskCommandSchema.safeParse(command) + expect(result.success).toBe(true) + + if (result.success && result.data.commandName === TaskCommandName.DeleteQueuedMessage) { + expect(result.data.commandName).toBe("DeleteQueuedMessage") + expect(result.data.data).toBe("msg-abc-123") + } + }) + + it("should reject DeleteQueuedMessage command with invalid data", () => { + const invalidCommand = { + commandName: TaskCommandName.DeleteQueuedMessage, + data: 123, // Should be string + } + + const result = taskCommandSchema.safeParse(invalidCommand) + expect(result.success).toBe(false) + }) + + it("should reject DeleteQueuedMessage command without data", () => { + const invalidCommand = { + commandName: TaskCommandName.DeleteQueuedMessage, + // Missing data field + } + + const result = taskCommandSchema.safeParse(invalidCommand) + expect(result.success).toBe(false) + }) }) }) diff --git a/packages/types/src/ipc.ts b/packages/types/src/ipc.ts index 90a1478a4db..fea040af0b6 100644 --- a/packages/types/src/ipc.ts +++ b/packages/types/src/ipc.ts @@ -49,6 +49,7 @@ export enum TaskCommandName { GetCommands = "GetCommands", GetModes = "GetModes", GetModels = "GetModels", + DeleteQueuedMessage = "DeleteQueuedMessage", } /** @@ -91,6 +92,10 @@ export const taskCommandSchema = z.discriminatedUnion("commandName", [ z.object({ commandName: z.literal(TaskCommandName.GetModels), }), + z.object({ + commandName: z.literal(TaskCommandName.DeleteQueuedMessage), + data: z.string(), // messageId + }), ]) export type TaskCommand = z.infer diff --git a/src/extension/__tests__/api-delete-queued-message.spec.ts b/src/extension/__tests__/api-delete-queued-message.spec.ts new file mode 100644 index 00000000000..149e2457f6a --- /dev/null +++ b/src/extension/__tests__/api-delete-queued-message.spec.ts @@ -0,0 +1,66 @@ +import { describe, it, expect, vi, beforeEach } from "vitest" +import * as vscode from "vscode" + +import { API } from "../api" +import { ClineProvider } from "../../core/webview/ClineProvider" + +vi.mock("vscode") +vi.mock("../../core/webview/ClineProvider") + +describe("API - DeleteQueuedMessage Command", () => { + let api: API + let mockOutputChannel: vscode.OutputChannel + let mockProvider: ClineProvider + let mockRemoveMessage: ReturnType + let mockLog: ReturnType + + beforeEach(() => { + mockOutputChannel = { + appendLine: vi.fn(), + } as unknown as vscode.OutputChannel + + mockRemoveMessage = vi.fn().mockReturnValue(true) + + mockProvider = { + context: {} as vscode.ExtensionContext, + postMessageToWebview: vi.fn().mockResolvedValue(undefined), + on: vi.fn(), + getCurrentTaskStack: vi.fn().mockReturnValue([]), + getCurrentTask: vi.fn().mockReturnValue({ + messageQueueService: { + removeMessage: mockRemoveMessage, + }, + }), + viewLaunched: true, + } as unknown as ClineProvider + + mockLog = vi.fn() + + api = new API(mockOutputChannel, mockProvider, undefined, true) + ;(api as any).log = mockLog + }) + + it("should remove a queued message by id", () => { + const messageId = "msg-abc-123" + + api.deleteQueuedMessage(messageId) + + expect(mockRemoveMessage).toHaveBeenCalledWith(messageId) + expect(mockRemoveMessage).toHaveBeenCalledTimes(1) + }) + + it("should handle missing current task gracefully", () => { + ;(mockProvider.getCurrentTask as ReturnType).mockReturnValue(undefined) + + // Should not throw + expect(() => api.deleteQueuedMessage("msg-abc-123")).not.toThrow() + }) + + it("should handle non-existent message id gracefully", () => { + mockRemoveMessage.mockReturnValue(false) + + // Should not throw even when removeMessage returns false + expect(() => api.deleteQueuedMessage("non-existent-id")).not.toThrow() + expect(mockRemoveMessage).toHaveBeenCalledWith("non-existent-id") + }) +}) diff --git a/src/extension/api.ts b/src/extension/api.ts index 25c81a65896..575bb9ba9dd 100644 --- a/src/extension/api.ts +++ b/src/extension/api.ts @@ -150,6 +150,10 @@ export class API extends EventEmitter implements RooCodeAPI { sendResponse(RooCodeEventName.ModelsResponse, [{}]) } + break + case TaskCommandName.DeleteQueuedMessage: + this.log(`[API] DeleteQueuedMessage -> ${data}`) + this.deleteQueuedMessage(data) break } }) @@ -266,6 +270,10 @@ export class API extends EventEmitter implements RooCodeAPI { await this.sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "sendMessage", text, images }) } + public deleteQueuedMessage(messageId: string) { + this.sidebarProvider.getCurrentTask()?.messageQueueService.removeMessage(messageId) + } + public async pressPrimaryButton() { await this.sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "primaryButtonClick" }) } From 9a9bf6a48418b0f6c143167901436097cb0f1d84 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Fri, 13 Feb 2026 21:43:52 -0700 Subject: [PATCH 2/3] Delete .changeset/delete-queued-message-ipc.md --- .changeset/delete-queued-message-ipc.md | 7 ------- src/extension/api.ts | 4 ++-- 2 files changed, 2 insertions(+), 9 deletions(-) delete mode 100644 .changeset/delete-queued-message-ipc.md diff --git a/.changeset/delete-queued-message-ipc.md b/.changeset/delete-queued-message-ipc.md deleted file mode 100644 index 93d095de701..00000000000 --- a/.changeset/delete-queued-message-ipc.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"roo-cline": minor -"@roo-code/types": minor -"@roo-code/ipc": minor ---- - -Add DeleteQueuedMessage IPC command to allow removing queued messages from the extension's message queue via the IPC protocol diff --git a/src/extension/api.ts b/src/extension/api.ts index 575bb9ba9dd..ae217f9dd6a 100644 --- a/src/extension/api.ts +++ b/src/extension/api.ts @@ -152,8 +152,8 @@ export class API extends EventEmitter implements RooCodeAPI { break case TaskCommandName.DeleteQueuedMessage: - this.log(`[API] DeleteQueuedMessage -> ${data}`) - this.deleteQueuedMessage(data) + this.log(`[API] DeleteQueuedMessage -> ${command.data}`) + this.deleteQueuedMessage(command.data) break } }) From 7314d658781988f4890444fd0cc7b8bcd38647de Mon Sep 17 00:00:00 2001 From: Roo Code Date: Wed, 18 Feb 2026 17:04:22 +0000 Subject: [PATCH 3/3] fix: add try/catch to DeleteQueuedMessage IPC handler and early return in deleteQueuedMessage --- .../__tests__/api-delete-queued-message.spec.ts | 6 +++++- src/extension/api.ts | 16 ++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/extension/__tests__/api-delete-queued-message.spec.ts b/src/extension/__tests__/api-delete-queued-message.spec.ts index 149e2457f6a..6bf6014bf84 100644 --- a/src/extension/__tests__/api-delete-queued-message.spec.ts +++ b/src/extension/__tests__/api-delete-queued-message.spec.ts @@ -49,11 +49,15 @@ describe("API - DeleteQueuedMessage Command", () => { expect(mockRemoveMessage).toHaveBeenCalledTimes(1) }) - it("should handle missing current task gracefully", () => { + it("should handle missing current task gracefully and log a message", () => { ;(mockProvider.getCurrentTask as ReturnType).mockReturnValue(undefined) // Should not throw expect(() => api.deleteQueuedMessage("msg-abc-123")).not.toThrow() + expect(mockLog).toHaveBeenCalledWith( + "[API#deleteQueuedMessage] no current task; ignoring delete for messageId msg-abc-123", + ) + expect(mockRemoveMessage).not.toHaveBeenCalled() }) it("should handle non-existent message id gracefully", () => { diff --git a/src/extension/api.ts b/src/extension/api.ts index ae217f9dd6a..4a66b40078d 100644 --- a/src/extension/api.ts +++ b/src/extension/api.ts @@ -153,7 +153,12 @@ export class API extends EventEmitter implements RooCodeAPI { break case TaskCommandName.DeleteQueuedMessage: this.log(`[API] DeleteQueuedMessage -> ${command.data}`) - this.deleteQueuedMessage(command.data) + try { + this.deleteQueuedMessage(command.data) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + this.log(`[API] DeleteQueuedMessage failed for messageId ${command.data}: ${errorMessage}`) + } break } }) @@ -271,7 +276,14 @@ export class API extends EventEmitter implements RooCodeAPI { } public deleteQueuedMessage(messageId: string) { - this.sidebarProvider.getCurrentTask()?.messageQueueService.removeMessage(messageId) + const currentTask = this.sidebarProvider.getCurrentTask() + + if (!currentTask) { + this.log(`[API#deleteQueuedMessage] no current task; ignoring delete for messageId ${messageId}`) + return + } + + currentTask.messageQueueService.removeMessage(messageId) } public async pressPrimaryButton() {