Skip to content

Commit 7eaa8e4

Browse files
committed
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
1 parent 1a3a873 commit 7eaa8e4

File tree

4 files changed

+83
-7
lines changed

4 files changed

+83
-7
lines changed

src/core/webview/ClineProvider.ts

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ export class ClineProvider
141141
private recentTasksCache?: string[]
142142
private pendingOperations: Map<string, PendingEditOperation> = new Map()
143143
private static readonly PENDING_OPERATION_TIMEOUT_MS = 30000 // 30 seconds
144+
private isSoftReloading = false // Flag to indicate soft reload state (cancel/checkpoint restore)
145+
private stateUpdateDebounceTimer: NodeJS.Timeout | null = null // Debounce timer for state updates
144146

145147
public isViewLaunched = false
146148
public settingsImportedAt?: number
@@ -583,6 +585,12 @@ export class ClineProvider
583585
this.clearAllPendingEditOperations()
584586
this.log("Cleared pending operations")
585587

588+
// Clear debounce timer if it exists
589+
if (this.stateUpdateDebounceTimer) {
590+
clearTimeout(this.stateUpdateDebounceTimer)
591+
this.stateUpdateDebounceTimer = null
592+
}
593+
586594
if (this.view && "dispose" in this.view) {
587595
this.view.dispose()
588596
this.log("Disposed webview")
@@ -1602,14 +1610,48 @@ export class ClineProvider
16021610
}
16031611

16041612
async postStateToWebview() {
1605-
const state = await this.getStateToPostToWebview()
1606-
this.postMessageToWebview({ type: "state", state })
1613+
// Clear existing debounce timer if it exists
1614+
if (this.stateUpdateDebounceTimer) {
1615+
clearTimeout(this.stateUpdateDebounceTimer)
1616+
this.stateUpdateDebounceTimer = null
1617+
}
16071618

1608-
// Check MDM compliance and send user to account tab if not compliant
1609-
// Only redirect if there's an actual MDM policy requiring authentication
1610-
if (this.mdmService?.requiresCloudAuth() && !this.checkMdmCompliance()) {
1611-
await this.postMessageToWebview({ type: "action", action: "cloudButtonClicked" })
1619+
// If we're in soft reload mode, send state immediately without debouncing
1620+
if (this.isSoftReloading) {
1621+
const state = await this.getStateToPostToWebview()
1622+
// Include soft reload flag to prevent UI flickering
1623+
this.postMessageToWebview({
1624+
type: "state",
1625+
state,
1626+
isSoftReload: this.isSoftReloading,
1627+
})
1628+
1629+
// Check MDM compliance and send user to account tab if not compliant
1630+
// Only redirect if there's an actual MDM policy requiring authentication
1631+
if (this.mdmService?.requiresCloudAuth() && !this.checkMdmCompliance()) {
1632+
await this.postMessageToWebview({ type: "action", action: "cloudButtonClicked" })
1633+
}
1634+
return
16121635
}
1636+
1637+
// Debounce state updates to prevent rapid flickering
1638+
this.stateUpdateDebounceTimer = setTimeout(async () => {
1639+
const state = await this.getStateToPostToWebview()
1640+
// Include soft reload flag to prevent UI flickering
1641+
this.postMessageToWebview({
1642+
type: "state",
1643+
state,
1644+
isSoftReload: this.isSoftReloading,
1645+
})
1646+
1647+
// Check MDM compliance and send user to account tab if not compliant
1648+
// Only redirect if there's an actual MDM policy requiring authentication
1649+
if (this.mdmService?.requiresCloudAuth() && !this.checkMdmCompliance()) {
1650+
await this.postMessageToWebview({ type: "action", action: "cloudButtonClicked" })
1651+
}
1652+
1653+
this.stateUpdateDebounceTimer = null
1654+
}, 50) // 50ms debounce delay
16131655
}
16141656

16151657
/**
@@ -2595,6 +2637,9 @@ export class ClineProvider
25952637
// Capture the current instance to detect if rehydrate already occurred elsewhere
25962638
const originalInstanceId = task.instanceId
25972639

2640+
// Set soft reload flag to prevent UI flickering
2641+
this.isSoftReloading = true
2642+
25982643
// Begin abort (non-blocking)
25992644
task.abortTask()
26002645

@@ -2623,6 +2668,8 @@ export class ClineProvider
26232668
this.log(
26242669
`[cancelTask] Skipping rehydrate: current instance ${current.instanceId} != original ${originalInstanceId}`,
26252670
)
2671+
// Reset soft reload flag
2672+
this.isSoftReloading = false
26262673
return
26272674
}
26282675

@@ -2633,12 +2680,20 @@ export class ClineProvider
26332680
this.log(
26342681
`[cancelTask] Skipping rehydrate after final check: current instance ${currentAfterCheck.instanceId} != original ${originalInstanceId}`,
26352682
)
2683+
// Reset soft reload flag
2684+
this.isSoftReloading = false
26362685
return
26372686
}
26382687
}
26392688

26402689
// Clears task again, so we need to abortTask manually above.
26412690
await this.createTaskWithHistoryItem({ ...historyItem, rootTask, parentTask })
2691+
2692+
// Reset soft reload flag after task is recreated
2693+
this.isSoftReloading = false
2694+
2695+
// Send a refresh without flickering
2696+
await this.postStateToWebview()
26422697
}
26432698

26442699
// Clear the current task without treating it as a subtask.

src/core/webview/checkpointRestoreHandler.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ export async function handleCheckpointRestoreOperation(config: CheckpointRestore
2727
const { provider, currentCline, messageTs, checkpoint, operation, editData } = config
2828

2929
try {
30+
// Set soft reload flag to prevent UI flickering
31+
;(provider as any).isSoftReloading = true
32+
3033
// For delete operations, ensure the task is properly aborted to handle any pending ask operations
3134
// This prevents "Current ask promise was ignored" errors
3235
// For edit operations, we don't abort because the checkpoint restore will handle it
@@ -78,7 +81,16 @@ export async function handleCheckpointRestoreOperation(config: CheckpointRestore
7881
}
7982
// For edit operations, the task cancellation in checkpointRestore
8083
// will trigger reinitialization, which will process pendingEditAfterRestore
84+
85+
// Reset soft reload flag after operation completes
86+
;(provider as any).isSoftReloading = false
87+
88+
// Send a refresh without flickering
89+
await provider.postStateToWebview()
8190
} catch (error) {
91+
// Reset soft reload flag on error
92+
;(provider as any).isSoftReloading = false
93+
8294
console.error(`Error in checkpoint restore (${operation}):`, error)
8395
vscode.window.showErrorMessage(
8496
`Error during checkpoint restore: ${error instanceof Error ? error.message : String(error)}`,

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ export interface ExtensionMessage {
205205
queuedMessages?: QueuedMessage[]
206206
list?: string[] // For dismissedUpsells
207207
organizationId?: string | null // For organizationSwitchResult
208+
isSoftReload?: boolean // Flag to indicate soft reload state (cancel/checkpoint restore) to prevent UI flickering
208209
}
209210

210211
export type ExtensionState = Pick<

webview-ui/src/components/chat/ChatView.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -728,7 +728,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
728728
const trimmedInput = text?.trim()
729729

730730
if (isStreaming) {
731-
vscode.postMessage({ type: "cancelTask" })
731+
// Set a flag to indicate soft reload for cancel operation
732+
vscode.postMessage({ type: "cancelTask", isSoftReload: true })
732733
setDidClickCancel(true)
733734
return
734735
}
@@ -780,6 +781,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
780781
(e: MessageEvent) => {
781782
const message: ExtensionMessage = e.data
782783

784+
// Check for soft reload flag to prevent UI flickering
785+
if (message.isSoftReload === true) {
786+
// During soft reload, we preserve UI state and skip certain operations
787+
// that would cause flickering
788+
return
789+
}
790+
783791
switch (message.type) {
784792
case "action":
785793
switch (message.action!) {

0 commit comments

Comments
 (0)