Skip to content

Commit b321db7

Browse files
committed
fix(task): drain queued messages while waiting for ask
1 parent 53e1ff0 commit b321db7

File tree

2 files changed

+69
-1
lines changed

2 files changed

+69
-1
lines changed

src/core/task/Task.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1241,7 +1241,37 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
12411241
}
12421242

12431243
// Wait for askResponse to be set
1244-
await pWaitFor(() => this.askResponse !== undefined || this.lastMessageTs !== askTs, { interval: 100 })
1244+
await pWaitFor(
1245+
() => {
1246+
if (this.askResponse !== undefined || this.lastMessageTs !== askTs) {
1247+
return true
1248+
}
1249+
1250+
// If a queued message arrives while we're blocked on an ask (e.g. a follow-up
1251+
// suggestion click that was incorrectly queued due to UI state), consume it
1252+
// immediately so the task doesn't hang.
1253+
if (!this.messageQueueService.isEmpty()) {
1254+
const message = this.messageQueueService.dequeueMessage()
1255+
if (message) {
1256+
// If this is a tool approval ask, we need to approve first (yesButtonClicked)
1257+
// and include any queued text/images.
1258+
if (
1259+
type === "tool" ||
1260+
type === "command" ||
1261+
type === "browser_action_launch" ||
1262+
type === "use_mcp_server"
1263+
) {
1264+
this.handleWebviewAskResponse("yesButtonClicked", message.text, message.images)
1265+
} else {
1266+
this.handleWebviewAskResponse("messageResponse", message.text, message.images)
1267+
}
1268+
}
1269+
}
1270+
1271+
return false
1272+
},
1273+
{ interval: 100 },
1274+
)
12451275

12461276
if (this.lastMessageTs !== askTs) {
12471277
// Could happen if we send multiple asks in a row i.e. with
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Task } from "../Task"
2+
3+
// Keep this test focused: if a queued message arrives while Task.ask() is blocked,
4+
// it should be consumed and used to fulfill the ask.
5+
6+
describe("Task.ask queued message drain", () => {
7+
it("consumes queued message while blocked on followup ask", async () => {
8+
const task = Object.create(Task.prototype) as Task
9+
;(task as any).abort = false
10+
;(task as any).clineMessages = []
11+
;(task as any).askResponse = undefined
12+
;(task as any).askResponseText = undefined
13+
;(task as any).askResponseImages = undefined
14+
;(task as any).lastMessageTs = undefined
15+
16+
// Message queue service exists in constructor; for unit test we can attach a real one.
17+
const { MessageQueueService } = await import("../../message-queue/MessageQueueService")
18+
;(task as any).messageQueueService = new MessageQueueService()
19+
20+
// Minimal stubs used by ask()
21+
;(task as any).addToClineMessages = vi.fn(async () => {})
22+
;(task as any).saveClineMessages = vi.fn(async () => {})
23+
;(task as any).updateClineMessage = vi.fn(async () => {})
24+
;(task as any).cancelAutoApprovalTimeout = vi.fn(() => {})
25+
;(task as any).checkpointSave = vi.fn(async () => {})
26+
;(task as any).emit = vi.fn()
27+
;(task as any).providerRef = { deref: () => undefined }
28+
29+
const askPromise = task.ask("followup", "Q?", false)
30+
31+
// Simulate webview queuing the user's selection text while the ask is pending.
32+
;(task as any).messageQueueService.addMessage("picked answer")
33+
34+
const result = await askPromise
35+
expect(result.response).toBe("messageResponse")
36+
expect(result.text).toBe("picked answer")
37+
})
38+
})

0 commit comments

Comments
 (0)