/** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { useCallback, useMemo, useReducer, useState } from 'react'; import { Box, Text } from 'ink'; import { theme } from '../semantic-colors.js'; import { useSettingsStore } from '../contexts/SettingsContext.js'; 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 { SettingScope } from '../../config/settings.js'; import { BaseSelectionList } from './shared/BaseSelectionList.js'; import type { SelectionListItem } from '../hooks/useSelectionList.js'; import { DialogFooter } from './shared/DialogFooter.js'; import { useKeyMatchers } from '../hooks/useKeyMatchers.js'; interface FooterConfigDialogProps { onClose?: () => void; } interface FooterConfigItem { key: string; id: string; label: string; description?: string; type: 'config' | 'labels-toggle' | 'reset'; } interface FooterConfigState { orderedIds: string[]; selectedIds: Set; } type FooterConfigAction = | { type: 'MOVE_ITEM'; id: string; direction: number } | { type: 'TOGGLE_ITEM'; id: string } | { type: 'SET_STATE'; payload: Partial }; function footerConfigReducer( state: FooterConfigState, action: FooterConfigAction, ): FooterConfigState { switch (action.type) { case 'MOVE_ITEM': { const currentIndex = state.orderedIds.indexOf(action.id); const newIndex = currentIndex + action.direction; if ( currentIndex === -1 || newIndex < 0 || newIndex >= state.orderedIds.length ) { return state; } const newOrderedIds = [...state.orderedIds]; [newOrderedIds[currentIndex], newOrderedIds[newIndex]] = [ newOrderedIds[newIndex], newOrderedIds[currentIndex], ]; return { ...state, orderedIds: newOrderedIds }; } case 'TOGGLE_ITEM': { const nextSelected = new Set(state.selectedIds); if (nextSelected.has(action.id)) { nextSelected.delete(action.id); } else { nextSelected.add(action.id); } return { ...state, selectedIds: nextSelected }; } case 'SET_STATE': return { ...state, ...action.payload }; default: return state; } } export const FooterConfigDialog: React.FC = ({ onClose, }) => { const keyMatchers = useKeyMatchers(); const { settings, setSetting } = useSettingsStore(); const { constrainHeight, terminalHeight, staticExtraHeight } = useUIState(); const [state, dispatch] = useReducer(footerConfigReducer, undefined, () => resolveFooterState(settings.merged), ); const { orderedIds, selectedIds } = state; const [focusKey, setFocusKey] = useState(orderedIds[0]); const listItems = useMemo((): Array> => { const items: Array> = orderedIds .map((id: string) => { const item = ALL_ITEMS.find((i) => i.id === id); if (!item) return null; return { key: id, value: { key: id, id, label: item.id, description: item.description as string, type: 'config' as const, }, }; }) .filter((i): i is NonNullable => i !== null); items.push({ key: 'show-labels', value: { key: 'show-labels', id: 'show-labels', label: 'Show footer labels', type: 'labels-toggle', }, }); items.push({ key: 'reset', value: { key: 'reset', id: 'reset', label: 'Reset to default footer', type: 'reset', }, }); return items; }, [orderedIds]); 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)) { setSetting(SettingScope.User, 'ui.footer.items', finalItems); } onClose?.(); }, [ orderedIds, selectedIds, setSetting, settings.merged.ui?.footer?.items, onClose, ]); const handleResetToDefaults = useCallback(() => { setSetting(SettingScope.User, 'ui.footer.items', undefined); const newState = resolveFooterState(settings.merged); dispatch({ type: 'SET_STATE', payload: newState }); setFocusKey(newState.orderedIds[0]); }, [setSetting, settings.merged]); const handleToggleLabels = useCallback(() => { const current = settings.merged.ui.footer.showLabels !== false; setSetting(SettingScope.User, 'ui.footer.showLabels', !current); }, [setSetting, settings.merged.ui.footer.showLabels]); const handleSelect = useCallback( (item: FooterConfigItem) => { if (item.type === 'config') { dispatch({ type: 'TOGGLE_ITEM', id: item.id }); } else if (item.type === 'labels-toggle') { handleToggleLabels(); } else if (item.type === 'reset') { handleResetToDefaults(); } }, [handleResetToDefaults, handleToggleLabels], ); const handleHighlight = useCallback((item: FooterConfigItem) => { setFocusKey(item.key); }, []); useKeypress( (key: Key) => { if (keyMatchers[Command.ESCAPE](key)) { handleSaveAndClose(); return true; } if (keyMatchers[Command.MOVE_LEFT](key)) { if (focusKey && orderedIds.includes(focusKey)) { dispatch({ type: 'MOVE_ITEM', id: focusKey, direction: -1 }); return true; } } if (keyMatchers[Command.MOVE_RIGHT](key)) { if (focusKey && orderedIds.includes(focusKey)) { dispatch({ type: 'MOVE_ITEM', id: focusKey, direction: 1 }); return true; } } return false; }, { isActive: true, priority: true }, ); const showLabels = settings.merged.ui.footer.showLabels !== false; // Preview logic const previewContent = useMemo(() => { if (focusKey === 'reset') { return ( Default footer (uses legacy settings) ); } const itemsToPreview = orderedIds.filter((id: string) => selectedIds.has(id), ); if (itemsToPreview.length === 0) return null; const itemColor = showLabels ? theme.text.primary : theme.ui.comment; const getColor = (id: string, defaultColor?: string) => defaultColor || itemColor; // Mock data for preview (headers come from ALL_ITEMS) const mockData: Record = { workspace: ( ~/project/path ), 'git-branch': main, sandbox: docker, 'model-name': ( gemini-2.5-pro ), 'context-used': ( 85% used ), quota: 97%, 'memory-usage': ( 260 MB ), 'session-id': ( 769992f9 ), 'code-changes': ( +12 -4 ), 'token-count': ( 1.5k tokens ), }; const rowItems: FooterRowItem[] = itemsToPreview .filter((id: string) => mockData[id]) .map((id: string) => ({ key: id, header: ALL_ITEMS.find((i) => i.id === id)?.header ?? id, element: mockData[id], flexGrow: 0, isFocused: id === focusKey, })); return ( ); }, [orderedIds, selectedIds, focusKey, showLabels]); const availableTerminalHeight = constrainHeight ? terminalHeight - staticExtraHeight : Number.MAX_SAFE_INTEGER; const BORDER_HEIGHT = 2; // Outer round border const STATIC_ELEMENTS = 13; // Text, margins, preview box, dialog footer // Default padding adds 2 lines (top and bottom) let includePadding = true; if (availableTerminalHeight < BORDER_HEIGHT + 2 + STATIC_ELEMENTS + 6) { includePadding = false; } const effectivePaddingY = includePadding ? 2 : 0; const availableListSpace = Math.max( 0, availableTerminalHeight - BORDER_HEIGHT - effectivePaddingY - STATIC_ELEMENTS, ); const maxItemsToShow = Math.max( 1, Math.min(listItems.length, Math.floor(availableListSpace / 2)), ); return ( Configure Footer{'\n'} Select which items to display in the footer. items={listItems} onSelect={handleSelect} onHighlight={handleHighlight} focusKey={focusKey} showNumbers={false} maxItemsToShow={maxItemsToShow} showScrollArrows={true} selectedIndicator=">" renderItem={(item, { isSelected, titleColor }) => { const configItem = item.value; const isChecked = configItem.type === 'config' ? selectedIds.has(configItem.id) : configItem.type === 'labels-toggle' ? showLabels : false; return ( {configItem.type !== 'reset' && ( [{isChecked ? '✓' : ' '}] )} {configItem.type !== 'reset' ? ' ' : ''} {configItem.label} {configItem.description && ( {' '} {configItem.description} )} ); }} /> Preview: {previewContent} ); };