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
5 changes: 4 additions & 1 deletion src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1546,7 +1546,10 @@ export class Task extends EventEmitter<TaskEvents> 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")
}
Expand Down
51 changes: 25 additions & 26 deletions src/core/task/__tests__/Task.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1523,14 +1523,17 @@ 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,
task: "initial task",
startTask: false,
})

// Spy on handleWebviewAskResponse
const handleResponseSpy = vi.spyOn(task, "handleWebviewAskResponse")

// Set up some existing messages to simulate an ongoing conversation
task.clineMessages = [
{
Expand All @@ -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 () => {
Expand All @@ -1561,38 +1561,39 @@ 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,
task: "initial task",
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 = [
Expand All @@ -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 () => {
Expand All @@ -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 },
Expand All @@ -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()
Expand Down
6 changes: 5 additions & 1 deletion src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading