Ver Fonte

fix: prevent task abortion when resuming via IPC/bridge (#10892)

Chris Estreich há 3 semanas atrás
pai
commit
13e090ef81

+ 4 - 1
src/core/task/Task.ts

@@ -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")
 			}

+ 25 - 26
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()

+ 5 - 1
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(