/** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { useCallback, useMemo, useState, useEffect } from 'react'; import { Box, Text } from 'ink'; import { theme } from '../semantic-colors.js'; import { useSettingsStore } from '../contexts/SettingsContext.js'; import { useKeypress, type Key } from '../hooks/useKeypress.js'; import { keyMatchers, Command } from '../keyMatchers.js'; import { TextInput } from './shared/TextInput.js'; import { useFuzzyList } from '../hooks/useFuzzyList.js'; import { ALL_ITEMS, DEFAULT_ORDER, deriveItemsFromLegacySettings, } from '../../config/footerItems.js'; import { SettingScope } from '../../config/settings.js'; interface FooterConfigDialogProps { onClose?: () => void; } export const FooterConfigDialog: React.FC = ({ onClose, }) => { const { settings, setSetting } = useSettingsStore(); // Initialize orderedIds and selectedIds const [orderedIds, setOrderedIds] = useState(() => { if (settings.merged.ui?.footer?.items) { // Start with saved items in their saved order const savedItems = settings.merged.ui.footer.items; // Then add any items from DEFAULT_ORDER that aren't in savedItems const others = DEFAULT_ORDER.filter((id) => !savedItems.includes(id)); return [...savedItems, ...others]; } // Fallback to legacy settings derivation const derived = deriveItemsFromLegacySettings(settings.merged); const others = DEFAULT_ORDER.filter((id) => !derived.includes(id)); return [...derived, ...others]; }); const [selectedIds, setSelectedIds] = useState>(() => { if (settings.merged.ui?.footer?.items) { return new Set(settings.merged.ui.footer.items); } return new Set(deriveItemsFromLegacySettings(settings.merged)); }); // Prepare items for fuzzy list const listItems = useMemo( () => orderedIds.map((id) => { const item = ALL_ITEMS.find((i) => i.id === id)!; return { key: id, label: item.id, description: item.description, }; }), [orderedIds], ); const { filteredItems, searchBuffer, searchQuery, maxLabelWidth } = useFuzzyList({ items: listItems, }); const [activeIndex, setActiveIndex] = useState(0); const [scrollOffset, setScrollOffset] = useState(0); const maxItemsToShow = 10; // Reset index when search changes useEffect(() => { setActiveIndex(0); setScrollOffset(0); }, [searchQuery]); const handleConfirm = useCallback(async () => { const item = filteredItems[activeIndex]; if (!item) return; const next = new Set(selectedIds); if (next.has(item.key)) { next.delete(item.key); } else { next.add(item.key); } setSelectedIds(next); // Save immediately on toggle const finalItems = orderedIds.filter((id) => next.has(id)); setSetting(SettingScope.User, 'ui.footer.items', finalItems); }, [filteredItems, activeIndex, orderedIds, setSetting, selectedIds]); const handleReorder = useCallback( (direction: number) => { if (searchQuery) return; // Reorder disabled when searching const currentItem = filteredItems[activeIndex]; if (!currentItem) return; const currentId = currentItem.key; const currentIndex = orderedIds.indexOf(currentId); const newIndex = currentIndex + direction; if (newIndex < 0 || newIndex >= orderedIds.length) return; const newOrderedIds = [...orderedIds]; [newOrderedIds[currentIndex], newOrderedIds[newIndex]] = [ newOrderedIds[newIndex], newOrderedIds[currentIndex], ]; setOrderedIds(newOrderedIds); setActiveIndex(newIndex); // Save immediately on reorder const finalItems = newOrderedIds.filter((id) => selectedIds.has(id)); setSetting(SettingScope.User, 'ui.footer.items', finalItems); // Adjust scroll offset if needed if (newIndex < scrollOffset) { setScrollOffset(newIndex); } else if (newIndex >= scrollOffset + maxItemsToShow) { setScrollOffset(newIndex - maxItemsToShow + 1); } }, [ searchQuery, filteredItems, activeIndex, orderedIds, scrollOffset, maxItemsToShow, selectedIds, setSetting, ], ); useKeypress( (key: Key) => { if (keyMatchers[Command.ESCAPE](key)) { onClose?.(); return true; } if (keyMatchers[Command.DIALOG_NAVIGATION_UP](key)) { const newIndex = activeIndex > 0 ? activeIndex - 1 : filteredItems.length - 1; setActiveIndex(newIndex); 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; setActiveIndex(newIndex); if (newIndex === 0) { setScrollOffset(0); } else if (newIndex >= scrollOffset + maxItemsToShow) { setScrollOffset(newIndex - maxItemsToShow + 1); } return true; } if (keyMatchers[Command.MOVE_LEFT](key)) { handleReorder(-1); return true; } if (keyMatchers[Command.MOVE_RIGHT](key)) { handleReorder(1); return true; } if (keyMatchers[Command.RETURN](key)) { void handleConfirm(); return true; } return false; }, { isActive: true, priority: true }, ); const visibleItems = filteredItems.slice( scrollOffset, scrollOffset + maxItemsToShow, ); // Preview logic const previewText = useMemo(() => { const itemsToPreview = orderedIds.filter((id) => selectedIds.has(id)); if (itemsToPreview.length === 0) return 'Empty Footer'; // Mock values for preview const mockValues: Record = { cwd: ~/dev/gemini-cli, 'git-branch': main*, 'sandbox-status': macOS Seatbelt, 'model-name': ( gemini-2.5-pro ), 'context-remaining': 85%, quota: 1.2k left, 'memory-usage': 124MB, 'error-count': 2 errors, 'session-id': 769992f9, 'code-changes': ( +12 -4 ), 'token-count': tokens:1.5k, corgi: 🐶, }; const elements: React.ReactNode[] = []; itemsToPreview.forEach((id, idx) => { if (idx > 0) { elements.push( {' | '} , ); } elements.push({mockValues[id] || id}); }); return elements; }, [orderedIds, selectedIds]); return ( Configure Footer Select which items to display in the footer. Type to search {searchBuffer && } {visibleItems.length === 0 ? ( No items found. ) : ( visibleItems.map((item, idx) => { const index = scrollOffset + idx; const isFocused = index === activeIndex; const isChecked = selectedIds.has(item.key); return ( {isFocused ? '> ' : ' '} [{isChecked ? '✓' : ' '}]{' '} {item.label.padEnd(maxLabelWidth + 1)} {item.description} ); }) )} ↑/↓ navigate · ←/→ reorder · enter select · esc close {searchQuery && ( Reordering is disabled when searching. )} Preview: {previewText} ); };