Skip to content
Open
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 @@ -46,6 +46,7 @@ export enum TaskCommandName {
CloseTask = "CloseTask",
ResumeTask = "ResumeTask",
SendMessage = "SendMessage",
DeleteQueuedMessage = "DeleteQueuedMessage",
}

/**
Expand Down Expand Up @@ -81,6 +82,10 @@ export const taskCommandSchema = z.discriminatedUnion("commandName", [
images: z.array(z.string()).optional(),
}),
}),
z.object({
commandName: z.literal(TaskCommandName.DeleteQueuedMessage),
data: z.string(), // messageId
}),
])

export type TaskCommand = z.infer<typeof taskCommandSchema>
Expand Down
66 changes: 66 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,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<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", () => {
;(mockProvider.getCurrentTask as ReturnType<typeof vi.fn>).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")
})
})
8 changes: 8 additions & 0 deletions src/extension/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
this.log(`[API] SendMessage -> ${data.text}`)
await this.sendMessage(data.text, data.images)
break
case TaskCommandName.DeleteQueuedMessage:
this.log(`[API] DeleteQueuedMessage -> ${data}`)
this.deleteQueuedMessage(data)
break
Comment on lines +98 to +101
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This handler should be wrapped in try/catch like cte's other IPC command handlers (see GetCommands/GetModes/GetModels in #11279). Without it, an exception from removeMessage would propagate unhandled to the IPC server. Also, use command.data instead of data to stay consistent with the refactor in #11162.

Suggested change
case TaskCommandName.DeleteQueuedMessage:
this.log(`[API] DeleteQueuedMessage -> ${data}`)
this.deleteQueuedMessage(data)
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 @@ -194,6 +198,10 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
await this.sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "sendMessage", text, images })
}

public deleteQueuedMessage(messageId: string) {
this.sidebarProvider.getCurrentTask()?.messageQueueService.removeMessage(messageId)
}
Comment on lines +201 to +203
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: If getCurrentTask() returns undefined, this silently no-ops with no log trail. Splitting it out and logging the miss would help with debugging in cloud worker scenarios.

Suggested change
public deleteQueuedMessage(messageId: string) {
this.sidebarProvider.getCurrentTask()?.messageQueueService.removeMessage(messageId)
}
public deleteQueuedMessage(messageId: string) {
const task = this.sidebarProvider.getCurrentTask()
if (!task) {
this.log(`[API] DeleteQueuedMessage: no current task, ignoring`)
return
}
task.messageQueueService.removeMessage(messageId)
}


public async pressPrimaryButton() {
await this.sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "primaryButtonClick" })
}
Expand Down
Loading