import React, { useState, useEffect, useRef } from "react"; import { motion } from "framer-motion"; import { Button } from "@/components/ui/button"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { api } from "@/lib/api"; import { X, Command, Search, Globe, FolderOpen, Zap, FileCode, Terminal, AlertCircle, User, Building2 } from "lucide-react"; import type { SlashCommand } from "@/lib/api"; import { cn } from "@/lib/utils"; import { useTrackEvent, useFeatureAdoptionTracking } from "@/hooks"; import { useTranslation } from "react-i18next"; interface SlashCommandPickerProps { /** * The project path for loading project-specific commands */ projectPath?: string; /** * Callback when a command is selected */ onSelect: (command: SlashCommand) => void; /** * Callback to close the picker */ onClose: () => void; /** * Initial search query (text after /) */ initialQuery?: string; /** * Optional className for styling */ className?: string; } // Get icon for command based on its properties const getCommandIcon = (command: SlashCommand) => { // If it has bash commands, show terminal icon if (command.has_bash_commands) return Terminal; // If it has file references, show file icon if (command.has_file_references) return FileCode; // If it accepts arguments, show zap icon if (command.accepts_arguments) return Zap; // Based on scope if (command.scope === "project") return FolderOpen; if (command.scope === "user") return Globe; // Default return Command; }; /** * SlashCommandPicker component - Autocomplete UI for slash commands * * @example * console.log('Selected:', command)} * onClose={() => setShowPicker(false)} * /> */ export const SlashCommandPicker: React.FC = ({ projectPath, onSelect, onClose, initialQuery = "", className, }) => { const { t } = useTranslation(); const [commands, setCommands] = useState([]); const [filteredCommands, setFilteredCommands] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [selectedIndex, setSelectedIndex] = useState(0); const [searchQuery, setSearchQuery] = useState(initialQuery); const [activeTab, setActiveTab] = useState("custom"); const commandListRef = useRef(null); // Analytics tracking const trackEvent = useTrackEvent(); const slashCommandFeatureTracking = useFeatureAdoptionTracking('slash_commands'); // Load commands on mount or when project path changes useEffect(() => { loadCommands(); }, [projectPath]); // Filter commands based on search query and active tab useEffect(() => { if (!commands.length) { setFilteredCommands([]); return; } const query = searchQuery.toLowerCase(); let filteredByTab: SlashCommand[]; // Filter by active tab if (activeTab === "default") { // Show default/built-in commands filteredByTab = commands.filter(cmd => cmd.scope === "default"); } else { // Show all custom commands (both user and project) filteredByTab = commands.filter(cmd => cmd.scope !== "default"); } // Then filter by search query let filtered: SlashCommand[]; if (!query) { filtered = filteredByTab; } else { filtered = filteredByTab.filter(cmd => { // Match against command name if (cmd.name.toLowerCase().includes(query)) return true; // Match against full command if (cmd.full_command.toLowerCase().includes(query)) return true; // Match against namespace if (cmd.namespace && cmd.namespace.toLowerCase().includes(query)) return true; // Match against description if (cmd.description && cmd.description.toLowerCase().includes(query)) return true; return false; }); // Sort by relevance filtered.sort((a, b) => { // Exact name match first const aExact = a.name.toLowerCase() === query; const bExact = b.name.toLowerCase() === query; if (aExact && !bExact) return -1; if (!aExact && bExact) return 1; // Then by name starts with const aStarts = a.name.toLowerCase().startsWith(query); const bStarts = b.name.toLowerCase().startsWith(query); if (aStarts && !bStarts) return -1; if (!aStarts && bStarts) return 1; // Then alphabetically return a.name.localeCompare(b.name); }); } setFilteredCommands(filtered); // Reset selected index when filtered list changes setSelectedIndex(0); }, [searchQuery, commands, activeTab]); // Keyboard navigation useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { switch (e.key) { case 'Escape': e.preventDefault(); onClose(); break; case 'Enter': e.preventDefault(); if (filteredCommands.length > 0 && selectedIndex < filteredCommands.length) { const command = filteredCommands[selectedIndex]; trackEvent.slashCommandSelected({ command_name: command.name, selection_method: 'keyboard' }); slashCommandFeatureTracking.trackUsage(); onSelect(command); } break; case 'ArrowUp': e.preventDefault(); setSelectedIndex(prev => Math.max(0, prev - 1)); break; case 'ArrowDown': e.preventDefault(); setSelectedIndex(prev => Math.min(filteredCommands.length - 1, prev + 1)); break; } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [filteredCommands, selectedIndex, onSelect, onClose]); // Scroll selected item into view useEffect(() => { if (commandListRef.current) { const selectedElement = commandListRef.current.querySelector(`[data-index="${selectedIndex}"]`); if (selectedElement) { selectedElement.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); } } }, [selectedIndex]); const loadCommands = async () => { try { setIsLoading(true); setError(null); // Always load fresh commands from filesystem const loadedCommands = await api.slashCommandsList(projectPath); setCommands(loadedCommands); } catch (err) { console.error("Failed to load slash commands:", err); setError(err instanceof Error ? err.message : 'Failed to load commands'); setCommands([]); } finally { setIsLoading(false); } }; const handleCommandClick = (command: SlashCommand) => { trackEvent.slashCommandSelected({ command_name: command.name, selection_method: 'click' }); slashCommandFeatureTracking.trackUsage(); onSelect(command); }; // Group commands by scope and namespace for the Custom tab const groupedCommands = filteredCommands.reduce((acc, cmd) => { let key: string; if (cmd.scope === "user") { key = cmd.namespace ? `${t('slashCommands.userCommands')}: ${cmd.namespace}` : t('slashCommands.userCommands'); } else if (cmd.scope === "project") { key = cmd.namespace ? `${t('slashCommands.projectCommands')}: ${cmd.namespace}` : t('slashCommands.projectCommands'); } else { key = cmd.namespace || t('slashCommands.commands'); } if (!acc[key]) { acc[key] = []; } acc[key].push(cmd); return acc; }, {} as Record); // Update search query from parent useEffect(() => { setSearchQuery(initialQuery); }, [initialQuery]); return ( {/* Header */}
{t('slashCommands.slashCommands')} {searchQuery && ( {t('slashCommands.searching')}: "{searchQuery}" )}
{/* Tabs */}
Default Custom
{/* Command List */}
{isLoading && (
Loading commands...
)} {error && (
{error}
)} {!isLoading && !error && ( <> {/* Default Tab Content */} {activeTab === "default" && ( <> {filteredCommands.length === 0 && (
{searchQuery ? 'No commands found' : 'No default commands available'} {!searchQuery && (

Default commands are built-in system commands

)}
)} {filteredCommands.length > 0 && (
{filteredCommands.map((command, index) => { const Icon = getCommandIcon(command); const isSelected = index === selectedIndex; return ( ); })}
)} )} {/* Custom Tab Content */} {activeTab === "custom" && ( <> {filteredCommands.length === 0 && (
{searchQuery ? 'No commands found' : 'No custom commands available'} {!searchQuery && (

Create commands in .claude/commands/ or ~/.claude/commands/

)}
)} {filteredCommands.length > 0 && (
{/* If no grouping needed, show flat list */} {Object.keys(groupedCommands).length === 1 ? (
{filteredCommands.map((command, index) => { const Icon = getCommandIcon(command); const isSelected = index === selectedIndex; return ( ); })}
) : ( // Show grouped by scope/namespace
{Object.entries(groupedCommands).map(([groupKey, groupCommands]) => (

{groupKey.startsWith(t('slashCommands.userCommands')) && } {groupKey.startsWith(t('slashCommands.projectCommands')) && } {groupKey}

{groupCommands.map((command) => { const Icon = getCommandIcon(command); const globalIndex = filteredCommands.indexOf(command); const isSelected = globalIndex === selectedIndex; return ( ); })}
))}
)}
)} )} )}
{/* Footer */}

↑↓ Navigate • Enter Select • Esc Close

); };