diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index dd1e53a64..5fa4baa03 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -690,11 +690,15 @@ export const AppContainer = (props: AppContainerProps) => { embeddedShellFocused, ); + // Track whether suggestions are visible for Tab key handling + const [hasSuggestionsVisible, setHasSuggestionsVisible] = useState(false); + // Auto-accept indicator const showAutoAcceptIndicator = useAutoAcceptIndicator({ config, addItem: historyManager.addItem, onApprovalModeChange: handleApprovalModeChange, + shouldBlockTab: () => hasSuggestionsVisible, }); const { messageQueue, addMessage, clearQueue, getQueuedMessagesText } = @@ -1516,6 +1520,7 @@ export const AppContainer = (props: AppContainerProps) => { handleFolderTrustSelect, setConstrainHeight, onEscapePromptChange: handleEscapePromptChange, + onSuggestionsVisibilityChange: setHasSuggestionsVisible, refreshStatic, handleFinalSubmit, handleClearScreen, diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx index d0f5353f1..73983c812 100644 --- a/packages/cli/src/ui/components/Composer.tsx +++ b/packages/cli/src/ui/components/Composer.tsx @@ -37,9 +37,14 @@ export const Composer = () => { // State for suggestions visibility const [showSuggestions, setShowSuggestions] = useState(false); - const handleSuggestionsVisibilityChange = useCallback((visible: boolean) => { - setShowSuggestions(visible); - }, []); + const handleSuggestionsVisibilityChange = useCallback( + (visible: boolean) => { + setShowSuggestions(visible); + // Also notify AppContainer for Tab key handling + uiActions.onSuggestionsVisibilityChange(visible); + }, + [uiActions], + ); return ( diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index e0c08808c..374ec61cf 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -188,6 +188,22 @@ export const InputPrompt: React.FC = ({ } }, [showEscapePrompt, onEscapePromptChange]); + // Notify parent component about suggestions visibility changes + useEffect(() => { + if (onSuggestionsVisibilityChange) { + const hasSuggestions = + completion.showSuggestions || + reverseSearchCompletion.showSuggestions || + commandSearchCompletion.showSuggestions; + onSuggestionsVisibilityChange(hasSuggestions); + } + }, [ + completion.showSuggestions, + reverseSearchCompletion.showSuggestions, + commandSearchCompletion.showSuggestions, + onSuggestionsVisibilityChange, + ]); + // Clear escape prompt timer on unmount useEffect( () => () => { diff --git a/packages/cli/src/ui/contexts/UIActionsContext.tsx b/packages/cli/src/ui/contexts/UIActionsContext.tsx index 17d74dd4e..000740bed 100644 --- a/packages/cli/src/ui/contexts/UIActionsContext.tsx +++ b/packages/cli/src/ui/contexts/UIActionsContext.tsx @@ -53,6 +53,7 @@ export interface UIActions { handleFolderTrustSelect: (choice: FolderTrustChoice) => void; setConstrainHeight: (value: boolean) => void; onEscapePromptChange: (show: boolean) => void; + onSuggestionsVisibilityChange: (visible: boolean) => void; refreshStatic: () => void; handleFinalSubmit: (value: string) => void; handleClearScreen: () => void; diff --git a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts index 430fc4c3c..19dfd0531 100644 --- a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts +++ b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts @@ -482,4 +482,80 @@ describe('useAutoAcceptIndicator', () => { ApprovalMode.YOLO, ); }); + + it('should not cycle approval mode on Windows when shouldBlockTab returns true', () => { + const originalPlatform = process.platform; + Object.defineProperty(process, 'platform', { + value: 'win32', + }); + + mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); + const mockShouldBlockTab = vi.fn(() => true); + + renderHook(() => + useAutoAcceptIndicator({ + config: mockConfigInstance as unknown as ActualConfigType, + addItem: vi.fn(), + shouldBlockTab: mockShouldBlockTab, + }), + ); + + // Simulate Tab key press on Windows + act(() => { + capturedUseKeypressHandler({ + name: 'tab', + shift: false, + ctrl: false, + meta: false, + } as Key); + }); + + // Should call shouldBlockTab to check if autocomplete is active + expect(mockShouldBlockTab).toHaveBeenCalled(); + // Should NOT cycle approval mode when shouldBlockTab returns true + expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled(); + + Object.defineProperty(process, 'platform', { + value: originalPlatform, + }); + }); + + it('should cycle approval mode on Windows when shouldBlockTab returns false', () => { + const originalPlatform = process.platform; + Object.defineProperty(process, 'platform', { + value: 'win32', + }); + + mockConfigInstance.getApprovalMode.mockReturnValue(ApprovalMode.DEFAULT); + const mockShouldBlockTab = vi.fn(() => false); + + renderHook(() => + useAutoAcceptIndicator({ + config: mockConfigInstance as unknown as ActualConfigType, + addItem: vi.fn(), + shouldBlockTab: mockShouldBlockTab, + }), + ); + + // Simulate Tab key press on Windows + act(() => { + capturedUseKeypressHandler({ + name: 'tab', + shift: false, + ctrl: false, + meta: false, + } as Key); + }); + + // Should call shouldBlockTab to check if autocomplete is active + expect(mockShouldBlockTab).toHaveBeenCalled(); + // Should cycle approval mode when shouldBlockTab returns false + expect(mockConfigInstance.setApprovalMode).toHaveBeenCalledWith( + ApprovalMode.AUTO_EDIT, + ); + + Object.defineProperty(process, 'platform', { + value: originalPlatform, + }); + }); }); diff --git a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts index e3908608c..3135a362b 100644 --- a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts +++ b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts @@ -18,12 +18,14 @@ export interface UseAutoAcceptIndicatorArgs { config: Config; addItem?: (item: HistoryItemWithoutId, timestamp: number) => void; onApprovalModeChange?: (mode: ApprovalMode) => void; + shouldBlockTab?: () => boolean; } export function useAutoAcceptIndicator({ config, addItem, onApprovalModeChange, + shouldBlockTab, }: UseAutoAcceptIndicatorArgs): ApprovalMode { const currentConfigValue = config.getApprovalMode(); const [showAutoAcceptIndicator, setShowAutoAcceptIndicator] = @@ -46,6 +48,12 @@ export function useAutoAcceptIndicator({ !key.meta; if (isShiftTab || isWindowsTab) { + // On Windows, check if we should block Tab key when autocomplete is active + if (isWindowsTab && shouldBlockTab?.()) { + // Don't cycle approval mode when autocomplete is showing + return; + } + const currentMode = config.getApprovalMode(); const currentIndex = APPROVAL_MODES.indexOf(currentMode); const nextIndex =