From c93d6908e2718562b80c7bc71c73b23572edee70 Mon Sep 17 00:00:00 2001 From: Daniel Riccio Date: Wed, 6 Aug 2025 19:04:54 -0500 Subject: [PATCH] fix: eliminate UI rerender during task cancellation - Add resetToResumableState() method to Task class to reset internal state without recreation - Simplify cancellation flow to handle everything within Task class - Update abortStream() to properly mark API requests as cancelled - Remove initClineWithHistoryItem() call from ClineProvider.cancelTask() - Task instance now persists through cancellation, preventing UI flicker The task now handles its own cancellation and resumption internally, maintaining the same instance throughout. This provides a seamless user experience with no visual disruption when cancelling and resuming tasks. --- src/core/task/Task.ts | 77 +++++++++++++++++++++++++++---- src/core/webview/ClineProvider.ts | 35 ++------------ 2 files changed, 72 insertions(+), 40 deletions(-) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 60fceb2bb8..18828a3088 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1489,8 +1489,8 @@ export class Task extends EventEmitter implements TaskLike { } } - public async abortTask(isAbandoned = false) { - // Aborting task +public async abortTask(isAbandoned = false, skipSave = false) { + console.log(`[subtasks] aborting task ${this.taskId}.${this.instanceId}`) // Will stop any autonomously running promises. if (isAbandoned) { @@ -1506,15 +1506,74 @@ export class Task extends EventEmitter implements TaskLike { console.error(`Error during task ${this.taskId}.${this.instanceId} disposal:`, error) // Don't rethrow - we want abort to always succeed } - // Save the countdown message in the automatic retry or other content. - try { - // Save the countdown message in the automatic retry or other content. - await this.saveClineMessages() - } catch (error) { - console.error(`Error saving messages during abort for task ${this.taskId}.${this.instanceId}:`, error) + + // Only save messages if not skipping (e.g., during user cancellation where messages are already saved) + if (!skipSave) { + try { + // Save the countdown message in the automatic retry or other content. + await this.saveClineMessages() + } catch (error) { + console.error(`Error saving messages during abort for task ${this.taskId}.${this.instanceId}:`, error) + } } } + /** + * Reset the task to a resumable state without recreating the instance. + * This is used when canceling a task to avoid unnecessary rerenders. + */ + public async resetToResumableState() { + console.log(`[subtasks] resetting task ${this.taskId}.${this.instanceId} to resumable state`) + + // Reset abort flags + this.abort = false + this.abandoned = false + + // Reset streaming state + this.isStreaming = false + this.isWaitingForFirstChunk = false + this.didFinishAbortingStream = true + this.didCompleteReadingStream = false + + // Clear streaming content + this.currentStreamingContentIndex = 0 + this.currentStreamingDidCheckpoint = false + this.assistantMessageContent = [] + this.userMessageContent = [] + this.userMessageContentReady = false + this.didRejectTool = false + this.didAlreadyUseTool = false + this.presentAssistantMessageLocked = false + this.presentAssistantMessageHasPendingUpdates = false + + // Reset API state + this.consecutiveMistakeCount = 0 + + // Reset ask response state to allow new messages + this.askResponse = undefined + this.askResponseText = undefined + this.askResponseImages = undefined + this.blockingAsk = undefined + + // Reset parser if exists + if (this.assistantMessageParser) { + this.assistantMessageParser.reset() + } + + // Only reset diff view if it's actively editing + // This avoids unnecessary operations when diff view is not in use + if (this.diffViewProvider && this.diffViewProvider.isEditing) { + await this.diffViewProvider.reset() + } + + // The task is now ready to be resumed + // The API request status has already been updated by abortStream + // We don't add the resume_task message here because ask() will add it + + // Keep messages and history intact for resumption + // The task is now ready to be resumed without recreation + } + // Used when a sub-task is launched and the parent task is waiting for it to // finish. // TBD: The 1s should be added to the settings, also should add a timeout to @@ -1584,7 +1643,7 @@ export class Task extends EventEmitter implements TaskLike { const currentUserContent = currentItem.userContent const currentIncludeFileDetails = currentItem.includeFileDetails - if (this.abort) { +if (this.abort) { throw new Error(`[RooCode#recursivelyMakeRooRequests] task ${this.taskId}.${this.instanceId} aborted`) } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 9e4434745f..61b2f0465e 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1279,38 +1279,11 @@ export class ClineProvider console.log(`[subtasks] cancelling task ${cline.taskId}.${cline.instanceId}`) - const { historyItem } = await this.getTaskWithId(cline.taskId) - // Preserve parent and root task information for history item. - const rootTask = cline.rootTask - const parentTask = cline.parentTask + // Just set the abort flag - the task will handle its own resumption + cline.abort = true - cline.abortTask() - - await pWaitFor( - () => - this.getCurrentTask()! === undefined || - this.getCurrentTask()!.isStreaming === false || - this.getCurrentTask()!.didFinishAbortingStream || - // If only the first chunk is processed, then there's no - // need to wait for graceful abort (closes edits, browser, - // etc). - this.getCurrentTask()!.isWaitingForFirstChunk, - { - timeout: 3_000, - }, - ).catch(() => { - console.error("Failed to abort task") - }) - - if (this.getCurrentTask()) { - // 'abandoned' will prevent this Cline instance from affecting - // future Cline instances. This may happen if its hanging on a - // streaming request. - this.getCurrentTask()!.abandoned = true - } - - // Clears task again, so we need to abortTask manually above. - await this.createTaskWithHistoryItem({ ...historyItem, rootTask, parentTask }) + // The task's streaming loop will detect the abort flag and handle the resumption + // No need to wait or do anything else here } async updateCustomInstructions(instructions?: string) {