import React from 'react'; import { Button } from '@/components/ui/button'; import { ButtonLarge } from '@/components/ui/button-large'; import { Input } from '@/components/ui/input'; import { toast } from '@/components/ui'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { RiAddLine, RiTerminalBoxLine, RiMore2Line, RiDeleteBinLine, RiFileCopyLine, RiRestartLine, RiEditLine } from '@remixicon/react'; import { useCommandsStore, isCommandBuiltIn, type Command } from '@/stores/useCommandsStore'; import { useUIStore } from '@/stores/useUIStore'; import { useDeviceInfo } from '@/lib/device'; import { isVSCodeRuntime } from '@/lib/desktop'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; import { cn } from '@/lib/utils'; interface CommandsSidebarProps { onItemSelect?: () => void; } export const CommandsSidebar: React.FC = ({ onItemSelect }) => { const [renameDialogCommand, setRenameDialogCommand] = React.useState(null); const [renameNewName, setRenameNewName] = React.useState(''); const { selectedCommandName, commands, setSelectedCommand, setCommandDraft, createCommand, deleteCommand, loadCommands, } = useCommandsStore(); const { setSidebarOpen } = useUIStore(); const { isMobile } = useDeviceInfo(); const [isDesktopRuntime, setIsDesktopRuntime] = React.useState(() => { if (typeof window === 'undefined') return false; return typeof window.opencodeDesktop !== 'undefined'; }); const isVSCode = React.useMemo(() => isVSCodeRuntime(), []); React.useEffect(() => { if (typeof window === 'undefined') return; setIsDesktopRuntime(typeof window.opencodeDesktop !== 'undefined'); }, []); React.useEffect(() => { loadCommands(); }, [loadCommands]); const bgClass = isDesktopRuntime ? 'bg-transparent' : isVSCode ? 'bg-background' : 'bg-sidebar'; 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?.(); if (isMobile) { setSidebarOpen(false); } }; const handleDeleteCommand = async (command: Command) => { if (isCommandBuiltIn(command)) { toast.error('Built-in commands cannot be deleted'); return; } if (window.confirm(`Are you sure you want to delete command "${command.name}"?`)) { const success = await deleteCommand(command.name); if (success) { toast.success(`Command "${command.name}" deleted successfully`); } else { toast.error('Failed to delete command'); } } }; const handleResetCommand = async (command: Command) => { if (!isCommandBuiltIn(command)) { return; } if (window.confirm(`Are you sure you want to reset command "${command.name}" to its default configuration?`)) { const success = await deleteCommand(command.name); if (success) { toast.success(`Command "${command.name}" reset to default`); } else { toast.error('Failed to reset command'); } } }; 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, subtask: command.subtask, }); setSelectedCommand(newName); if (isMobile) { setSidebarOpen(false); } }; 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('Command name is required'); return; } if (sanitizedName === renameDialogCommand.name) { setRenameDialogCommand(null); return; } if (commands.some((cmd) => cmd.name === sanitizedName)) { toast.error('A command with this name already exists'); 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, subtask: renameDialogCommand.subtask, }); if (success) { // Delete old command const deleteSuccess = await deleteCommand(renameDialogCommand.name); if (deleteSuccess) { toast.success(`Command renamed to "${sanitizedName}"`); setSelectedCommand(sanitizedName); } else { toast.error('Failed to remove old command after rename'); } } else { toast.error('Failed to rename command'); } setRenameDialogCommand(null); }; const builtInCommands = commands.filter(isCommandBuiltIn); const customCommands = commands.filter((cmd) => !isCommandBuiltIn(cmd)); return (
Total {commands.length}
{commands.length === 0 ? (

No commands configured

Use the + button above to create one

) : ( <> {builtInCommands.length > 0 && ( <>
Built-in Commands
{[...builtInCommands].sort((a, b) => a.name.localeCompare(b.name)).map((command) => ( { setSelectedCommand(command.name); onItemSelect?.(); if (isMobile) { setSidebarOpen(false); } }} onReset={() => handleResetCommand(command)} onDuplicate={() => handleDuplicateCommand(command)} /> ))} )} {customCommands.length > 0 && ( <>
Custom Commands
{[...customCommands].sort((a, b) => a.name.localeCompare(b.name)).map((command) => ( { setSelectedCommand(command.name); onItemSelect?.(); if (isMobile) { setSidebarOpen(false); } }} onRename={() => handleOpenRenameDialog(command)} onDelete={() => handleDeleteCommand(command)} onDuplicate={() => handleDuplicateCommand(command)} /> ))} )} )}
{/* Rename Dialog */} !open && setRenameDialogCommand(null)}> Rename Command Enter a new name for the command "/{renameDialogCommand?.name}" setRenameNewName(e.target.value)} placeholder="New command name..." className="text-foreground placeholder:text-muted-foreground" onKeyDown={(e) => { if (e.key === 'Enter') { handleRenameCommand(); } }} /> Rename
); }; interface CommandListItemProps { command: Command; isSelected: boolean; onSelect: () => void; onDelete?: () => void; onReset?: () => void; onRename?: () => void; onDuplicate: () => void; } const CommandListItem: React.FC = ({ command, isSelected, onSelect, onDelete, onReset, onRename, onDuplicate, }) => { return (
{onRename && ( { e.stopPropagation(); onRename(); }} > Rename )} { e.stopPropagation(); onDuplicate(); }} > Duplicate {onReset && ( { e.stopPropagation(); onReset(); }} > Reset )} {onDelete && ( { e.stopPropagation(); onDelete(); }} className="text-destructive focus:text-destructive" > Delete )}
); };