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
8 changes: 8 additions & 0 deletions packages/ipc/src/ipc-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type IpcMessage,
IpcOrigin,
IpcMessageType,
TaskCommandName,
ipcMessageSchema,
} from "@roo-code/types"

Expand Down Expand Up @@ -98,6 +99,13 @@ export class IpcClient extends EventEmitter<IpcClientEvents> {
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)
}
Expand Down
8 changes: 8 additions & 0 deletions packages/types/src/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export enum TaskCommandName {
CancelTask = "CancelTask",
CloseTask = "CloseTask",
ResumeTask = "ResumeTask",
SendMessage = "SendMessage",
}

/**
Expand Down Expand Up @@ -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<typeof taskCommandSchema>
Expand Down
155 changes: 155 additions & 0 deletions src/extension/__tests__/api-send-message.spec.ts
Original file line number Diff line number Diff line change
@@ -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<typeof vi.fn>
let mockLog: ReturnType<typeof vi.fn>

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)
})
})
4 changes: 4 additions & 0 deletions src/extension/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ export class API extends EventEmitter<RooCodeEvents> 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
}
})
}
Expand Down