From 6aa129ec5e7011ef15ef69cf217f92f731e13995 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 12 Aug 2025 19:18:22 +0000 Subject: [PATCH] fix: ensure cancel button properly aborts streaming responses - Set abort flag immediately in ClineProvider.cancelTask() before calling abortTask() - Add early abort check at the beginning of streaming loop before processing chunks - Add proper iterator cleanup when aborting stream - Keep existing abort check after chunk processing for redundancy This ensures the cancel button works even when chunks are arriving constantly, addressing the issue where the abort flag appeared to never be set. Fixes #7014 --- src/core/task/Task.ts | 28 +++++++++++++++++++++++++++- src/core/webview/ClineProvider.ts | 6 +++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index cb6694b7f04..3fb059103b3 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1663,6 +1663,26 @@ export class Task extends EventEmitter implements TaskLike { const iterator = stream[Symbol.asyncIterator]() let item = await iterator.next() while (!item.done) { + // Check abort flag BEFORE processing the chunk + if (this.abort) { + console.log(`aborting stream (early check), 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).catch(() => {}) + } + + break // Aborts the stream. + } + const chunk = item.value item = await iterator.next() if (!chunk) { @@ -1707,8 +1727,9 @@ export class Task extends EventEmitter implements TaskLike { } } + // Check abort flag AFTER processing the chunk as well if (this.abort) { - console.log(`aborting stream, this.abandoned = ${this.abandoned}`) + console.log(`aborting stream (after chunk), this.abandoned = ${this.abandoned}`) if (!this.abandoned) { // Only need to gracefully abort if this instance @@ -1718,6 +1739,11 @@ export class Task extends EventEmitter implements TaskLike { await abortStream("user_cancelled") } + // Clean up the iterator if it has a return method + if (iterator.return) { + await iterator.return(undefined).catch(() => {}) + } + break // Aborts the stream. } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index e6817e1825f..d07ddaba271 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1228,6 +1228,10 @@ export class ClineProvider const rootTask = cline.rootTask const parentTask = cline.parentTask + // Set the abort flag immediately to signal cancellation + cline.abort = true + + // Then call abortTask to handle cleanup cline.abortTask() await pWaitFor( @@ -1243,7 +1247,7 @@ export class ClineProvider timeout: 3_000, }, ).catch(() => { - console.error("Failed to abort task") + console.error("Failed to abort task gracefully") }) if (this.getCurrentCline()) {