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!) {