feat(cli): warn users that rewind is disabled in IDE mode (#4122)
Some checks are pending
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run

This commit is contained in:
jinye 2026-05-15 20:27:37 +08:00 committed by GitHub
parent df32345d05
commit 435f711e33
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 94 additions and 3 deletions

View file

@ -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(
<AppContainer
config={mockConfig}
settings={mockSettings}
version="1.0.0"
initializationResult={mockInitResult}
/>,
);
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(
<AppContainer
config={mockConfig}
settings={mockSettings}
version="1.0.0"
initializationResult={mockInitResult}
/>,
);
capturedUIActions.openRewindSelector();
expect(mockAddItemDisabled).not.toHaveBeenCalled();
});
});
});
describe('dedupeNewestFirst', () => {

View file

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