diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 274060a19b..cebd97c581 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -112,6 +112,7 @@ export class ClineProvider protected mcpHub?: McpHub // Change from private to protected private marketplaceManager: MarketplaceManager private mdmService?: MdmService + private isModeSwitching = false public isViewLaunched = false public settingsImportedAt?: number @@ -956,6 +957,23 @@ export class ClineProvider * @param newMode The mode to switch to */ public async handleModeSwitch(newMode: Mode) { + // Prevent concurrent mode switches + if (this.isModeSwitching) { + this.log(`Mode switch already in progress, ignoring switch to ${newMode}`) + return + } + + this.isModeSwitching = true + + try { + await this.performModeSwitch(newMode) + } finally { + // Always reset the flag, even if an error occurs + this.isModeSwitching = false + } + } + + private async performModeSwitch(newMode: Mode) { const cline = this.getCurrentCline() if (cline) { diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index e73ac67701..cc374ae7c1 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -179,6 +179,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction(false) const [isCondensing, setIsCondensing] = useState(false) const [showAnnouncementModal, setShowAnnouncementModal] = useState(false) + const [isModeSwitching, setIsModeSwitching] = useState(false) const everVisibleMessagesTsRef = useRef>( new LRUCache({ max: 100, @@ -1456,6 +1457,14 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + // Prevent concurrent mode switches + if (isModeSwitching) { + return + } + + // Set flag to prevent concurrent switches + setIsModeSwitching(true) + // Update local state and notify extension to sync mode change setMode(modeSlug) @@ -1464,8 +1473,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + setIsModeSwitching(false) + }, 300) }, - [setMode], + [setMode, isModeSwitching], ) const handleSuggestionClickInRow = useCallback( @@ -1714,23 +1728,31 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - const allModes = getAllModes(customModes) - const currentModeIndex = allModes.findIndex((m) => m.slug === mode) - const nextModeIndex = (currentModeIndex + 1) % allModes.length - // Update local state and notify extension to sync mode change - switchToMode(allModes[nextModeIndex].slug) - }, [mode, customModes, switchToMode]) + // Function to handle mode switching with debouncing + const switchToNextMode = useMemo( + () => + debounce(() => { + const allModes = getAllModes(customModes) + const currentModeIndex = allModes.findIndex((m) => m.slug === mode) + const nextModeIndex = (currentModeIndex + 1) % allModes.length + // Update local state and notify extension to sync mode change + switchToMode(allModes[nextModeIndex].slug) + }, 150), + [mode, customModes, switchToMode], + ) - // Function to handle switching to previous mode - const switchToPreviousMode = useCallback(() => { - const allModes = getAllModes(customModes) - const currentModeIndex = allModes.findIndex((m) => m.slug === mode) - const previousModeIndex = (currentModeIndex - 1 + allModes.length) % allModes.length - // Update local state and notify extension to sync mode change - switchToMode(allModes[previousModeIndex].slug) - }, [mode, customModes, switchToMode]) + // Function to handle switching to previous mode with debouncing + const switchToPreviousMode = useMemo( + () => + debounce(() => { + const allModes = getAllModes(customModes) + const currentModeIndex = allModes.findIndex((m) => m.slug === mode) + const previousModeIndex = (currentModeIndex - 1 + allModes.length) % allModes.length + // Update local state and notify extension to sync mode change + switchToMode(allModes[previousModeIndex].slug) + }, 150), + [mode, customModes, switchToMode], + ) // Add keyboard event handler const handleKeyDown = useCallback( @@ -1752,13 +1774,20 @@ const ChatViewComponent: React.ForwardRefRenderFunction { window.addEventListener("keydown", handleKeyDown) return () => { window.removeEventListener("keydown", handleKeyDown) + // Cancel any pending debounced calls when unmounting + if (typeof (switchToNextMode as any).cancel === "function") { + ;(switchToNextMode as any).cancel() + } + if (typeof (switchToPreviousMode as any).cancel === "function") { + ;(switchToPreviousMode as any).cancel() + } } - }, [handleKeyDown]) + }, [handleKeyDown, switchToNextMode, switchToPreviousMode]) useImperativeHandle(ref, () => ({ acceptInput: () => { diff --git a/webview-ui/src/components/chat/__tests__/ChatView.keyboard-fix.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatView.keyboard-fix.spec.tsx index 4b23166bce..f91603d2a8 100644 --- a/webview-ui/src/components/chat/__tests__/ChatView.keyboard-fix.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatView.keyboard-fix.spec.tsx @@ -162,8 +162,8 @@ describe("ChatView - Keyboard Shortcut Fix for Dvorak", () => { shiftKey: false, }) - // Wait for event to be processed - await new Promise((resolve) => setTimeout(resolve, 50)) + // Wait for event to be processed and debounce delay (150ms) + await new Promise((resolve) => setTimeout(resolve, 200)) // Check if mode switch was triggered const callsAfterPeriod = (vscode.postMessage as any).mock.calls @@ -183,7 +183,7 @@ describe("ChatView - Keyboard Shortcut Fix for Dvorak", () => { }) // Wait for event to be processed - await new Promise((resolve) => setTimeout(resolve, 50)) + await new Promise((resolve) => setTimeout(resolve, 200)) // Check that NO mode switch was triggered const callsAfterV = (vscode.postMessage as any).mock.calls @@ -244,8 +244,8 @@ describe("ChatView - Keyboard Shortcut Fix for Dvorak", () => { shiftKey: false, }) - // Wait for event to be processed - await new Promise((resolve) => setTimeout(resolve, 50)) + // Wait for event to be processed and debounce delay (150ms) + await new Promise((resolve) => setTimeout(resolve, 200)) // Check if mode switch was triggered const calls = (vscode.postMessage as any).mock.calls @@ -277,8 +277,8 @@ describe("ChatView - Keyboard Shortcut Fix for Dvorak", () => { shiftKey: true, // Should go to previous mode }) - // Wait for event to be processed - await new Promise((resolve) => setTimeout(resolve, 50)) + // Wait for event to be processed and debounce delay (150ms) + await new Promise((resolve) => setTimeout(resolve, 200)) // Check if mode switch was triggered const calls = (vscode.postMessage as any).mock.calls