/** * CommandPanel Component * * 命令管理面板:列出、搜索、创建、编辑、删除命令 */ import { useState, useEffect, useCallback } from 'react'; import { X, Plus, Search, Pencil, Trash2, RefreshCw, Terminal, User, FolderOpen, Cog, } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; import { toast } from 'sonner'; import { cn } from '../utils/cn'; import { modalOverlay, modalContent, smoothTransition } from '../utils/animations'; import { Button } from '../primitives/Button'; import { Input } from '../primitives/Input'; import { Skeleton } from './Skeleton'; import { CommandEditor } from './CommandEditor'; import { listCommands, deleteCommand, reloadCommands, type CommandListResponse, } from '../api/client.js'; interface CommandPanelProps { onClose: () => void; /** 是否启用响应式布局 */ responsive?: boolean; } type CommandItem = CommandListResponse['commands'][number]; // Source 图标映射 function getSourceIcon(source: string) { switch (source) { case 'builtin': return ; case 'user': return ; case 'project': return ; default: return ; } } // Source 标签样式 function getSourceBadgeClass(source: string) { switch (source) { case 'builtin': return 'bg-surface-muted text-fg-secondary'; case 'user': return 'bg-blue-500/20 text-blue-400'; case 'project': return 'bg-green-500/20 text-green-400'; default: return 'bg-surface-muted text-fg-secondary'; } } export function CommandPanel({ onClose, responsive = false }: CommandPanelProps) { // 数据状态 const [commands, setCommands] = useState([]); const [filteredCommands, setFilteredCommands] = useState([]); const [stats, setStats] = useState<{ total: number; bySource: Record } | null>( null ); // UI 状态 const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [deletingCommand, setDeletingCommand] = useState(null); const [reloading, setReloading] = useState(false); // 编辑器状态 const [editorOpen, setEditorOpen] = useState(false); const [editingCommand, setEditingCommand] = useState(undefined); // 加载命令列表 const loadCommands = useCallback(async () => { setLoading(true); try { const result = await listCommands(); if (result.success) { setCommands(result.data.commands); setFilteredCommands(result.data.commands); setStats(result.data.stats); } else { toast.error('Failed to load commands'); } } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to load commands'); } finally { setLoading(false); } }, []); // 初始加载 useEffect(() => { loadCommands(); }, [loadCommands]); // 搜索过滤 useEffect(() => { if (!searchQuery.trim()) { setFilteredCommands(commands); return; } const query = searchQuery.toLowerCase(); const filtered = commands.filter( (cmd) => cmd.name.toLowerCase().includes(query) || cmd.description?.toLowerCase().includes(query) ); setFilteredCommands(filtered); }, [searchQuery, commands]); // 删除命令 const handleDelete = async (name: string) => { if (!confirm(`Are you sure you want to delete command "/${name}"?`)) { return; } setDeletingCommand(name); try { const result = await deleteCommand(name); if (result.success) { toast.success(`Command "/${name}" deleted`); loadCommands(); } else { toast.error(result.error || 'Failed to delete command'); } } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to delete command'); } finally { setDeletingCommand(null); } }; // 重新加载命令 const handleReload = async () => { setReloading(true); try { const result = await reloadCommands(); if (result.success) { toast.success('Commands reloaded'); loadCommands(); } else { toast.error('Failed to reload commands'); } } catch (err) { toast.error(err instanceof Error ? err.message : 'Failed to reload commands'); } finally { setReloading(false); } }; // 打开编辑器(创建) const openCreateEditor = () => { setEditingCommand(undefined); setEditorOpen(true); }; // 打开编辑器(编辑) const openEditEditor = (name: string) => { setEditingCommand(name); setEditorOpen(true); }; // 编辑器保存回调 const handleEditorSaved = () => { loadCommands(); }; // Loading 骨架屏 const LoadingSkeleton = () => (
{[1, 2, 3, 4, 5].map((i) => (
))}
); return ( <> e.stopPropagation()} className={cn( 'bg-surface-subtle max-h-[90vh] overflow-hidden flex flex-col', responsive ? 'w-full md:w-full md:max-w-2xl md:mx-4 rounded-t-2xl md:rounded-lg' : 'rounded-lg w-full max-w-2xl mx-4' )} > {/* Header */}
{responsive && (
)}

Commands

{stats && (

{stats.total} commands ({stats.bySource.builtin || 0} builtin,{' '} {stats.bySource.user || 0} user, {stats.bySource.project || 0} project)

)}
{/* Toolbar */}
setSearchQuery(e.target.value)} placeholder="Search commands..." className="pl-9" />
{/* Command List */}
{loading ? ( ) : filteredCommands.length === 0 ? (

{searchQuery ? `No commands matching "${searchQuery}"` : 'No commands found'}

{!searchQuery && ( )}
) : ( {filteredCommands.map((command) => ( {/* Icon */} {getSourceIcon(command.source)} {/* Info */}
/{command.name} {command.source}
{command.description && (

{command.description}

)}
{/* Actions */}
{command.source !== 'builtin' && ( )}
))}
)}
{/* Command Editor */} {editorOpen && ( setEditorOpen(false)} onSaved={handleEditorSaved} responsive={responsive} /> )} ); }