diff --git a/packages/ipc/src/ipc-client.ts b/packages/ipc/src/ipc-client.ts index 0d3a767e004..2e68152a5f8 100644 --- a/packages/ipc/src/ipc-client.ts +++ b/packages/ipc/src/ipc-client.ts @@ -9,6 +9,7 @@ import { type IpcMessage, IpcOrigin, IpcMessageType, + TaskCommandName, ipcMessageSchema, } from "@roo-code/types" @@ -98,6 +99,13 @@ export class IpcClient extends EventEmitter { this.sendMessage(message) } + public sendTaskMessage(text?: string, images?: string[]) { + this.sendCommand({ + commandName: TaskCommandName.SendMessage, + data: { text, images }, + }) + } + public sendMessage(message: IpcMessage) { ipc.of[this._id]?.emit("message", message) } diff --git a/packages/types/src/ipc.ts b/packages/types/src/ipc.ts index ace39c3f2b6..4e1b1ac3551 100644 --- a/packages/types/src/ipc.ts +++ b/packages/types/src/ipc.ts @@ -45,6 +45,7 @@ export enum TaskCommandName { CancelTask = "CancelTask", CloseTask = "CloseTask", ResumeTask = "ResumeTask", + SendMessage = "SendMessage", } /** @@ -73,6 +74,13 @@ export const taskCommandSchema = z.discriminatedUnion("commandName", [ commandName: z.literal(TaskCommandName.ResumeTask), data: z.string(), }), + z.object({ + commandName: z.literal(TaskCommandName.SendMessage), + data: z.object({ + text: z.string().optional(), + images: z.array(z.string()).optional(), + }), + }), ]) export type TaskCommand = z.infer diff --git a/src/extension/__tests__/api-send-message.spec.ts b/src/extension/__tests__/api-send-message.spec.ts new file mode 100644 index 00000000000..ea1331f618c --- /dev/null +++ b/src/extension/__tests__/api-send-message.spec.ts @@ -0,0 +1,155 @@ +import { describe, it, expect, vi, beforeEach } from "vitest" +import * as vscode from "vscode" + +import { API } from "../api" +import { ClineProvider } from "../../core/webview/ClineProvider" +import { TaskCommandName } from "@roo-code/types" + +vi.mock("vscode") +vi.mock("../../core/webview/ClineProvider") + +describe("API - SendMessage Command", () => { + let api: API + let mockOutputChannel: vscode.OutputChannel + let mockProvider: ClineProvider + let mockPostMessageToWebview: ReturnType + let mockLog: ReturnType + + beforeEach(() => { + // Setup mocks + mockOutputChannel = { + appendLine: vi.fn(), + } as unknown as vscode.OutputChannel + + mockPostMessageToWebview = vi.fn().mockResolvedValue(undefined) + + mockProvider = { + context: {} as vscode.ExtensionContext, + postMessageToWebview: mockPostMessageToWebview, + on: vi.fn(), + getCurrentTaskStack: vi.fn().mockReturnValue([]), + viewLaunched: true, + } as unknown as ClineProvider + + mockLog = vi.fn() + + // Create API instance with logging enabled for testing + api = new API(mockOutputChannel, mockProvider, undefined, true) + // Override the log method to use our mock + ;(api as any).log = mockLog + }) + + it("should handle SendMessage command with text only", async () => { + // Arrange + const messageText = "Hello, this is a test message" + + // Act + await api.sendMessage(messageText) + + // Assert + expect(mockPostMessageToWebview).toHaveBeenCalledWith({ + type: "invoke", + invoke: "sendMessage", + text: messageText, + images: undefined, + }) + }) + + it("should handle SendMessage command with text and images", async () => { + // Arrange + const messageText = "Analyze this image" + const images = [ + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + ] + + // Act + await api.sendMessage(messageText, images) + + // Assert + expect(mockPostMessageToWebview).toHaveBeenCalledWith({ + type: "invoke", + invoke: "sendMessage", + text: messageText, + images, + }) + }) + + it("should handle SendMessage command with images only", async () => { + // Arrange + const images = [ + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + ] + + // Act + await api.sendMessage(undefined, images) + + // Assert + expect(mockPostMessageToWebview).toHaveBeenCalledWith({ + type: "invoke", + invoke: "sendMessage", + text: undefined, + images, + }) + }) + + it("should handle SendMessage command with empty parameters", async () => { + // Act + await api.sendMessage() + + // Assert + expect(mockPostMessageToWebview).toHaveBeenCalledWith({ + type: "invoke", + invoke: "sendMessage", + text: undefined, + images: undefined, + }) + }) + + it("should log SendMessage command when processed via IPC", async () => { + // This test verifies the logging behavior when the command comes through IPC + // We need to simulate the IPC handler directly since we can't easily test the full IPC flow + + const messageText = "Test message from IPC" + const commandData = { + text: messageText, + images: undefined, + } + + // Simulate the IPC command handler calling sendMessage + mockLog(`[API] SendMessage -> ${commandData.text}`) + await api.sendMessage(commandData.text, commandData.images) + + // Assert that logging occurred + expect(mockLog).toHaveBeenCalledWith(`[API] SendMessage -> ${messageText}`) + + // Assert that the message was sent + expect(mockPostMessageToWebview).toHaveBeenCalledWith({ + type: "invoke", + invoke: "sendMessage", + text: messageText, + images: undefined, + }) + }) + + it("should handle SendMessage with multiple images", async () => { + // Arrange + const messageText = "Compare these images" + const images = [ + "data:image/png;base64,image1data", + "data:image/png;base64,image2data", + "data:image/png;base64,image3data", + ] + + // Act + await api.sendMessage(messageText, images) + + // Assert + expect(mockPostMessageToWebview).toHaveBeenCalledWith({ + type: "invoke", + invoke: "sendMessage", + text: messageText, + images, + }) + expect(mockPostMessageToWebview).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/extension/api.ts b/src/extension/api.ts index 1820ddee6ca..8f642777f66 100644 --- a/src/extension/api.ts +++ b/src/extension/api.ts @@ -91,6 +91,10 @@ export class API extends EventEmitter implements RooCodeAPI { // The error is logged for debugging purposes } break + case TaskCommandName.SendMessage: + this.log(`[API] SendMessage -> ${data.text}`) + await this.sendMessage(data.text, data.images) + break } }) }