diff --git a/packages/cli/src/config/footerItems.test.ts b/packages/cli/src/config/footerItems.test.ts index d9ef9bc3f2..9e32dcb175 100644 --- a/packages/cli/src/config/footerItems.test.ts +++ b/packages/cli/src/config/footerItems.test.ts @@ -153,5 +153,49 @@ describe('footerItems', () => { expect(state.orderedIds).toContain('auth'); expect(state.selectedIds.has('auth')).toBe(true); }); + + it('includes context-used in selectedIds when hideContextPercentage is false and items is undefined', () => { + const settings = createMockSettings({ + ui: { + footer: { + hideContextPercentage: false, + }, + }, + }).merged; + + const state = resolveFooterState(settings); + expect(state.selectedIds.has('context-used')).toBe(true); + expect(state.orderedIds).toContain('context-used'); + }); + + it('does not include context-used in selectedIds when hideContextPercentage is true (default)', () => { + const settings = createMockSettings({ + ui: { + footer: { + hideContextPercentage: true, + }, + }, + }).merged; + + const state = resolveFooterState(settings); + expect(state.selectedIds.has('context-used')).toBe(false); + // context-used should still be in orderedIds (as unselected) + expect(state.orderedIds).toContain('context-used'); + }); + + it('persisted items array takes precedence over hideContextPercentage', () => { + const settings = createMockSettings({ + ui: { + footer: { + items: ['workspace', 'model-name'], + hideContextPercentage: false, + }, + }, + }).merged; + + const state = resolveFooterState(settings); + // items array explicitly omits context-used, so it should not be selected + expect(state.selectedIds.has('context-used')).toBe(false); + }); }); }); diff --git a/packages/cli/src/gemini.test.tsx b/packages/cli/src/gemini.test.tsx index 611850bd4a..7712d39bb2 100644 --- a/packages/cli/src/gemini.test.tsx +++ b/packages/cli/src/gemini.test.tsx @@ -304,6 +304,25 @@ describe('gemini.tsx main function', () => { vi.restoreAllMocks(); }); + it('should suppress AbortError and not open debug console', async () => { + const debugLoggerErrorSpy = vi.spyOn(debugLogger, 'error'); + const debugLoggerLogSpy = vi.spyOn(debugLogger, 'log'); + const abortError = new DOMException( + 'The operation was aborted.', + 'AbortError', + ); + + setupUnhandledRejectionHandler(); + process.emit('unhandledRejection', abortError, Promise.resolve()); + + await new Promise(process.nextTick); + + expect(debugLoggerErrorSpy).not.toHaveBeenCalled(); + expect(debugLoggerLogSpy).toHaveBeenCalledWith( + expect.stringContaining('Suppressed unhandled AbortError'), + ); + }); + it('should log unhandled promise rejections and open debug console on first error', async () => { const processExitSpy = vi .spyOn(process, 'exit') diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 166ee0e7eb..eedfcc950a 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -164,6 +164,14 @@ export function getNodeMemoryArgs(isDebugMode: boolean): string[] { export function setupUnhandledRejectionHandler() { let unhandledRejectionOccurred = false; process.on('unhandledRejection', (reason, _promise) => { + // AbortError is expected when the user cancels a request (e.g. pressing ESC). + // It may surface as an unhandled rejection due to async timing in the + // streaming pipeline, but it is not a bug. + if (reason instanceof Error && reason.name === 'AbortError') { + debugLogger.log(`Suppressed unhandled AbortError: ${reason.message}`); + return; + } + const errorMessage = `========================================= This is an unexpected error. Please file a bug report using the /bug tool. CRITICAL: Unhandled Promise Rejection! diff --git a/packages/cli/src/ui/components/FooterConfigDialog.tsx b/packages/cli/src/ui/components/FooterConfigDialog.tsx index 3291e6bccf..0c1f9ce320 100644 --- a/packages/cli/src/ui/components/FooterConfigDialog.tsx +++ b/packages/cli/src/ui/components/FooterConfigDialog.tsx @@ -13,7 +13,11 @@ import { useUIState } from '../contexts/UIStateContext.js'; import { useKeypress, type Key } from '../hooks/useKeypress.js'; import { Command } from '../key/keyMatchers.js'; import { FooterRow, type FooterRowItem } from './Footer.js'; -import { ALL_ITEMS, resolveFooterState } from '../../config/footerItems.js'; +import { + ALL_ITEMS, + resolveFooterState, + deriveItemsFromLegacySettings, +} from '../../config/footerItems.js'; import { SettingScope } from '../../config/settings.js'; import { BaseSelectionList } from './shared/BaseSelectionList.js'; import type { SelectionListItem } from '../hooks/useSelectionList.js'; @@ -137,17 +141,16 @@ export const FooterConfigDialog: React.FC = ({ const handleSaveAndClose = useCallback(() => { const finalItems = orderedIds.filter((id: string) => selectedIds.has(id)); const currentSetting = settings.merged.ui?.footer?.items; - if (JSON.stringify(finalItems) !== JSON.stringify(currentSetting)) { + // When items haven't been explicitly set yet (legacy mode), compare against + // the legacy-derived items to avoid persisting items and silently overriding + // legacy boolean settings like hideContextPercentage. + const effectiveCurrent = + currentSetting ?? deriveItemsFromLegacySettings(settings.merged); + if (JSON.stringify(finalItems) !== JSON.stringify(effectiveCurrent)) { setSetting(SettingScope.User, 'ui.footer.items', finalItems); } onClose?.(); - }, [ - orderedIds, - selectedIds, - setSetting, - settings.merged.ui?.footer?.items, - onClose, - ]); + }, [orderedIds, selectedIds, setSetting, settings.merged, onClose]); const handleResetToDefaults = useCallback(() => { setSetting(SettingScope.User, 'ui.footer.items', undefined);