import React from 'react'; import { cn, fuzzyMatch } from '@/lib/utils'; import { useConfigStore } from '@/stores/useConfigStore'; import { useAgentsStore, isAgentBuiltIn, type AgentWithExtras } from '@/stores/useAgentsStore'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; interface AgentInfo { name: string; description?: string; mode?: string | null; scope?: string; isBuiltIn?: boolean; } export interface AgentMentionAutocompleteHandle { handleKeyDown: (key: string) => void; } type AutocompleteTab = 'commands' | 'agents' | 'files'; const isMentionableAgentMode = (mode?: string | null): boolean => { if (!mode) return false; return mode !== 'primary'; }; interface AgentMentionAutocompleteProps { searchQuery: string; onAgentSelect: (agentName: string) => void; onClose: () => void; showTabs?: boolean; activeTab?: AutocompleteTab; onTabSelect?: (tab: AutocompleteTab) => void; } export const AgentMentionAutocomplete = React.forwardRef(({ searchQuery, onAgentSelect, onClose, showTabs, activeTab = 'agents', onTabSelect, }, ref) => { const containerRef = React.useRef(null); const [selectedIndex, setSelectedIndex] = React.useState(0); const [agents, setAgents] = React.useState([]); const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]); const ignoreTabClickRef = React.useRef(false); const { getVisibleAgents } = useConfigStore(); const { agents: agentsWithMetadata, loadAgents } = useAgentsStore(); React.useEffect(() => { if (agentsWithMetadata.length === 0) { void loadAgents(); } }, [loadAgents, agentsWithMetadata.length]); React.useEffect(() => { const visibleAgents = getVisibleAgents(); const filtered = visibleAgents .filter((agent) => isMentionableAgentMode(agent.mode)) .map((agent) => { const metadata = agentsWithMetadata.find(a => a.name === agent.name) as (AgentWithExtras & { scope?: string }) | undefined; return { name: agent.name, description: agent.description, mode: agent.mode ?? undefined, scope: metadata?.scope, isBuiltIn: metadata ? isAgentBuiltIn(metadata) : false, }; }); const normalizedQuery = searchQuery.trim(); const matches = normalizedQuery.length ? filtered.filter((agent) => fuzzyMatch(agent.name, normalizedQuery)) : filtered; matches.sort((a, b) => a.name.localeCompare(b.name)); setAgents(matches); setSelectedIndex(0); }, [getVisibleAgents, searchQuery, agentsWithMetadata]); React.useEffect(() => { itemRefs.current[selectedIndex]?.scrollIntoView({ behavior: 'smooth', block: 'nearest', }); }, [selectedIndex]); React.useEffect(() => { const handlePointerDown = (event: MouseEvent | TouchEvent) => { const target = event.target as Node | null; if (!target || !containerRef.current) { return; } if (!containerRef.current.contains(target)) { onClose(); } }; document.addEventListener('pointerdown', handlePointerDown, true); return () => { document.removeEventListener('pointerdown', handlePointerDown, true); }; }, [onClose]); React.useImperativeHandle(ref, () => ({ handleKeyDown: (key: string) => { if (key === 'Escape') { onClose(); return; } if (!agents.length) { return; } if (key === 'ArrowDown') { setSelectedIndex((prev) => (prev + 1) % agents.length); return; } if (key === 'ArrowUp') { setSelectedIndex((prev) => (prev - 1 + agents.length) % agents.length); return; } if (key === 'Enter' || key === 'Tab') { const agent = agents[(selectedIndex + agents.length) % agents.length]; if (agent) { onAgentSelect(agent.name); } } }, }), [agents, onAgentSelect, onClose, selectedIndex]); const renderAgent = (agent: AgentInfo, index: number) => { const isSystem = agent.isBuiltIn; const isProject = agent.scope === 'project'; return (
{ itemRefs.current[index] = el; }} className={cn( 'flex items-start gap-2 px-3 py-1.5 cursor-pointer rounded-lg typography-ui-label', index === selectedIndex && 'bg-interactive-selection' )} onClick={() => onAgentSelect(agent.name)} onMouseEnter={() => setSelectedIndex(index)} >
#{agent.name} {isSystem ? ( system ) : agent.scope ? ( {agent.scope} ) : null}
{agent.description && (
{agent.description}
)}
); }; return (
{showTabs ? (
{([ { id: 'commands' as const, label: 'Commands' }, { id: 'agents' as const, label: 'Agents' }, { id: 'files' as const, label: 'Files' }, ]).map((tab) => ( ))}
) : null} {agents.length ? (
{agents.map((agent, index) => renderAgent(agent, index))}
) : (
No agents found
)}
↑↓ navigate • Enter select • Esc close
); }); AgentMentionAutocomplete.displayName = 'AgentMentionAutocomplete';