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
This commit is contained in:
Roo Code 2025-10-16 01:00:15 +00:00
parent 1a3a873002
commit 7eaa8e4c12
4 changed files with 84 additions and 8 deletions

View file

@ -141,6 +141,8 @@ export class ClineProvider
private recentTasksCache?: string[]
private pendingOperations: Map<string, PendingEditOperation> = 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.

View file

@ -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)}`,

View file

@ -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<

View file

@ -728,7 +728,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
const trimmedInput = text?.trim()
if (isStreaming) {
vscode.postMessage({ type: "cancelTask" })
// Set a flag to indicate soft reload for cancel operation
vscode.postMessage({ type: "cancelTask", isSoftReload: true })
setDidClickCancel(true)
return
}
@ -780,6 +781,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
(e: MessageEvent) => {
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!) {