Skip to content
Merged
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
7 changes: 7 additions & 0 deletions packages/ipc/src/ipc-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
48 changes: 47 additions & 1 deletion packages/types/src/__tests__/ipc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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)
})
})
})
5 changes: 5 additions & 0 deletions packages/types/src/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export enum TaskCommandName {
GetCommands = "GetCommands",
GetModes = "GetModes",
GetModels = "GetModels",
DeleteQueuedMessage = "DeleteQueuedMessage",
}

/**
Expand Down Expand Up @@ -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>
Expand Down
70 changes: 70 additions & 0 deletions src/extension/__tests__/api-delete-queued-message.spec.ts
Original file line number Diff line number Diff line change
@@ -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")
})
})
20 changes: 20 additions & 0 deletions src/extension/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
})
Expand Down Expand Up @@ -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" })
}
Expand Down
Loading