diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index a235cf48242..b13c4bff6f0 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1546,7 +1546,10 @@ export class Task extends EventEmitter implements TaskLike { this.emit(RooCodeEventName.TaskUserMessage, this.taskId) - provider.postMessageToWebview({ type: "invoke", invoke: "sendMessage", text, images }) + // Handle the message directly instead of routing through the webview. + // This avoids a race condition where the webview's message state hasn't + // hydrated yet, causing it to interpret the message as a new task request. + this.handleWebviewAskResponse("messageResponse", text, images) } else { console.error("[Task#submitUserMessage] Provider reference lost") } diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index c69050b22a5..870cdc556e2 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -1523,7 +1523,7 @@ describe("Cline", () => { }) describe("submitUserMessage", () => { - it("should always route through webview sendMessage invoke", async () => { + it("should call handleWebviewAskResponse directly", async () => { const task = new Task({ provider: mockProvider, apiConfiguration: mockApiConfig, @@ -1531,6 +1531,9 @@ describe("Cline", () => { startTask: false, }) + // Spy on handleWebviewAskResponse + const handleResponseSpy = vi.spyOn(task, "handleWebviewAskResponse") + // Set up some existing messages to simulate an ongoing conversation task.clineMessages = [ { @@ -1544,13 +1547,10 @@ describe("Cline", () => { // 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"], - }) + // Verify handleWebviewAskResponse was called directly (not webview) + expect(handleResponseSpy).toHaveBeenCalledWith("messageResponse", "test message", ["image1.png"]) + // Should NOT route through webview anymore + expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled() }) it("should handle empty messages gracefully", async () => { @@ -1561,18 +1561,21 @@ describe("Cline", () => { startTask: false, }) + // Spy on handleWebviewAskResponse + const handleResponseSpy = vi.spyOn(task, "handleWebviewAskResponse") + // Call with empty text and no images task.submitUserMessage("", []) - // Should not call postMessageToWebview for empty messages - expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled() + // Should not call handleWebviewAskResponse for empty messages + expect(handleResponseSpy).not.toHaveBeenCalled() // Call with whitespace only task.submitUserMessage(" ", []) - expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled() + expect(handleResponseSpy).not.toHaveBeenCalled() }) - it("should route through webview for both new and existing tasks", async () => { + it("should call handleWebviewAskResponse for both new and existing task states", async () => { const task = new Task({ provider: mockProvider, apiConfiguration: mockApiConfig, @@ -1580,19 +1583,17 @@ describe("Cline", () => { startTask: false, }) + // Spy on handleWebviewAskResponse + const handleResponseSpy = vi.spyOn(task, "handleWebviewAskResponse") + // 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"], - }) + expect(handleResponseSpy).toHaveBeenCalledWith("messageResponse", "new task", ["image1.png"]) // Clear mock - mockProvider.postMessageToWebview.mockClear() + handleResponseSpy.mockClear() // Test with existing messages (ongoing task scenario) task.clineMessages = [ @@ -1605,12 +1606,7 @@ describe("Cline", () => { ] task.submitUserMessage("follow-up message", ["image2.png"]) - expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({ - type: "invoke", - invoke: "sendMessage", - text: "follow-up message", - images: ["image2.png"], - }) + expect(handleResponseSpy).toHaveBeenCalledWith("messageResponse", "follow-up message", ["image2.png"]) }) it("should handle undefined provider gracefully", async () => { @@ -1621,6 +1617,9 @@ describe("Cline", () => { startTask: false, }) + // Spy on handleWebviewAskResponse + const handleResponseSpy = vi.spyOn(task, "handleWebviewAskResponse") + // Simulate weakref returning undefined Object.defineProperty(task, "providerRef", { value: { deref: () => undefined }, @@ -1635,7 +1634,7 @@ describe("Cline", () => { task.submitUserMessage("test message") expect(consoleErrorSpy).toHaveBeenCalledWith("[Task#submitUserMessage] Provider reference lost") - expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled() + expect(handleResponseSpy).not.toHaveBeenCalled() // Restore console.error consoleErrorSpy.mockRestore() diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 94370357685..8c85ad07a5b 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -862,7 +862,11 @@ export class ClineProvider this.webviewDisposables.push(configDisposable) // If the extension is starting a new session, clear previous task state. - await this.removeClineFromStack() + // But don't clear if there's already an active task (e.g., resumed via IPC/bridge). + const currentTask = this.getCurrentTask() + if (!currentTask || currentTask.abandoned || currentTask.abort) { + await this.removeClineFromStack() + } } public async createTaskWithHistoryItem(