From bb9151c433695477dd7d67401d225535c61518a8 Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Tue, 9 Sep 2025 13:12:35 -0500 Subject: [PATCH 1/5] Fix message queue re-queue loop in Task.ask() When the queue had messages during an ask operation, Task.ask() would call submitUserMessage() which posts an 'invoke: sendMessage' to the webview. However, since sendingDisabled is true during ask operations, the webview would re-queue the message, creating an infinite loop. Fixed by directly calling setMessageResponse() when processing queued messages, bypassing the webview round-trip and immediately satisfying the pending ask operation. --- src/core/task/Task.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index c5be865731a..b9173231ebb 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -845,9 +845,8 @@ export class Task extends EventEmitter implements TaskLike { const message = this.messageQueueService.dequeueMessage() if (message) { - setTimeout(async () => { - await this.submitUserMessage(message.text, message.images) - }, 0) + // Fulfill the ask directly to avoid re-queuing via the webview while sending is disabled + this.setMessageResponse(message.text, message.images) } } From 716a2dca86e45f39ffbea1c72af510a8e21cd4e3 Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Tue, 9 Sep 2025 14:11:44 -0500 Subject: [PATCH 2/5] fix: process queued messages after file edit operations complete When a file is edited using tools (write_to_file, apply_diff, insert_content, search_and_replace), any user messages that were queued during the edit operation were not being sent afterwards. This fix ensures that after each file edit tool completes, it checks the message queue and processes one queued message if present. The fix prevents messages from getting stuck in the queue after file edits, ensuring a smooth user experience when typing while the assistant is performing file operations. --- src/core/tools/applyDiffTool.ts | 16 ++++++++++++++++ src/core/tools/insertContentTool.ts | 16 ++++++++++++++++ src/core/tools/searchAndReplaceTool.ts | 18 ++++++++++++++++++ src/core/tools/writeToFileTool.ts | 18 ++++++++++++++++++ 4 files changed, 68 insertions(+) diff --git a/src/core/tools/applyDiffTool.ts b/src/core/tools/applyDiffTool.ts index 903e3c846ec..85139a8e91f 100644 --- a/src/core/tools/applyDiffTool.ts +++ b/src/core/tools/applyDiffTool.ts @@ -245,6 +245,22 @@ export async function applyDiffToolLegacy( await cline.diffViewProvider.reset() + // After completing file edits, process one queued user message if present + try { + if (!cline.messageQueueService.isEmpty()) { + const queued = cline.messageQueueService.dequeueMessage() + if (queued) { + setTimeout(() => { + cline + .submitUserMessage(queued.text, queued.images) + .catch((err) => console.error("[applyDiffTool] Failed to submit queued message:", err)) + }, 0) + } + } + } catch (e) { + console.error("[applyDiffTool] Queue processing error:", e) + } + return } } catch (error) { diff --git a/src/core/tools/insertContentTool.ts b/src/core/tools/insertContentTool.ts index e22a3681671..f6cb52deee3 100644 --- a/src/core/tools/insertContentTool.ts +++ b/src/core/tools/insertContentTool.ts @@ -187,6 +187,22 @@ export async function insertContentTool( pushToolResult(message) await cline.diffViewProvider.reset() + + // After completing file edits, process one queued user message if present + try { + if (!cline.messageQueueService.isEmpty()) { + const queued = cline.messageQueueService.dequeueMessage() + if (queued) { + setTimeout(() => { + cline + .submitUserMessage(queued.text, queued.images) + .catch((err) => console.error("[insertContentTool] Failed to submit queued message:", err)) + }, 0) + } + } + } catch (e) { + console.error("[insertContentTool] Queue processing error:", e) + } } catch (error) { handleError("insert content", error) await cline.diffViewProvider.reset() diff --git a/src/core/tools/searchAndReplaceTool.ts b/src/core/tools/searchAndReplaceTool.ts index 49129344158..752b79bcef0 100644 --- a/src/core/tools/searchAndReplaceTool.ts +++ b/src/core/tools/searchAndReplaceTool.ts @@ -263,6 +263,24 @@ export async function searchAndReplaceTool( // Record successful tool usage and cleanup cline.recordToolUsage("search_and_replace") await cline.diffViewProvider.reset() + + // After completing file edits, process one queued user message if present + try { + if (!cline.messageQueueService.isEmpty()) { + const queued = cline.messageQueueService.dequeueMessage() + if (queued) { + setTimeout(() => { + cline + .submitUserMessage(queued.text, queued.images) + .catch((err) => + console.error("[searchAndReplaceTool] Failed to submit queued message:", err), + ) + }, 0) + } + } + } catch (e) { + console.error("[searchAndReplaceTool] Queue processing error:", e) + } } catch (error) { handleError("search and replace", error) await cline.diffViewProvider.reset() diff --git a/src/core/tools/writeToFileTool.ts b/src/core/tools/writeToFileTool.ts index e82eab92bc8..e41e2d49990 100644 --- a/src/core/tools/writeToFileTool.ts +++ b/src/core/tools/writeToFileTool.ts @@ -308,6 +308,24 @@ export async function writeToFileTool( await cline.diffViewProvider.reset() + // After completing file edits, process one queued user message if present + try { + if (!cline.messageQueueService.isEmpty()) { + const queued = cline.messageQueueService.dequeueMessage() + if (queued) { + setTimeout(() => { + cline + .submitUserMessage(queued.text, queued.images) + .catch((err) => + console.error("[writeToFileTool] Failed to submit queued message:", err), + ) + }, 0) + } + } + } catch (e) { + console.error("[writeToFileTool] Queue processing error:", e) + } + return } } catch (error) { From 06dc18b6d8013a91211963ad44b091dee0eaadcd Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Tue, 9 Sep 2025 14:46:02 -0500 Subject: [PATCH 3/5] refactor: add centralized message queue processing after file edits - Added processQueuedMessages() method to Task class for centralized queue handling - Integrated queue processing in all file editing tools: - writeToFileTool: processes queue after completion - applyDiffTool: processes queue after completion, rejection, or error - insertContentTool: processes queue after completion - searchAndReplaceTool: processes queue after completion - multiApplyDiffTool: processes queue after completion or error - Ensures user messages typed during file edits are sent immediately after edit completes - Prevents messages from getting stuck in the queue --- src/core/task/Task.ts | 24 ++++++++++++++++++++++++ src/core/tools/applyDiffTool.ts | 19 ++++--------------- src/core/tools/insertContentTool.ts | 17 ++--------------- src/core/tools/multiApplyDiffTool.ts | 5 +++++ src/core/tools/searchAndReplaceTool.ts | 19 ++----------------- src/core/tools/writeToFileTool.ts | 19 ++----------------- 6 files changed, 39 insertions(+), 64 deletions(-) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index b9173231ebb..5a736434f56 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -2897,4 +2897,28 @@ export class Task extends EventEmitter implements TaskLike { public get cwd() { return this.workspacePath } + + /** + * Process any queued messages by dequeuing and submitting them. + * This ensures that queued user messages are sent when appropriate, + * preventing them from getting stuck in the queue. + * + * @param context - Context string for logging (e.g., the calling tool name) + */ + public processQueuedMessages(): void { + try { + if (!this.messageQueueService.isEmpty()) { + const queued = this.messageQueueService.dequeueMessage() + if (queued) { + setTimeout(() => { + this.submitUserMessage(queued.text, queued.images).catch((err) => + console.error(`[Task] Failed to submit queued message:`, err), + ) + }, 0) + } + } + } catch (e) { + console.error(`[Task] Queue processing error:`, e) + } + } } diff --git a/src/core/tools/applyDiffTool.ts b/src/core/tools/applyDiffTool.ts index 85139a8e91f..dcdd1346240 100644 --- a/src/core/tools/applyDiffTool.ts +++ b/src/core/tools/applyDiffTool.ts @@ -207,6 +207,7 @@ export async function applyDiffToolLegacy( if (!didApprove) { await cline.diffViewProvider.revertChanges() // Cline likely handles closing the diff view + cline.processQueuedMessages() return } @@ -245,27 +246,15 @@ export async function applyDiffToolLegacy( await cline.diffViewProvider.reset() - // After completing file edits, process one queued user message if present - try { - if (!cline.messageQueueService.isEmpty()) { - const queued = cline.messageQueueService.dequeueMessage() - if (queued) { - setTimeout(() => { - cline - .submitUserMessage(queued.text, queued.images) - .catch((err) => console.error("[applyDiffTool] Failed to submit queued message:", err)) - }, 0) - } - } - } catch (e) { - console.error("[applyDiffTool] Queue processing error:", e) - } + // Process any queued messages after file edit completes + cline.processQueuedMessages() return } } catch (error) { await handleError("applying diff", error) await cline.diffViewProvider.reset() + cline.processQueuedMessages() return } } diff --git a/src/core/tools/insertContentTool.ts b/src/core/tools/insertContentTool.ts index f6cb52deee3..e7d3a06ab92 100644 --- a/src/core/tools/insertContentTool.ts +++ b/src/core/tools/insertContentTool.ts @@ -188,21 +188,8 @@ export async function insertContentTool( await cline.diffViewProvider.reset() - // After completing file edits, process one queued user message if present - try { - if (!cline.messageQueueService.isEmpty()) { - const queued = cline.messageQueueService.dequeueMessage() - if (queued) { - setTimeout(() => { - cline - .submitUserMessage(queued.text, queued.images) - .catch((err) => console.error("[insertContentTool] Failed to submit queued message:", err)) - }, 0) - } - } - } catch (e) { - console.error("[insertContentTool] Queue processing error:", e) - } + // Process any queued messages after file edit completes + cline.processQueuedMessages() } catch (error) { handleError("insert content", error) await cline.diffViewProvider.reset() diff --git a/src/core/tools/multiApplyDiffTool.ts b/src/core/tools/multiApplyDiffTool.ts index 50695b1da73..d0fe6557503 100644 --- a/src/core/tools/multiApplyDiffTool.ts +++ b/src/core/tools/multiApplyDiffTool.ts @@ -172,6 +172,7 @@ Original error: ${errorMessage}` TelemetryService.instance.captureDiffApplicationError(cline.taskId, cline.consecutiveMistakeCount) await cline.say("diff_error", `Failed to parse apply_diff XML: ${errorMessage}`) pushToolResult(detailedError) + cline.processQueuedMessages() return } } else if (legacyPath && typeof legacyDiffContent === "string") { @@ -195,6 +196,7 @@ Original error: ${errorMessage}` "args (or legacy 'path' and 'diff' parameters)", ) pushToolResult(errorMsg) + cline.processQueuedMessages() return } @@ -210,6 +212,7 @@ Original error: ${errorMessage}` : "args (must contain at least one valid file element)", ), ) + cline.processQueuedMessages() return } @@ -675,10 +678,12 @@ ${errorDetails ? `\nTechnical details:\n${errorDetails}\n` : ""} // Push the final result combining all operation results pushToolResult(results.join("\n\n") + singleBlockNotice) + cline.processQueuedMessages() return } catch (error) { await handleError("applying diff", error) await cline.diffViewProvider.reset() + cline.processQueuedMessages() return } } diff --git a/src/core/tools/searchAndReplaceTool.ts b/src/core/tools/searchAndReplaceTool.ts index 752b79bcef0..b0ee3947e1e 100644 --- a/src/core/tools/searchAndReplaceTool.ts +++ b/src/core/tools/searchAndReplaceTool.ts @@ -264,23 +264,8 @@ export async function searchAndReplaceTool( cline.recordToolUsage("search_and_replace") await cline.diffViewProvider.reset() - // After completing file edits, process one queued user message if present - try { - if (!cline.messageQueueService.isEmpty()) { - const queued = cline.messageQueueService.dequeueMessage() - if (queued) { - setTimeout(() => { - cline - .submitUserMessage(queued.text, queued.images) - .catch((err) => - console.error("[searchAndReplaceTool] Failed to submit queued message:", err), - ) - }, 0) - } - } - } catch (e) { - console.error("[searchAndReplaceTool] Queue processing error:", e) - } + // Process any queued messages after file edit completes + cline.processQueuedMessages() } catch (error) { handleError("search and replace", error) await cline.diffViewProvider.reset() diff --git a/src/core/tools/writeToFileTool.ts b/src/core/tools/writeToFileTool.ts index e41e2d49990..5abd96a20af 100644 --- a/src/core/tools/writeToFileTool.ts +++ b/src/core/tools/writeToFileTool.ts @@ -308,23 +308,8 @@ export async function writeToFileTool( await cline.diffViewProvider.reset() - // After completing file edits, process one queued user message if present - try { - if (!cline.messageQueueService.isEmpty()) { - const queued = cline.messageQueueService.dequeueMessage() - if (queued) { - setTimeout(() => { - cline - .submitUserMessage(queued.text, queued.images) - .catch((err) => - console.error("[writeToFileTool] Failed to submit queued message:", err), - ) - }, 0) - } - } - } catch (e) { - console.error("[writeToFileTool] Queue processing error:", e) - } + // Process any queued messages after file edit completes + cline.processQueuedMessages() return } From 652b77ef60b26dab0e12cae829e407e2d98dd04b Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Tue, 9 Sep 2025 15:21:37 -0500 Subject: [PATCH 4/5] fix: add processQueuedMessages to mock cline objects in tests --- src/core/tools/__tests__/applyDiffTool.experiment.spec.ts | 1 + src/core/tools/__tests__/multiApplyDiffTool.spec.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/core/tools/__tests__/applyDiffTool.experiment.spec.ts b/src/core/tools/__tests__/applyDiffTool.experiment.spec.ts index e763125d4a4..f82d4b1820c 100644 --- a/src/core/tools/__tests__/applyDiffTool.experiment.spec.ts +++ b/src/core/tools/__tests__/applyDiffTool.experiment.spec.ts @@ -40,6 +40,7 @@ describe("applyDiffTool experiment routing", () => { api: { getModel: vi.fn().mockReturnValue({ id: "test-model" }), }, + processQueuedMessages: vi.fn(), } as any mockBlock = { diff --git a/src/core/tools/__tests__/multiApplyDiffTool.spec.ts b/src/core/tools/__tests__/multiApplyDiffTool.spec.ts index 0bdedb9cd56..5e591f9fe79 100644 --- a/src/core/tools/__tests__/multiApplyDiffTool.spec.ts +++ b/src/core/tools/__tests__/multiApplyDiffTool.spec.ts @@ -91,6 +91,7 @@ describe("multiApplyDiffTool", () => { trackFileContext: vi.fn().mockResolvedValue(undefined), }, didEditFile: false, + processQueuedMessages: vi.fn(), } as any mockAskApproval = vi.fn().mockResolvedValue(true) From 400f26e7e31b7998659338680a2b0384f06bd49b Mon Sep 17 00:00:00 2001 From: daniel-lxs Date: Thu, 11 Sep 2025 19:53:37 -0500 Subject: [PATCH 5/5] Fix approval flow for queued messages in Task.ts When messages are queued while a tool is waiting for approval, they now properly handle the approval before sending. This prevents tools from being rejected when processing queued messages. - Modified Task.ts to check if the ask type requires approval (tool, command, browser_action_launch, use_mcp_server) - For approval-required asks, use handleWebviewAskResponse('yesButtonClicked') instead of setMessageResponse() - For other ask types like followup, continue using setMessageResponse() as before This ensures the correct order: approve tools first, then send messages. --- src/core/task/Task.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 5a736434f56..cf16df8dcc7 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -845,8 +845,19 @@ export class Task extends EventEmitter implements TaskLike { const message = this.messageQueueService.dequeueMessage() if (message) { - // Fulfill the ask directly to avoid re-queuing via the webview while sending is disabled - this.setMessageResponse(message.text, message.images) + // Check if this is a tool approval ask that needs to be handled + if ( + type === "tool" || + type === "command" || + type === "browser_action_launch" || + 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) + } else { + // For other ask types (like followup), fulfill the ask directly + this.setMessageResponse(message.text, message.images) + } } }