/** * @license * Copyright 2025 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ import React, { useState, useMemo } from 'react'; import { Box, Text } from 'ink'; import { SemanticColors } from '../colors.js'; import { useResponsive } from '../hooks/useResponsive.js'; import { truncateEnd } from '../utils/responsive.js'; import { useKeypress } from '../hooks/useKeypress.js'; import { getBorderStyle } from '../contexts/UnicodeRenderingContext.js'; interface ProviderDialogProps { providers: string[]; currentProvider?: string; onSelect: (providerName: string) => void; onClose: () => void; } interface ProviderDialogViewProps { providers: string[]; filteredProviders: string[]; index: number; searchTerm: string; isSearching: boolean; isNarrow: boolean; isWide: boolean; width: number; columns: number; colWidth: number; } interface ProviderDialogController { viewProps: ProviderDialogViewProps; onKeypress: (key: { name?: string; sequence?: string; ctrl?: boolean; meta?: boolean; }) => void; } interface ProviderDialogKeypressState { filteredProviders: string[]; index: number; searchTerm: string; isSearching: boolean; isNarrow: boolean; columns: number; setSearchTerm: React.Dispatch>; setIsSearching: React.Dispatch>; move: (delta: number) => void; onSelect: (providerName: string) => void; onClose: () => void; } function ProviderSearchHeader({ searchTerm, isSearching, filteredCount, totalCount, }: { searchTerm: string; isSearching: boolean; filteredCount: number; totalCount: number; }) { return ( Search:{' '} {isSearching && } {searchTerm} {searchTerm && ( {' '} (Found {filteredCount} of {totalCount} providers) )} ); } function ProviderListItem({ name, selected, isSearching, isNarrow, isWide, colWidth, }: { name: string; selected: boolean; isSearching: boolean; isNarrow: boolean; isWide: boolean; colWidth: number; }) { const getDisplayName = () => { if (isWide) { return name; } return name.length > 20 ? truncateEnd(name, 20) : name; }; const displayName = getDisplayName(); const getTextColor = () => { if (selected) { return SemanticColors.text.accent; } if (isSearching && !isNarrow) { return SemanticColors.text.secondary; } return SemanticColors.text.primary; }; return ( {selected ? '● ' : '○ '} {displayName} ); } function ProviderGrid({ filteredProviders, index, isSearching, isNarrow, isWide, columns, colWidth, }: Pick< ProviderDialogViewProps, | 'filteredProviders' | 'index' | 'isSearching' | 'isNarrow' | 'isWide' | 'columns' | 'colWidth' >) { const rows = Math.ceil(filteredProviders.length / columns); const grid: React.ReactNode[] = []; for (let r = 0; r < rows; r++) { const rowItems = [] as React.ReactNode[]; for (let c = 0; c < columns; c++) { const i = r * columns + c; if (i < filteredProviders.length) { rowItems.push( , ); } } grid.push({rowItems}); } return <>{grid}; } function ProviderResults({ filteredProviders, searchTerm, ...gridProps }: Pick & Pick< ProviderDialogViewProps, 'index' | 'isSearching' | 'isNarrow' | 'isWide' | 'columns' | 'colWidth' >) { if (filteredProviders.length === 0) { return ( No providers match "{searchTerm}" ); } return ; } function NarrowProviderDialogContent(props: ProviderDialogViewProps) { const { filteredProviders, searchTerm } = props; return ( Select Provider Search: {searchTerm} Type to filter, Enter to select, Esc to cancel {filteredProviders.length} providers{searchTerm && ` found`} ); } function WideProviderDialogContent(props: ProviderDialogViewProps) { const { providers, filteredProviders, index, searchTerm, isSearching } = props; return ( {isSearching ? 'Search Providers' : 'Select Provider (←/→/↑/↓, Enter to choose, Esc to cancel)'} {filteredProviders.length > 0 && !isSearching && ( Selected: {filteredProviders[index]} )} Tab to switch modes ); } function ProviderDialogContent(props: ProviderDialogViewProps) { return props.isNarrow ? ( ) : ( ); } function ProviderDialogFrame(props: ProviderDialogViewProps) { return props.isNarrow ? ( ) : ( ); } function createProviderDialogKeypressHandler({ filteredProviders, index, searchTerm, isSearching, isNarrow, columns, setSearchTerm, setIsSearching, move, onSelect, onClose, }: ProviderDialogKeypressState) { return (key: { name?: string; sequence?: string; ctrl?: boolean; meta?: boolean; }) => { if (key.name === 'escape') { if (isSearching && searchTerm.length > 0) setSearchTerm(''); else onClose(); return; } if (isSearching || isNarrow) { if (key.name === 'return' && filteredProviders.length > 0) { if (isNarrow) { onSelect(filteredProviders[index]); return; } setIsSearching(false); } else if (key.name === 'tab' && !isNarrow) setIsSearching(false); else if (key.name === 'backspace' || key.name === 'delete') { setSearchTerm((prev) => prev.slice(0, -1)); } else if (isPrintableKeypress(key)) { setSearchTerm((prev) => prev + key.sequence); } return; } if (key.name === 'return' && filteredProviders.length > 0) { onSelect(filteredProviders[index]); return; } if (key.name === 'tab') setIsSearching(true); if (filteredProviders.length === 0) return; if (key.name === 'left') move(-1); if (key.name === 'right') move(1); if (key.name === 'up') move(-columns); if (key.name === 'down') move(columns); }; } function useProviderDialogController({ providers, currentProvider, onSelect, onClose, }: ProviderDialogProps): ProviderDialogController { const { isNarrow, isWide, width } = useResponsive(); const [searchTerm, setSearchTerm] = useState(''); const [isSearching, setIsSearching] = useState(isNarrow); const filteredProviders = useMemo( () => providers.filter((p) => p.toLowerCase().includes(searchTerm.toLowerCase()), ), [providers, searchTerm], ); const [index, setIndex] = useState(() => { const currentIndex = providers.findIndex((p) => p === currentProvider); return Math.max(0, currentIndex); }); React.useEffect(() => { if (searchTerm.length === 0) { const currentIndex = providers.findIndex((p) => p === currentProvider); setIndex(Math.max(0, currentIndex)); } else { setIndex(0); } }, [searchTerm, providers, currentProvider]); const columns = isNarrow ? 1 : 3; const longest = filteredProviders.reduce( (len, p) => Math.max(len, p.length), 0, ); const colWidth = isWide ? Math.max(longest + 4, 30) : Math.max(longest + 4, 20); const move = (delta: number) => { let next = index + delta; if (next < 0) next = 0; if (next >= filteredProviders.length) next = filteredProviders.length - 1; setIndex(next); }; const onKeypress = createProviderDialogKeypressHandler({ filteredProviders, index, searchTerm, isSearching, isNarrow, columns, setSearchTerm, setIsSearching, move, onSelect, onClose, }); return { onKeypress, viewProps: { providers, filteredProviders, index, searchTerm, isSearching, isNarrow, isWide, width, columns, colWidth, }, }; } export const ProviderDialog: React.FC = (props) => { const { viewProps, onKeypress } = useProviderDialogController(props); useKeypress(onKeypress, { isActive: true }); return ; }; function isPrintableKeypress(key: { sequence?: string; ctrl?: boolean; meta?: boolean; }) { if (key.sequence === undefined) return false; if (typeof key.sequence !== 'string') return false; if (key.ctrl === true || key.meta === true) return false; return key.sequence.length === 1; }