Skip to content

Commit 13e090e

Browse files
authored
fix: prevent task abortion when resuming via IPC/bridge (#10892)
1 parent 94459ad commit 13e090e

3 files changed

Lines changed: 34 additions & 28 deletions

File tree

src/core/task/Task.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1546,7 +1546,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
15461546

15471547
this.emit(RooCodeEventName.TaskUserMessage, this.taskId)
15481548

1549-
provider.postMessageToWebview({ type: "invoke", invoke: "sendMessage", text, images })
1549+
// Handle the message directly instead of routing through the webview.
1550+
// This avoids a race condition where the webview's message state hasn't
1551+
// hydrated yet, causing it to interpret the message as a new task request.
1552+
this.handleWebviewAskResponse("messageResponse", text, images)
15501553
} else {
15511554
console.error("[Task#submitUserMessage] Provider reference lost")
15521555
}

src/core/task/__tests__/Task.spec.ts

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1523,14 +1523,17 @@ describe("Cline", () => {
15231523
})
15241524

15251525
describe("submitUserMessage", () => {
1526-
it("should always route through webview sendMessage invoke", async () => {
1526+
it("should call handleWebviewAskResponse directly", async () => {
15271527
const task = new Task({
15281528
provider: mockProvider,
15291529
apiConfiguration: mockApiConfig,
15301530
task: "initial task",
15311531
startTask: false,
15321532
})
15331533

1534+
// Spy on handleWebviewAskResponse
1535+
const handleResponseSpy = vi.spyOn(task, "handleWebviewAskResponse")
1536+
15341537
// Set up some existing messages to simulate an ongoing conversation
15351538
task.clineMessages = [
15361539
{
@@ -1544,13 +1547,10 @@ describe("Cline", () => {
15441547
// Call submitUserMessage
15451548
task.submitUserMessage("test message", ["image1.png"])
15461549

1547-
// Verify postMessageToWebview was called with sendMessage invoke
1548-
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
1549-
type: "invoke",
1550-
invoke: "sendMessage",
1551-
text: "test message",
1552-
images: ["image1.png"],
1553-
})
1550+
// Verify handleWebviewAskResponse was called directly (not webview)
1551+
expect(handleResponseSpy).toHaveBeenCalledWith("messageResponse", "test message", ["image1.png"])
1552+
// Should NOT route through webview anymore
1553+
expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled()
15541554
})
15551555

15561556
it("should handle empty messages gracefully", async () => {
@@ -1561,38 +1561,39 @@ describe("Cline", () => {
15611561
startTask: false,
15621562
})
15631563

1564+
// Spy on handleWebviewAskResponse
1565+
const handleResponseSpy = vi.spyOn(task, "handleWebviewAskResponse")
1566+
15641567
// Call with empty text and no images
15651568
task.submitUserMessage("", [])
15661569

1567-
// Should not call postMessageToWebview for empty messages
1568-
expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled()
1570+
// Should not call handleWebviewAskResponse for empty messages
1571+
expect(handleResponseSpy).not.toHaveBeenCalled()
15691572

15701573
// Call with whitespace only
15711574
task.submitUserMessage(" ", [])
1572-
expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled()
1575+
expect(handleResponseSpy).not.toHaveBeenCalled()
15731576
})
15741577

1575-
it("should route through webview for both new and existing tasks", async () => {
1578+
it("should call handleWebviewAskResponse for both new and existing task states", async () => {
15761579
const task = new Task({
15771580
provider: mockProvider,
15781581
apiConfiguration: mockApiConfig,
15791582
task: "initial task",
15801583
startTask: false,
15811584
})
15821585

1586+
// Spy on handleWebviewAskResponse
1587+
const handleResponseSpy = vi.spyOn(task, "handleWebviewAskResponse")
1588+
15831589
// Test with no messages (new task scenario)
15841590
task.clineMessages = []
15851591
task.submitUserMessage("new task", ["image1.png"])
15861592

1587-
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
1588-
type: "invoke",
1589-
invoke: "sendMessage",
1590-
text: "new task",
1591-
images: ["image1.png"],
1592-
})
1593+
expect(handleResponseSpy).toHaveBeenCalledWith("messageResponse", "new task", ["image1.png"])
15931594

15941595
// Clear mock
1595-
mockProvider.postMessageToWebview.mockClear()
1596+
handleResponseSpy.mockClear()
15961597

15971598
// Test with existing messages (ongoing task scenario)
15981599
task.clineMessages = [
@@ -1605,12 +1606,7 @@ describe("Cline", () => {
16051606
]
16061607
task.submitUserMessage("follow-up message", ["image2.png"])
16071608

1608-
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
1609-
type: "invoke",
1610-
invoke: "sendMessage",
1611-
text: "follow-up message",
1612-
images: ["image2.png"],
1613-
})
1609+
expect(handleResponseSpy).toHaveBeenCalledWith("messageResponse", "follow-up message", ["image2.png"])
16141610
})
16151611

16161612
it("should handle undefined provider gracefully", async () => {
@@ -1621,6 +1617,9 @@ describe("Cline", () => {
16211617
startTask: false,
16221618
})
16231619

1620+
// Spy on handleWebviewAskResponse
1621+
const handleResponseSpy = vi.spyOn(task, "handleWebviewAskResponse")
1622+
16241623
// Simulate weakref returning undefined
16251624
Object.defineProperty(task, "providerRef", {
16261625
value: { deref: () => undefined },
@@ -1635,7 +1634,7 @@ describe("Cline", () => {
16351634
task.submitUserMessage("test message")
16361635

16371636
expect(consoleErrorSpy).toHaveBeenCalledWith("[Task#submitUserMessage] Provider reference lost")
1638-
expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled()
1637+
expect(handleResponseSpy).not.toHaveBeenCalled()
16391638

16401639
// Restore console.error
16411640
consoleErrorSpy.mockRestore()

src/core/webview/ClineProvider.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -862,7 +862,11 @@ export class ClineProvider
862862
this.webviewDisposables.push(configDisposable)
863863

864864
// If the extension is starting a new session, clear previous task state.
865-
await this.removeClineFromStack()
865+
// But don't clear if there's already an active task (e.g., resumed via IPC/bridge).
866+
const currentTask = this.getCurrentTask()
867+
if (!currentTask || currentTask.abandoned || currentTask.abort) {
868+
await this.removeClineFromStack()
869+
}
866870
}
867871

868872
public async createTaskWithHistoryItem(

0 commit comments

Comments
 (0)