From 7eaa8e4c12aa99d193a1941a369f035b6d2b3c6e Mon Sep 17 00:00:00 2001 From: Roo Code Date: Thu, 16 Oct 2025 01:00:15 +0000 Subject: [PATCH] fix: prevent chat UI flickering during cancel operations and checkpoint restoration - Added isSoftReloading flag to ClineProvider to track soft reload state - Updated ExtensionMessage interface to include optional isSoftReload property - Modified cancelTask to set/reset soft reload flag around task recreation - Updated checkpointRestoreHandler to use soft reload flag for checkpoint operations - Added UI-side handling in ChatView to check isSoftReload flag and skip re-rendering - Implemented debouncing mechanism (50ms) for state updates to prevent rapid flickering - Cancel button now passes isSoftReload flag to prevent UI flicker This fixes the issue where the chat window would flicker and sometimes duplicate messages when users press Cancel to stop an API response or when restoring checkpoints after editing messages. Fixes #8677 --- src/core/webview/ClineProvider.ts | 69 ++++++++++++++++++-- src/core/webview/checkpointRestoreHandler.ts | 12 ++++ src/shared/ExtensionMessage.ts | 1 + webview-ui/src/components/chat/ChatView.tsx | 10 ++- 4 files changed, 84 insertions(+), 8 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 91b8687966..72e7a8b30f 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -141,6 +141,8 @@ export class ClineProvider private recentTasksCache?: string[] private pendingOperations: Map = new Map() private static readonly PENDING_OPERATION_TIMEOUT_MS = 30000 // 30 seconds + private isSoftReloading = false // Flag to indicate soft reload state (cancel/checkpoint restore) + private stateUpdateDebounceTimer: NodeJS.Timeout | null = null // Debounce timer for state updates public isViewLaunched = false public settingsImportedAt?: number @@ -583,6 +585,12 @@ export class ClineProvider this.clearAllPendingEditOperations() this.log("Cleared pending operations") + // Clear debounce timer if it exists + if (this.stateUpdateDebounceTimer) { + clearTimeout(this.stateUpdateDebounceTimer) + this.stateUpdateDebounceTimer = null + } + if (this.view && "dispose" in this.view) { this.view.dispose() this.log("Disposed webview") @@ -1602,14 +1610,48 @@ export class ClineProvider } async postStateToWebview() { - const state = await this.getStateToPostToWebview() - this.postMessageToWebview({ type: "state", state }) - - // Check MDM compliance and send user to account tab if not compliant - // Only redirect if there's an actual MDM policy requiring authentication - if (this.mdmService?.requiresCloudAuth() && !this.checkMdmCompliance()) { - await this.postMessageToWebview({ type: "action", action: "cloudButtonClicked" }) + // Clear existing debounce timer if it exists + if (this.stateUpdateDebounceTimer) { + clearTimeout(this.stateUpdateDebounceTimer) + this.stateUpdateDebounceTimer = null } + + // If we're in soft reload mode, send state immediately without debouncing + if (this.isSoftReloading) { + const state = await this.getStateToPostToWebview() + // Include soft reload flag to prevent UI flickering + this.postMessageToWebview({ + type: "state", + state, + isSoftReload: this.isSoftReloading, + }) + + // Check MDM compliance and send user to account tab if not compliant + // Only redirect if there's an actual MDM policy requiring authentication + if (this.mdmService?.requiresCloudAuth() && !this.checkMdmCompliance()) { + await this.postMessageToWebview({ type: "action", action: "cloudButtonClicked" }) + } + return + } + + // Debounce state updates to prevent rapid flickering + this.stateUpdateDebounceTimer = setTimeout(async () => { + const state = await this.getStateToPostToWebview() + // Include soft reload flag to prevent UI flickering + this.postMessageToWebview({ + type: "state", + state, + isSoftReload: this.isSoftReloading, + }) + + // Check MDM compliance and send user to account tab if not compliant + // Only redirect if there's an actual MDM policy requiring authentication + if (this.mdmService?.requiresCloudAuth() && !this.checkMdmCompliance()) { + await this.postMessageToWebview({ type: "action", action: "cloudButtonClicked" }) + } + + this.stateUpdateDebounceTimer = null + }, 50) // 50ms debounce delay } /** @@ -2595,6 +2637,9 @@ export class ClineProvider // Capture the current instance to detect if rehydrate already occurred elsewhere const originalInstanceId = task.instanceId + // Set soft reload flag to prevent UI flickering + this.isSoftReloading = true + // Begin abort (non-blocking) task.abortTask() @@ -2623,6 +2668,8 @@ export class ClineProvider this.log( `[cancelTask] Skipping rehydrate: current instance ${current.instanceId} != original ${originalInstanceId}`, ) + // Reset soft reload flag + this.isSoftReloading = false return } @@ -2633,12 +2680,20 @@ export class ClineProvider this.log( `[cancelTask] Skipping rehydrate after final check: current instance ${currentAfterCheck.instanceId} != original ${originalInstanceId}`, ) + // Reset soft reload flag + this.isSoftReloading = false return } } // Clears task again, so we need to abortTask manually above. await this.createTaskWithHistoryItem({ ...historyItem, rootTask, parentTask }) + + // Reset soft reload flag after task is recreated + this.isSoftReloading = false + + // Send a refresh without flickering + await this.postStateToWebview() } // Clear the current task without treating it as a subtask. diff --git a/src/core/webview/checkpointRestoreHandler.ts b/src/core/webview/checkpointRestoreHandler.ts index a3f62f74f3..339d2365b0 100644 --- a/src/core/webview/checkpointRestoreHandler.ts +++ b/src/core/webview/checkpointRestoreHandler.ts @@ -27,6 +27,9 @@ export async function handleCheckpointRestoreOperation(config: CheckpointRestore const { provider, currentCline, messageTs, checkpoint, operation, editData } = config try { + // Set soft reload flag to prevent UI flickering + ;(provider as any).isSoftReloading = true + // For delete operations, ensure the task is properly aborted to handle any pending ask operations // This prevents "Current ask promise was ignored" errors // For edit operations, we don't abort because the checkpoint restore will handle it @@ -78,7 +81,16 @@ export async function handleCheckpointRestoreOperation(config: CheckpointRestore } // For edit operations, the task cancellation in checkpointRestore // will trigger reinitialization, which will process pendingEditAfterRestore + + // Reset soft reload flag after operation completes + ;(provider as any).isSoftReloading = false + + // Send a refresh without flickering + await provider.postStateToWebview() } catch (error) { + // Reset soft reload flag on error + ;(provider as any).isSoftReloading = false + console.error(`Error in checkpoint restore (${operation}):`, error) vscode.window.showErrorMessage( `Error during checkpoint restore: ${error instanceof Error ? error.message : String(error)}`, diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 66f389f81c..09e4d786c3 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -205,6 +205,7 @@ export interface ExtensionMessage { queuedMessages?: QueuedMessage[] list?: string[] // For dismissedUpsells organizationId?: string | null // For organizationSwitchResult + isSoftReload?: boolean // Flag to indicate soft reload state (cancel/checkpoint restore) to prevent UI flickering } export type ExtensionState = Pick< diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index d358c68f1c..28568908f5 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -728,7 +728,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction { const message: ExtensionMessage = e.data + // Check for soft reload flag to prevent UI flickering + if (message.isSoftReload === true) { + // During soft reload, we preserve UI state and skip certain operations + // that would cause flickering + return + } + switch (message.type) { case "action": switch (message.action!) {