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

This commit is contained in:
Chris Estreich 2026-01-22 02:31:51 -08:00 • committed by GitHub
parent 94459adba1
commit 13e090ef81
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 34 additions and 28 deletions

View file

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

View file

@ -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()

View file

@ -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(