diff --git a/packages/cli/src/config/footerItems.ts b/packages/cli/src/config/footerItems.ts index 7990b82f65..1503caca10 100644 --- a/packages/cli/src/config/footerItems.ts +++ b/packages/cli/src/config/footerItems.ts @@ -47,7 +47,7 @@ export const ALL_ITEMS: FooterItem[] = [ { id: 'quota', label: 'quota', - description: 'Remaining quota and reset time', + description: 'Remaining usage on daily limit', defaultEnabled: true, }, { diff --git a/packages/cli/src/ui/components/Footer.tsx b/packages/cli/src/ui/components/Footer.tsx index 4d614b8ba3..3752b40e59 100644 --- a/packages/cli/src/ui/components/Footer.tsx +++ b/packages/cli/src/ui/components/Footer.tsx @@ -17,6 +17,11 @@ import process from 'node:process'; import { MemoryUsageDisplay } from './MemoryUsageDisplay.js'; import { ContextUsageDisplay } from './ContextUsageDisplay.js'; import { QuotaDisplay } from './QuotaDisplay.js'; +import { + getStatusColor, + QUOTA_THRESHOLD_HIGH, + QUOTA_THRESHOLD_MEDIUM, +} from '../utils/displayUtils.js'; import { DebugProfiler } from './DebugProfiler.js'; import { isDevelopment } from '../../utils/installationInfo.js'; import { useUIState } from '../contexts/UIStateContext.js'; @@ -55,10 +60,16 @@ const CwdIndicator: React.FC = ({ interface BranchIndicatorProps { branchName: string; + showParentheses?: boolean; } -const BranchIndicator: React.FC = ({ branchName }) => ( - ({branchName}*) +const BranchIndicator: React.FC = ({ + branchName, + showParentheses = true, +}) => ( + + {showParentheses ? `(${branchName}*)` : `${branchName}*`} + ); interface SandboxIndicatorProps { @@ -322,7 +333,10 @@ export const Footer: React.FC = () => { } case 'git-branch': { if (branchName) { - addElement(id, ); + addElement( + id, + , + ); } break; } @@ -355,16 +369,21 @@ export const Footer: React.FC = () => { break; } case 'quota': { - if (quotaStats) { - addElement( - id, - , - ); + if ( + quotaStats && + quotaStats.remaining !== undefined && + quotaStats.limit + ) { + const percentage = (quotaStats.remaining / quotaStats.limit) * 100; + const color = getStatusColor(percentage, { + green: QUOTA_THRESHOLD_HIGH, + yellow: QUOTA_THRESHOLD_MEDIUM, + }); + const text = + quotaStats.remaining === 0 + ? 'limit reached' + : `daily ${percentage.toFixed(0)}%`; + addElement(id, {text}); } break; } diff --git a/packages/cli/src/ui/components/FooterConfigDialog.test.tsx b/packages/cli/src/ui/components/FooterConfigDialog.test.tsx index a696d49b07..d82817689d 100644 --- a/packages/cli/src/ui/components/FooterConfigDialog.test.tsx +++ b/packages/cli/src/ui/components/FooterConfigDialog.test.tsx @@ -133,7 +133,7 @@ describe('', () => { // Initial state: 'cwd' is active. // Verify 'cwd' content exists in the preview area - expect(lastFrame()).toContain('~/dev/gemini-cli'); + expect(lastFrame()).toContain('~/project/path'); // Move focus down to 'git-branch' act(() => { @@ -146,4 +146,34 @@ describe('', () => { expect(output).toContain('main*'); }); }); + + it('shows an empty preview when all items are deselected', async () => { + const settings = createMockSettings(); + const { lastFrame, stdin } = renderWithProviders( + , + { settings }, + ); + + // Deselect all items (assuming we know which ones are selected by default) + // By default: cwd, git-branch, sandbox-status, model-name, quota are selected. + // They are at indices 0, 1, 2, 3, 4. + for (let i = 0; i < 5; i++) { + act(() => { + stdin.write('\r'); // Toggle (deselect) + stdin.write('\u001b[B'); // Down arrow + }); + } + + await waitFor(() => { + const output = lastFrame(); + expect(output).toBeDefined(); + expect(output).toContain('Preview:'); + // The preview area should not contain any of the mock values + expect(output).not.toContain('~/project/path'); + expect(output).not.toContain('main*'); + expect(output).not.toContain('docker'); + expect(output).not.toContain('gemini-2.5-pro'); + expect(output).not.toContain('1.2k left'); + }); + }); }); diff --git a/packages/cli/src/ui/components/FooterConfigDialog.tsx b/packages/cli/src/ui/components/FooterConfigDialog.tsx index ecbdd85ea9..88fc57dba8 100644 --- a/packages/cli/src/ui/components/FooterConfigDialog.tsx +++ b/packages/cli/src/ui/components/FooterConfigDialog.tsx @@ -96,7 +96,31 @@ export const FooterConfigDialog: React.FC = ({ setScrollOffset(0); }, [searchQuery]); + // The reset action lives one index past the filtered item list + const isResetFocused = activeIndex === filteredItems.length; + + const handleResetToDefaults = useCallback(() => { + // Clear the custom items setting so the legacy footer path is used + setSetting(SettingScope.User, 'ui.footer.items', undefined); + + // Reset local state to reflect legacy-derived items + const validIds = new Set(ALL_ITEMS.map((i) => i.id)); + const derived = deriveItemsFromLegacySettings(settings.merged).filter( + (id) => validIds.has(id), + ); + const others = DEFAULT_ORDER.filter((id) => !derived.includes(id)); + setOrderedIds([...derived, ...others]); + setSelectedIds(new Set(derived)); + setActiveIndex(0); + setScrollOffset(0); + }, [setSetting, settings.merged]); + const handleConfirm = useCallback(async () => { + if (isResetFocused) { + handleResetToDefaults(); + return; + } + const item = filteredItems[activeIndex]; if (!item) return; @@ -111,7 +135,15 @@ export const FooterConfigDialog: React.FC = ({ // Save immediately on toggle const finalItems = orderedIds.filter((id) => next.has(id)); setSetting(SettingScope.User, 'ui.footer.items', finalItems); - }, [filteredItems, activeIndex, orderedIds, setSetting, selectedIds]); + }, [ + filteredItems, + activeIndex, + orderedIds, + setSetting, + selectedIds, + isResetFocused, + handleResetToDefaults, + ]); const handleReorder = useCallback( (direction: number) => { @@ -165,24 +197,31 @@ export const FooterConfigDialog: React.FC = ({ } if (keyMatchers[Command.DIALOG_NAVIGATION_UP](key)) { - const newIndex = - activeIndex > 0 ? activeIndex - 1 : filteredItems.length - 1; + // Navigation wraps: items 0..filteredItems.length-1, then reset row at filteredItems.length + const totalSlots = filteredItems.length + 1; + const newIndex = activeIndex > 0 ? activeIndex - 1 : totalSlots - 1; setActiveIndex(newIndex); - if (newIndex === filteredItems.length - 1) { - setScrollOffset(Math.max(0, filteredItems.length - maxItemsToShow)); - } else if (newIndex < scrollOffset) { - setScrollOffset(newIndex); + // Only adjust scroll when within the item list + if (newIndex < filteredItems.length) { + if (newIndex === filteredItems.length - 1) { + setScrollOffset(Math.max(0, filteredItems.length - maxItemsToShow)); + } else if (newIndex < scrollOffset) { + setScrollOffset(newIndex); + } } return true; } if (keyMatchers[Command.DIALOG_NAVIGATION_DOWN](key)) { - const newIndex = - activeIndex < filteredItems.length - 1 ? activeIndex + 1 : 0; + const totalSlots = filteredItems.length + 1; + const newIndex = activeIndex < totalSlots - 1 ? activeIndex + 1 : 0; setActiveIndex(newIndex); if (newIndex === 0) { setScrollOffset(0); - } else if (newIndex >= scrollOffset + maxItemsToShow) { + } else if ( + newIndex < filteredItems.length && + newIndex >= scrollOffset + maxItemsToShow + ) { setScrollOffset(newIndex - maxItemsToShow + 1); } return true; @@ -217,15 +256,23 @@ export const FooterConfigDialog: React.FC = ({ // Preview logic const previewText = useMemo(() => { + if (isResetFocused) { + return ( + + Default footer (uses legacy settings) + + ); + } + const itemsToPreview = orderedIds.filter((id) => selectedIds.has(id)); - if (itemsToPreview.length === 0) return 'Empty Footer'; + if (itemsToPreview.length === 0) return null; const getColor = (id: string, defaultColor?: string) => id === activeId ? 'white' : defaultColor || theme.text.secondary; // Mock values for preview const mockValues: Record = { - cwd: ~/dev/gemini-cli, + cwd: ~/project/path, 'git-branch': main*, 'sandbox-status': ( docker @@ -236,9 +283,9 @@ export const FooterConfigDialog: React.FC = ({ ), 'context-remaining': ( - 85% + 85% context left ), - quota: 1.2k left, + quota: daily 97%, 'memory-usage': 124MB, 'session-id': 769992f9, 'code-changes': ( @@ -266,7 +313,7 @@ export const FooterConfigDialog: React.FC = ({ }); return elements; - }, [orderedIds, selectedIds, activeId]); + }, [orderedIds, selectedIds, activeId, isResetFocused]); return ( = ({ )} + + + {isResetFocused ? '> ' : ' '} + Reset to default footer + + + ↑/↓ navigate · ←/→ reorder · enter select · esc close