import React from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { toast } from '@/components/ui'; import { isMobileDeviceViaCSS } from '@/lib/device'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu'; import { useCommandsStore, isCommandBuiltIn, type Command } from '@/stores/useCommandsStore'; import { useSkillsStore } from '@/stores/useSkillsStore'; import { useShallow } from 'zustand/react/shallow'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { cn } from '@/lib/utils'; import { SettingsProjectSelector } from '@/components/sections/shared/SettingsProjectSelector'; import { Icon } from "@/components/icon/Icon"; import { useI18n } from '@/lib/i18n'; interface CommandsSidebarProps { onItemSelect?: () => void; } export const CommandsSidebar: React.FC = ({ onItemSelect }) => { const { t } = useI18n(); const [renameDialogCommand, setRenameDialogCommand] = React.useState(null); const [renameNewName, setRenameNewName] = React.useState(''); const [confirmActionCommand, setConfirmActionCommand] = React.useState(null); const [confirmActionType, setConfirmActionType] = React.useState<'delete' | 'reset' | null>(null); const [isConfirmActionPending, setIsConfirmActionPending] = React.useState(false); const [openMenuCommand, setOpenMenuCommand] = React.useState(null); const { selectedCommandName, commands, setSelectedCommand, setCommandDraft, createCommand, deleteCommand, loadCommands, } = useCommandsStore(useShallow((s) => ({ selectedCommandName: s.selectedCommandName, commands: s.commands, setSelectedCommand: s.setSelectedCommand, setCommandDraft: s.setCommandDraft, createCommand: s.createCommand, deleteCommand: s.deleteCommand, loadCommands: s.loadCommands, }))); const skills = useSkillsStore((s) => s.skills); const loadSkills = useSkillsStore((s) => s.loadSkills); React.useEffect(() => { loadCommands(); loadSkills(); }, [loadCommands, loadSkills]); const skillNames = React.useMemo(() => new Set(skills.map((skill) => skill.name)), [skills]); const commandOnlyItems = React.useMemo( () => commands.filter((command) => !skillNames.has(command.name)), [commands, skillNames], ); React.useEffect(() => { if (!selectedCommandName) { return; } if (skillNames.has(selectedCommandName)) { setSelectedCommand(null); } }, [selectedCommandName, setSelectedCommand, skillNames]); const bgClass = 'bg-background'; const handleCreateNew = () => { // Generate unique name const baseName = 'new-command'; let newName = baseName; let counter = 1; while (commands.some((c) => c.name === newName)) { newName = `${baseName}-${counter}`; counter++; } // Set draft and open the page for editing setCommandDraft({ name: newName, scope: 'user' }); setSelectedCommand(newName); onItemSelect?.(); }; const handleDeleteCommand = async (command: Command) => { if (isCommandBuiltIn(command)) { toast.error(t('settings.commands.sidebar.toast.builtInCannotDelete')); return; } setConfirmActionCommand(command); setConfirmActionType('delete'); }; const handleResetCommand = async (command: Command) => { if (!isCommandBuiltIn(command)) { return; } setConfirmActionCommand(command); setConfirmActionType('reset'); }; const closeConfirmActionDialog = () => { setConfirmActionCommand(null); setConfirmActionType(null); }; const handleConfirmAction = async () => { if (!confirmActionCommand || !confirmActionType) { return; } setIsConfirmActionPending(true); const success = await deleteCommand(confirmActionCommand.name); if (success) { if (confirmActionType === 'delete') { toast.success(t('settings.commands.sidebar.toast.commandDeleted', { name: confirmActionCommand.name })); } else { toast.success(t('settings.commands.sidebar.toast.commandReset', { name: confirmActionCommand.name })); } closeConfirmActionDialog(); } else if (confirmActionType === 'delete') { toast.error(t('settings.commands.sidebar.toast.deleteFailed')); } else { toast.error(t('settings.commands.sidebar.toast.resetFailed')); } setIsConfirmActionPending(false); }; const handleDuplicateCommand = (command: Command) => { const baseName = command.name; let copyNumber = 1; let newName = `${baseName}-copy`; while (commands.some((c) => c.name === newName)) { copyNumber++; newName = `${baseName}-copy-${copyNumber}`; } // Set draft with prefilled values from source command setCommandDraft({ name: newName, scope: command.scope || 'user', description: command.description, template: command.template, agent: command.agent, model: command.model, }); setSelectedCommand(newName); }; const handleOpenRenameDialog = (command: Command) => { setRenameNewName(command.name); setRenameDialogCommand(command); }; const handleRenameCommand = async () => { if (!renameDialogCommand) return; const sanitizedName = renameNewName.trim().replace(/\s+/g, '-'); if (!sanitizedName) { toast.error(t('settings.commands.sidebar.toast.commandNameRequired')); return; } if (sanitizedName === renameDialogCommand.name) { setRenameDialogCommand(null); return; } if (commands.some((cmd) => cmd.name === sanitizedName)) { toast.error(t('settings.commands.sidebar.toast.commandExists')); return; } // Create new command with new name and all existing config const success = await createCommand({ name: sanitizedName, description: renameDialogCommand.description, template: renameDialogCommand.template, agent: renameDialogCommand.agent, model: renameDialogCommand.model, }); if (success) { // Delete old command const deleteSuccess = await deleteCommand(renameDialogCommand.name); if (deleteSuccess) { toast.success(`Command renamed to "${sanitizedName}"`); setSelectedCommand(sanitizedName); } else { toast.error(t('settings.commands.sidebar.toast.removeOldAfterRenameFailed')); } } else { toast.error(t('settings.commands.sidebar.toast.renameFailed')); } setRenameDialogCommand(null); }; const builtInCommands = commandOnlyItems.filter(isCommandBuiltIn); const customCommands = commandOnlyItems.filter((cmd) => !isCommandBuiltIn(cmd)); return (

{t('settings.commands.sidebar.title')}

{t('settings.commands.sidebar.total', { count: commandOnlyItems.length })}
{commandOnlyItems.length === 0 ? (

{t('settings.commands.sidebar.empty.title')}

{t('settings.commands.sidebar.empty.description')}

) : ( <> {builtInCommands.length > 0 && ( <>
{t('settings.commands.sidebar.section.builtIn')}
{[...builtInCommands].sort((a, b) => a.name.localeCompare(b.name)).map((command) => ( { setSelectedCommand(command.name); onItemSelect?.(); }} onReset={() => handleResetCommand(command)} onDuplicate={() => handleDuplicateCommand(command)} isMenuOpen={openMenuCommand === command.name} onMenuOpenChange={(open) => setOpenMenuCommand(open ? command.name : null)} /> ))} )} {customCommands.length > 0 && ( <>
{t('settings.commands.sidebar.section.custom')}
{[...customCommands].sort((a, b) => a.name.localeCompare(b.name)).map((command) => ( { setSelectedCommand(command.name); onItemSelect?.(); }} onRename={() => handleOpenRenameDialog(command)} onDelete={() => handleDeleteCommand(command)} onDuplicate={() => handleDuplicateCommand(command)} isMenuOpen={openMenuCommand === command.name} onMenuOpenChange={(open) => setOpenMenuCommand(open ? command.name : null)} /> ))} )} )}
{ if (!open && !isConfirmActionPending) { closeConfirmActionDialog(); } }} > {confirmActionType === 'delete' ? t('settings.commands.sidebar.dialog.deleteTitle') : t('settings.commands.sidebar.dialog.resetTitle')} {confirmActionType === 'delete' ? t('settings.commands.sidebar.dialog.deleteDescription', { name: confirmActionCommand?.name ?? '' }) : t('settings.commands.sidebar.dialog.resetDescription', { name: confirmActionCommand?.name ?? '' })} {/* Rename Dialog */} !open && setRenameDialogCommand(null)}> {t('settings.commands.sidebar.renameDialog.title')} {t('settings.commands.sidebar.renameDialog.description', { name: renameDialogCommand?.name ?? '' })} setRenameNewName(e.target.value)} placeholder={t('settings.commands.sidebar.renameDialog.placeholder')} className="text-foreground placeholder:text-muted-foreground" onKeyDown={(e) => { if (e.key === 'Enter') { handleRenameCommand(); } }} />
); }; interface CommandListItemProps { command: Command; isSelected: boolean; onSelect: () => void; onDelete?: () => void; onReset?: () => void; onRename?: () => void; onDuplicate: () => void; isMenuOpen: boolean; onMenuOpenChange: (open: boolean) => void; } const CommandListItem: React.FC = ({ command, isSelected, onSelect, onDelete, onReset, onRename, onDuplicate, isMenuOpen, onMenuOpenChange, }) => { const { t } = useI18n(); const isMobile = isMobileDeviceViaCSS(); const [isContextMenuOpen, setIsContextMenuOpen] = React.useState(false); const renderMenuItems = (Item: React.ElementType) => ( <> {onRename && ( { e.stopPropagation(); onRename(); }}> {t('settings.common.actions.rename')} )} { e.stopPropagation(); onDuplicate(); }}> {t('settings.common.actions.duplicate')} {onReset && ( { e.stopPropagation(); onReset(); }}> {t('settings.common.actions.reset')} )} {onDelete && ( { e.stopPropagation(); onDelete(); }} className="text-destructive focus:text-destructive"> {t('settings.common.actions.delete')} )} ); return ( { e.preventDefault(); setIsContextMenuOpen(true); } : undefined} />}>
{ if (open) setIsContextMenuOpen(false); onMenuOpenChange(open); }}> {renderMenuItems(DropdownMenuItem)}
{renderMenuItems(ContextMenuItem)}
); };