diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx index ca2c44979..5003edb8b 100644 --- a/packages/cli/src/ui/AppContainer.test.tsx +++ b/packages/cli/src/ui/AppContainer.test.tsx @@ -3077,6 +3077,85 @@ describe('AppContainer State Management', () => { expect(trigger.activeCount()).toBe(0); }); }); + + describe('IDE mode rewind guard', () => { + it('shows info message instead of opening rewind selector when IDE mode is enabled', () => { + const mockAddItem = vi.fn(); + mockedUseHistory.mockReturnValue({ + history: [{ id: 1, type: 'user', text: 'hello' }], + addItem: mockAddItem, + updateItem: vi.fn(), + clearItems: vi.fn(), + loadHistory: vi.fn(), + truncateToItem: vi.fn(), + }); + mockedUseGeminiStream.mockReturnValue({ + streamingState: 'idle', + submitQuery: vi.fn(), + initError: null, + pendingHistoryItems: [], + thought: null, + cancelOngoingRequest: vi.fn(), + retryLastPrompt: vi.fn(), + }); + vi.spyOn(mockConfig, 'getIdeMode').mockReturnValue(true); + + render( + , + ); + + capturedUIActions.openRewindSelector(); + + expect(mockAddItem).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'info', + text: expect.stringMatching(/rewind.*disabled.*IDE/i), + }), + expect.any(Number), + ); + expect(capturedUIState.isRewindSelectorOpen).toBeFalsy(); + }); + + it('opens rewind selector normally when IDE mode is disabled', () => { + const mockAddItemDisabled = vi.fn(); + mockedUseHistory.mockReturnValue({ + history: [{ id: 1, type: 'user', text: 'hello' }], + addItem: mockAddItemDisabled, + updateItem: vi.fn(), + clearItems: vi.fn(), + loadHistory: vi.fn(), + truncateToItem: vi.fn(), + }); + mockedUseGeminiStream.mockReturnValue({ + streamingState: 'idle', + submitQuery: vi.fn(), + initError: null, + pendingHistoryItems: [], + thought: null, + cancelOngoingRequest: vi.fn(), + retryLastPrompt: vi.fn(), + }); + vi.spyOn(mockConfig, 'getIdeMode').mockReturnValue(false); + + render( + , + ); + + capturedUIActions.openRewindSelector(); + + expect(mockAddItemDisabled).not.toHaveBeenCalled(); + }); + }); }); describe('dedupeNewestFirst', () => { diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 4e8a39120..0e87599d7 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -2160,14 +2160,25 @@ export const AppContainer = (props: AppContainerProps) => { }, []); // --- Rewind selector callbacks --- + // IDE guard here is NOT redundant with the keyboard handler guard (line ~2375): + // /rewind calls openRewindSelector directly, bypassing the keyboard handler. const openRewindSelector = useCallback(() => { if (streamingState !== StreamingState.Idle) return; - if (config.getIdeMode()) return; if (dialogsVisibleRef.current) return; + if (config.getIdeMode()) { + historyManager.addItem( + { + type: 'info', + text: 'Rewind is disabled in IDE mode.', + }, + Date.now(), + ); + return; + } const hasUserTurns = historyManager.history.some((h) => h.type === 'user'); if (!hasUserTurns) return; setIsRewindSelectorOpen(true); - }, [streamingState, config, historyManager.history]); + }, [streamingState, config, historyManager]); openRewindSelectorRef.current = openRewindSelector; const closeRewindSelector = useCallback(() => { @@ -2559,7 +2570,8 @@ export const AppContainer = (props: AppContainerProps) => { // Input is empty and idle — double-ESC opens rewind selector if ( streamingState === StreamingState.Idle && - !dialogsVisibleRef.current + !dialogsVisibleRef.current && + !config.getIdeMode() ) { if (escapeTimerRef.current) { clearTimeout(escapeTimerRef.current);