import { useMemo, useRef, useEffect, useState } from "react"; import { cn } from "../../src/lib/utils"; import type { AvailableCommand } from "../../src/acp/types"; // ============================================================================= // Slash command picker — floating above ChatInput // ============================================================================= interface CommandMenuProps { commands: AvailableCommand[]; /** Text after "/" used for filtering */ filter: string; onSelect: (command: AvailableCommand) => void; onClose: () => void; className?: string; } /** * Prefix match — checks if the text starts with the query. */ function prefixMatch(query: string, text: string): boolean { if (!query) return true; return text.toLowerCase().startsWith(query.toLowerCase()); } export function CommandMenu({ commands, filter, onSelect, onClose, className, }: CommandMenuProps) { const containerRef = useRef(null); const [activeIndex, setActiveIndex] = useState(0); // Filter commands by current input const filtered = useMemo(() => { if (!filter) return commands; return commands.filter( (cmd) => prefixMatch(filter, cmd.name), ); }, [commands, filter]); // Reset active index when filter changes useEffect(() => { setActiveIndex(0); }, [filter]); // Close on outside click useEffect(() => { const handleClick = (e: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { onClose(); } }; document.addEventListener("mousedown", handleClick); return () => document.removeEventListener("mousedown", handleClick); }, [onClose]); // Handle keyboard navigation (ArrowUp/ArrowDown/Enter) via document-level listener useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (filtered.length === 0) return; if (e.key === "ArrowDown") { e.preventDefault(); setActiveIndex((prev) => (prev + 1) % filtered.length); } else if (e.key === "ArrowUp") { e.preventDefault(); setActiveIndex((prev) => (prev - 1 + filtered.length) % filtered.length); } else if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); const cmd = filtered[activeIndex]; if (cmd) onSelect(cmd); } }; document.addEventListener("keydown", handleKeyDown, true); // capture phase return () => document.removeEventListener("keydown", handleKeyDown, true); }, [filtered, activeIndex, onSelect]); // Scroll active item into view useEffect(() => { const container = containerRef.current; if (!container) return; const active = container.querySelector("[data-active='true']"); active?.scrollIntoView({ block: "nearest" }); }, [activeIndex]); return (
{filtered.length === 0 ? (
没有匹配的命令
) : ( filtered.map((cmd, index) => ( )) )}
); }