Skip to content
Draft
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
16 changes: 10 additions & 6 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1432,9 +1432,12 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
if (message) {
// Check if this is a tool approval ask that needs to be handled.
if (type === "tool" || type === "command" || type === "use_mcp_server") {
// For tool approvals, we need to approve first, then send
// the message if there's text/images.
this.handleWebviewAskResponse("yesButtonClicked", message.text, message.images)
// For tool/command approvals, reject the pending tool and
// send the queued text as a plain message -- matching the
// behaviour when a user types a reply without clicking
// Approve. This prevents queued messages from
// unconditionally auto-approving unapproved commands.
this.handleWebviewAskResponse("messageResponse", message.text, message.images)
} else {
// For other ask types (like followup or command_output), fulfill the ask
// directly.
Expand All @@ -1456,10 +1459,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
if (shouldDrainQueuedMessageForAsk && !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 this is a tool approval ask, reject the pending tool and
// forward the queued text as a plain message so that
// unapproved commands are never auto-executed.
if (type === "tool" || type === "command" || type === "use_mcp_server") {
this.handleWebviewAskResponse("yesButtonClicked", message.text, message.images)
this.handleWebviewAskResponse("messageResponse", message.text, message.images)
} else {
this.handleWebviewAskResponse("messageResponse", message.text, message.images)
}
Expand Down
83 changes: 83 additions & 0 deletions src/core/task/__tests__/ask-queued-message-drain.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,89 @@ describe("Task.ask queued message drain", () => {
expect(result.text).toBe("picked answer")
})

it("rejects unapproved command asks with messageResponse when queued message is drained", 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

const { MessageQueueService } = await import("../../message-queue/MessageQueueService")
;(task as any).messageQueueService = new MessageQueueService()
;(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 }

// Queue a message before asking for command approval.
;(task as any).messageQueueService.addMessage("continue please")

const result = await task.ask("command", "rm -rf /tmp/test", false)

// The queued message must NOT approve the command -- it should reject
// with messageResponse, matching the behaviour of typing a reply
// without clicking Approve.
expect(result.response).toBe("messageResponse")
expect(result.text).toBe("continue please")
})

it("rejects unapproved tool asks with messageResponse when queued message is drained", 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

const { MessageQueueService } = await import("../../message-queue/MessageQueueService")
;(task as any).messageQueueService = new MessageQueueService()
;(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 }
;(task as any).messageQueueService.addMessage("go ahead")

const result = await task.ask("tool", "write_to_file /etc/hosts", false)

expect(result.response).toBe("messageResponse")
expect(result.text).toBe("go ahead")
})

it("rejects unapproved use_mcp_server asks with messageResponse when queued message is drained", 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

const { MessageQueueService } = await import("../../message-queue/MessageQueueService")
;(task as any).messageQueueService = new MessageQueueService()
;(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 }
;(task as any).messageQueueService.addMessage("next step")

const result = await task.ask("use_mcp_server", "mcp server call", false)

expect(result.response).toBe("messageResponse")
expect(result.text).toBe("next step")
})

it("does not consume queued messages for command_output asks", async () => {
const task = Object.create(Task.prototype) as Task
;(task as any).abort = false
Expand Down
Loading