/** * @license * Copyright 2025 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { useState, useMemo, useCallback } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../../colors.js'; import { useKeypress } from '../../hooks/useKeypress.js'; import { ListFocusMode, type SubagentInfo } from './types.js'; const isListFocusMode = (mode: ListFocusMode): boolean => mode === ListFocusMode.LIST; interface SubagentListMenuProps { subagents: SubagentInfo[]; onSelect: (subagent: SubagentInfo) => void; onEdit: (subagent: SubagentInfo) => void; onAttachProfile: (subagent: SubagentInfo) => void; onDelete: (subagent: SubagentInfo) => void; onBack: () => void; isLoading?: boolean; isFocused?: boolean; } function useFilteredSubagents(subagents: SubagentInfo[], searchTerm: string) { return useMemo(() => { if (!searchTerm.trim()) return subagents; const term = searchTerm.toLowerCase(); return subagents.filter( (s) => s.name.toLowerCase().includes(term) || s.profile.toLowerCase().includes(term), ); }, [subagents, searchTerm]); } function useSafeIndex(selectedIndex: number, filteredLength: number) { return useMemo(() => { if (filteredLength === 0) return 0; return Math.min(selectedIndex, filteredLength - 1); }, [selectedIndex, filteredLength]); } function SearchBar({ searchTerm, focusMode, filteredCount, totalCount, }: { searchTerm: string; focusMode: ListFocusMode; filteredCount: number; totalCount: number; }) { return ( Search: {focusMode === ListFocusMode.SEARCH ? ( {searchTerm} | ) : ( {searchTerm || '(press Tab to search)'} )} {' '} Found {filteredCount} {searchTerm ? ` of ${totalCount}` : ''} subagents ); } function SubagentItemRow({ subagent, isSelected, }: { subagent: SubagentInfo; isSelected: boolean; }) { return ( {isSelected ? '→ ' : ' '} {subagent.name.padEnd(25)} {subagent.profile} ); } function SubagentListViewport({ filteredSubagents, safeIndex, focusMode, }: { filteredSubagents: SubagentInfo[]; safeIndex: number; focusMode: ListFocusMode; }) { const maxVisible = 10; const viewStart = Math.max(0, safeIndex - Math.floor(maxVisible / 2)); const viewEnd = Math.min(filteredSubagents.length, viewStart + maxVisible); const visibleSubagents = filteredSubagents.slice(viewStart, viewEnd); if (filteredSubagents.length === 0) { return No matching subagents; } return ( {visibleSubagents.map((subagent, idx) => { const actualIndex = viewStart + idx; const isSelected = actualIndex === safeIndex && isListFocusMode(focusMode); return ( ); })} ); } function EmptySubagentsMessage() { return ( No subagents found. Use 'Create Subagent' to create one. [ESC] Back to menu ); } function useSearchKeys(opts: { isFocused: boolean; isLoading: boolean; focusMode: ListFocusMode; handleSearchInput: (char: string) => void; handleBackspace: () => void; setSearchTerm: React.Dispatch>; setFocusMode: React.Dispatch>; }) { useKeypress( (key) => { if ( !opts.isFocused || opts.isLoading || opts.focusMode !== ListFocusMode.SEARCH ) return; if (key.name === 'backspace' || key.name === 'delete') { opts.handleBackspace(); return; } if (key.name === 'return') { opts.setFocusMode(ListFocusMode.LIST); return; } if (key.name === 'escape') { opts.setSearchTerm(''); opts.setFocusMode(ListFocusMode.LIST); return; } const input = key.sequence; if (input && input.length === 1 && !key.ctrl && !key.meta) { opts.handleSearchInput(input); } }, { isActive: opts.isFocused && !opts.isLoading && opts.focusMode === ListFocusMode.SEARCH, }, ); } function useListNavKeys(opts: { focusMode: ListFocusMode; filteredSubagents: SubagentInfo[]; safeIndex: number; isFocused: boolean; isLoading: boolean; moveSelection: (d: number) => void; onSelect: (s: SubagentInfo) => void; onEdit: (s: SubagentInfo) => void; onAttachProfile: (s: SubagentInfo) => void; onDelete: (s: SubagentInfo) => void; onBack: () => void; setSearchTerm: React.Dispatch>; setFocusMode: React.Dispatch>; }) { useKeypress( (key) => { const input = key.sequence; if (key.name === 'escape' && opts.focusMode === ListFocusMode.LIST) { opts.onBack(); return; } if (key.name === 'tab') { opts.setFocusMode( opts.focusMode === ListFocusMode.LIST ? ListFocusMode.SEARCH : ListFocusMode.LIST, ); return; } if (!isListFocusMode(opts.focusMode)) return; if (key.name === 'up') { opts.moveSelection(-1); return; } if (key.name === 'down') { opts.moveSelection(1); return; } if (key.name === 'return' && opts.filteredSubagents.length > 0) { opts.onSelect(opts.filteredSubagents[opts.safeIndex]); return; } if (input === 'e' && opts.filteredSubagents.length > 0) { opts.onEdit(opts.filteredSubagents[opts.safeIndex]); return; } if (input === 'a' && opts.filteredSubagents.length > 0) { opts.onAttachProfile(opts.filteredSubagents[opts.safeIndex]); return; } if (input === 'd' && opts.filteredSubagents.length > 0) { opts.onDelete(opts.filteredSubagents[opts.safeIndex]); } }, { isActive: opts.isFocused && !opts.isLoading }, ); } function useListMenuState(subagents: SubagentInfo[], searchTerm: string) { const filteredSubagents = useFilteredSubagents(subagents, searchTerm); const [selectedIndex, setSelectedIndex] = useState(0); const safeIndex = useSafeIndex(selectedIndex, filteredSubagents.length); const moveSelection = useCallback( (delta: number) => { if (filteredSubagents.length === 0) return; let newIndex = safeIndex + delta; if (newIndex < 0) newIndex = 0; if (newIndex >= filteredSubagents.length) newIndex = filteredSubagents.length - 1; setSelectedIndex(newIndex); }, [safeIndex, filteredSubagents.length], ); return { filteredSubagents, safeIndex, moveSelection }; } export const SubagentListMenu: React.FC = ({ subagents, onSelect, onEdit, onAttachProfile, onDelete, onBack, isLoading = false, isFocused = true, }) => { const [searchTerm, setSearchTerm] = useState(''); const [focusMode, setFocusMode] = useState(ListFocusMode.LIST); const { filteredSubagents, safeIndex, moveSelection } = useListMenuState( subagents, searchTerm, ); const handleSearchInput = useCallback((char: string) => { setSearchTerm((prev) => prev + char); }, []); const handleBackspace = useCallback(() => { setSearchTerm((prev) => prev.slice(0, -1)); }, []); useSearchKeys({ isFocused, isLoading, focusMode, handleSearchInput, handleBackspace, setSearchTerm, setFocusMode, }); useListNavKeys({ focusMode, filteredSubagents, safeIndex, isFocused, isLoading, moveSelection, onSelect, onEdit, onAttachProfile, onDelete, onBack, setSearchTerm, setFocusMode, }); if (isLoading) { return ( Loading subagents... ); } if (subagents.length === 0) { return ; } return ( Controls: ↑↓ Navigate [Enter] Details [e] Edit [a] Attach [d] Delete [ESC] Main menu ); };