From 84f719659b68d453f6a47580f7831ceb820f3cb1 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 12 Aug 2025 19:06:35 +0000 Subject: [PATCH] fix: fix cancel button not stopping streaming response - Move abort check before awaiting iterator.next() to prevent blocking - Add proper stream cleanup with iterator.return() when aborting - Apply same fix to background usage collection loop The issue was that the abort check happened after waiting for the next chunk, which blocked cancellation. Now the check happens before each await, allowing immediate response to cancel requests. Fixes #7014 --- src/core/task/Task.ts | 50 ++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index cb6694b7f04..4f40945c60d 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1663,11 +1663,32 @@ export class Task extends EventEmitter implements TaskLike { const iterator = stream[Symbol.asyncIterator]() let item = await iterator.next() while (!item.done) { + // Check for abort BEFORE processing the chunk and waiting for the next one + if (this.abort) { + console.log(`aborting stream, this.abandoned = ${this.abandoned}`) + + if (!this.abandoned) { + // Only need to gracefully abort if this instance + // isn't abandoned (sometimes OpenRouter stream + // hangs, in which case this would affect future + // instances of Cline). + await abortStream("user_cancelled") + } + + // Clean up the iterator if it has a return method + if (iterator.return) { + await iterator.return(undefined) + } + + break // Aborts the stream. + } + const chunk = item.value - item = await iterator.next() + if (!chunk) { // Sometimes chunk is undefined, no idea that can cause // it, but this workaround seems to fix it. + item = await iterator.next() continue } @@ -1707,20 +1728,6 @@ export class Task extends EventEmitter implements TaskLike { } } - if (this.abort) { - console.log(`aborting stream, this.abandoned = ${this.abandoned}`) - - if (!this.abandoned) { - // Only need to gracefully abort if this instance - // isn't abandoned (sometimes OpenRouter stream - // hangs, in which case this would affect future - // instances of Cline). - await abortStream("user_cancelled") - } - - break // Aborts the stream. - } - if (this.didRejectTool) { // `userContent` has a tool rejection, so interrupt the // assistant's response to present the user's feedback. @@ -1737,6 +1744,9 @@ export class Task extends EventEmitter implements TaskLike { "\n\n[Response interrupted by a tool use result. Only one tool may be used at a time and should be placed at the end of the message.]" break } + + // Get next item at the end, after all checks + item = await iterator.next() } // Create a copy of current token values to avoid race conditions @@ -1815,6 +1825,16 @@ export class Task extends EventEmitter implements TaskLike { // Use the same iterator that the main loop was using while (!item.done) { + // Check for abort first + if (this.abort) { + console.log(`[Background Usage Collection] Aborting due to task cancellation`) + // Clean up the iterator before breaking + if (iterator.return) { + await iterator.return(undefined) + } + break + } + // Check for timeout if (Date.now() - startTime > timeoutMs) { console.warn(