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
32 changes: 31 additions & 1 deletion src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1241,7 +1241,37 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}

// Wait for askResponse to be set
await pWaitFor(() => this.askResponse !== undefined || this.lastMessageTs !== askTs, { interval: 100 })
await pWaitFor(
() => {
if (this.askResponse !== undefined || this.lastMessageTs !== askTs) {
return true
}

// If a queued message arrives while we're blocked on an ask (e.g. a follow-up
// suggestion click that was incorrectly queued due to UI state), consume it
// immediately so the task doesn't hang.
if (!this.messageQueueService.isEmpty()) {
const message = this.messageQueueService.dequeueMessage()
if (message) {
// If this is a tool approval ask, we need to approve first (yesButtonClicked)
// and include any queued text/images.
if (
type === "tool" ||
type === "command" ||
type === "browser_action_launch" ||
type === "use_mcp_server"
) {
this.handleWebviewAskResponse("yesButtonClicked", message.text, message.images)
} else {
this.handleWebviewAskResponse("messageResponse", message.text, message.images)
}
}
}

return false
},
{ interval: 100 },
)

if (this.lastMessageTs !== askTs) {
// Could happen if we send multiple asks in a row i.e. with
Expand Down
38 changes: 38 additions & 0 deletions src/core/task/__tests__/ask-queued-message-drain.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Task } from "../Task"

// Keep this test focused: if a queued message arrives while Task.ask() is blocked,
// it should be consumed and used to fulfill the ask.

describe("Task.ask queued message drain", () => {
it("consumes queued message while blocked on followup ask", async () => {
const task = Object.create(Task.prototype) as Task
;(task as any).abort = false
;(task as any).clineMessages = []
;(task as any).askResponse = undefined
;(task as any).askResponseText = undefined
;(task as any).askResponseImages = undefined
;(task as any).lastMessageTs = undefined

// Message queue service exists in constructor; for unit test we can attach a real one.
const { MessageQueueService } = await import("../../message-queue/MessageQueueService")
;(task as any).messageQueueService = new MessageQueueService()

// Minimal stubs used by ask()
;(task as any).addToClineMessages = vi.fn(async () => {})
;(task as any).saveClineMessages = vi.fn(async () => {})
;(task as any).updateClineMessage = vi.fn(async () => {})
;(task as any).cancelAutoApprovalTimeout = vi.fn(() => {})
;(task as any).checkpointSave = vi.fn(async () => {})
;(task as any).emit = vi.fn()
;(task as any).providerRef = { deref: () => undefined }

const askPromise = task.ask("followup", "Q?", false)

// Simulate webview queuing the user's selection text while the ask is pending.
;(task as any).messageQueueService.addMessage("picked answer")

const result = await askPromise
expect(result.response).toBe("messageResponse")
expect(result.text).toBe("picked answer")
})
})
Loading