feat: add DeleteQueuedMessage IPC command (#11464)

* feat: add DeleteQueuedMessage IPC command for queue removal

* Delete .changeset/delete-queued-message-ipc.md

* fix: add try/catch to DeleteQueuedMessage IPC handler and early return in deleteQueuedMessage

---------

Co-authored-by: Roo Code <roomote@roocode.com>
Co-authored-by: Hannes Rudolph <hrudolph@gmail.com>
This commit is contained in:
roomote[bot] 2026-02-18 10:42:48 -07:00 committed by GitHub
parent bfbfaf6d46
commit d575295883
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 149 additions and 1 deletions

View file

@ -108,6 +108,13 @@ export class IpcClient extends EventEmitter<IpcClientEvents> {
})
}
public deleteQueuedMessage(messageId: string) {
this.sendCommand({
commandName: TaskCommandName.DeleteQueuedMessage,
data: messageId,
})
}
public sendMessage(message: IpcMessage) {
ipc.of[this._id]?.emit("message", message)
}

View file

@ -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)
})
})
})

View file

@ -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<typeof taskCommandSchema>

View file

@ -0,0 +1,70 @@
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<typeof vi.fn>
let mockLog: ReturnType<typeof vi.fn>
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 and log a message", () => {
;(mockProvider.getCurrentTask as ReturnType<typeof vi.fn>).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", () => {
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")
})
})

View file

@ -150,6 +150,15 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
sendResponse(RooCodeEventName.ModelsResponse, [{}])
}
break
case TaskCommandName.DeleteQueuedMessage:
this.log(`[API] 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
}
})
@ -266,6 +275,17 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
await this.sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "sendMessage", text, images })
}
public deleteQueuedMessage(messageId: string) {
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() {
await this.sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "primaryButtonClick" })
}