From 0bcb9c62789f3decaddc30c935d3ad4e38bfad1b Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Sun, 10 Aug 2025 03:02:36 -0400 Subject: [PATCH 1/2] Add submitUserMessage to Task --- packages/types/src/task.ts | 1 + src/core/task/Task.ts | 26 ++++++ src/core/task/__tests__/Task.spec.ts | 120 +++++++++++++++++++++++++++ 3 files changed, 147 insertions(+) diff --git a/packages/types/src/task.ts b/packages/types/src/task.ts index 4da1a1f6f5..a4a2f5f0fc 100644 --- a/packages/types/src/task.ts +++ b/packages/types/src/task.ts @@ -70,6 +70,7 @@ export interface TaskLike { off(event: K, listener: (...args: TaskEvents[K]) => void | Promise): this setMessageResponse(text: string, images?: string[]): void + submitUserMessage(text: string, images?: string[]): void } export type TaskEvents = { diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 1dd615f0eb..5e96b6fb16 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -740,6 +740,32 @@ export class Task extends EventEmitter implements TaskLike { this.askResponseImages = images } + public submitUserMessage(text: string, images?: string[]): void { + try { + const trimmed = (text ?? "").trim() + const imgs = images ?? [] + + if (!trimmed && imgs.length === 0) { + return + } + + const provider = this.providerRef.deref() + if (!provider) { + console.error("[Task#submitUserMessage] Provider reference lost") + return + } + + void provider.postMessageToWebview({ + type: "invoke", + invoke: "sendMessage", + text: trimmed, + images: imgs, + }) + } catch (error) { + console.error("[Task#submitUserMessage] Failed to submit user message:", error) + } + } + async handleTerminalOperation(terminalOperation: "continue" | "abort") { if (terminalOperation === "continue") { this.terminalProcess?.continue() diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index 9aa5a8d7a8..39df433814 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -1493,5 +1493,125 @@ describe("Cline", () => { expect(noModelTask.apiConfiguration.apiProvider).toBe("openai") }) }) + + describe("submitUserMessage", () => { + it("should always route through webview sendMessage invoke", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "initial task", + startTask: false, + }) + + // Set up some existing messages to simulate an ongoing conversation + task.clineMessages = [ + { + ts: Date.now(), + type: "say", + say: "text", + text: "Initial message", + }, + ] + + // Call submitUserMessage + task.submitUserMessage("test message", ["image1.png"]) + + // Verify postMessageToWebview was called with sendMessage invoke + expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "invoke", + invoke: "sendMessage", + text: "test message", + images: ["image1.png"], + }) + }) + + it("should handle empty messages gracefully", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "initial task", + startTask: false, + }) + + // Call with empty text and no images + task.submitUserMessage("", []) + + // Should not call postMessageToWebview for empty messages + expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled() + + // Call with whitespace only + task.submitUserMessage(" ", []) + expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled() + }) + + it("should route through webview for both new and existing tasks", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "initial task", + startTask: false, + }) + + // Test with no messages (new task scenario) + task.clineMessages = [] + task.submitUserMessage("new task", ["image1.png"]) + + expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "invoke", + invoke: "sendMessage", + text: "new task", + images: ["image1.png"], + }) + + // Clear mock + mockProvider.postMessageToWebview.mockClear() + + // Test with existing messages (ongoing task scenario) + task.clineMessages = [ + { + ts: Date.now(), + type: "say", + say: "text", + text: "Initial message", + }, + ] + task.submitUserMessage("follow-up message", ["image2.png"]) + + expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "invoke", + invoke: "sendMessage", + text: "follow-up message", + images: ["image2.png"], + }) + }) + + it("should handle undefined provider gracefully", async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "initial task", + startTask: false, + }) + + // Simulate weakref returning undefined + Object.defineProperty(task, "providerRef", { + value: { deref: () => undefined }, + writable: false, + configurable: true, + }) + + // Spy on console.error to verify error is logged + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + + // Should log error but not throw + task.submitUserMessage("test message") + + expect(consoleErrorSpy).toHaveBeenCalledWith("[Task#submitUserMessage] Provider reference lost") + expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled() + + // Restore console.error + consoleErrorSpy.mockRestore() + }) + }) }) }) From f877d302e2096f1a00ecdc3c3cc53b443465880d Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Sun, 10 Aug 2025 03:07:52 -0400 Subject: [PATCH 2/2] Bump types to 1.45.0 --- packages/types/npm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/types/npm/package.json b/packages/types/npm/package.json index f73a83a7b6..35e01d4ef7 100644 --- a/packages/types/npm/package.json +++ b/packages/types/npm/package.json @@ -1,6 +1,6 @@ { "name": "@roo-code/types", - "version": "1.44.0", + "version": "1.45.0", "description": "TypeScript type definitions for Roo Code.", "publishConfig": { "access": "public",