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, RiDeleteBinLine, RiFileCopyLine, RiMore2Line, RiEditLine, RiBookOpenLine } from '@remixicon/react'; import { useSkillsStore, type DiscoveredSkill } from '@/stores/useSkillsStore'; import { useUIStore } from '@/stores/useUIStore'; import { useDeviceInfo } from '@/lib/device'; import { isVSCodeRuntime } from '@/lib/desktop'; import { cn } from '@/lib/utils'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; interface SkillsSidebarProps { onItemSelect?: () => void; } export const SkillsSidebar: React.FC = ({ onItemSelect }) => { const [renameDialogSkill, setRenameDialogSkill] = React.useState(null); const [renameNewName, setRenameNewName] = React.useState(''); const [deleteDialogSkill, setDeleteDialogSkill] = React.useState(null); const [isDeletePending, setIsDeletePending] = React.useState(false); const { selectedSkillName, skills, setSelectedSkill, setSkillDraft, createSkill, deleteSkill, loadSkills, getSkillDetail, } = useSkillsStore(); const { setSidebarOpen } = useUIStore(); const { isMobile } = useDeviceInfo(); const isVSCode = React.useMemo(() => isVSCodeRuntime(), []); React.useEffect(() => { loadSkills(); }, [loadSkills]); const bgClass = isVSCode ? 'bg-background' : 'bg-sidebar'; const handleCreateNew = () => { // Generate unique name const baseName = 'new-skill'; let newName = baseName; let counter = 1; while (skills.some((s) => s.name === newName)) { newName = `${baseName}-${counter}`; counter++; } // Set draft and open the page for editing setSkillDraft({ name: newName, scope: 'user', description: '' }); setSelectedSkill(newName); onItemSelect?.(); if (isMobile) { setSidebarOpen(false); } }; const handleDeleteSkill = async (skill: DiscoveredSkill) => { setDeleteDialogSkill(skill); }; const handleConfirmDeleteSkill = async () => { if (!deleteDialogSkill) { return; } setIsDeletePending(true); const success = await deleteSkill(deleteDialogSkill.name); if (success) { toast.success(`Skill "${deleteDialogSkill.name}" deleted successfully`); setDeleteDialogSkill(null); } else { toast.error('Failed to delete skill'); } setIsDeletePending(false); }; const handleDuplicateSkill = async (skill: DiscoveredSkill) => { const baseName = skill.name; let copyNumber = 1; let newName = `${baseName}-copy`; while (skills.some((s) => s.name === newName)) { copyNumber++; newName = `${baseName}-copy-${copyNumber}`; } // Get full skill detail to copy const detail = await getSkillDetail(skill.name); if (!detail) { toast.error('Failed to load skill details for duplication'); return; } // Set draft with prefilled values from source skill setSkillDraft({ name: newName, scope: skill.scope || 'user', description: detail.sources.md.fields.includes('description') ? '' : '', // Will be populated from page instructions: '', }); setSelectedSkill(newName); if (isMobile) { setSidebarOpen(false); } }; const handleOpenRenameDialog = (skill: DiscoveredSkill) => { setRenameNewName(skill.name); setRenameDialogSkill(skill); }; const handleRenameSkill = async () => { if (!renameDialogSkill) return; const sanitizedName = renameNewName.trim().replace(/\s+/g, '-').toLowerCase(); if (!sanitizedName) { toast.error('Skill name is required'); return; } if (sanitizedName === renameDialogSkill.name) { setRenameDialogSkill(null); return; } if (skills.some((s) => s.name === sanitizedName)) { toast.error('A skill with this name already exists'); return; } // Get full detail to copy const detail = await getSkillDetail(renameDialogSkill.name); if (!detail) { toast.error('Failed to load skill details'); setRenameDialogSkill(null); return; } // Create new skill with new name const success = await createSkill({ name: sanitizedName, description: 'Renamed skill', // Will need proper description scope: renameDialogSkill.scope, }); if (success) { // Delete old skill const deleteSuccess = await deleteSkill(renameDialogSkill.name); if (deleteSuccess) { toast.success(`Skill renamed to "${sanitizedName}"`); setSelectedSkill(sanitizedName); } else { toast.error('Failed to remove old skill after rename'); } } else { toast.error('Failed to rename skill'); } setRenameDialogSkill(null); }; // Separate project and user skills const projectSkills = skills.filter((s) => s.scope === 'project'); const userSkills = skills.filter((s) => s.scope === 'user'); return (
Total {skills.length}
{skills.length === 0 ? (

No skills configured

Use the + button above to create one

) : ( <> {projectSkills.length > 0 && ( <>
Project Skills
{projectSkills.map((skill) => ( { setSelectedSkill(skill.name); onItemSelect?.(); if (isMobile) { setSidebarOpen(false); } }} onRename={() => handleOpenRenameDialog(skill)} onDelete={() => handleDeleteSkill(skill)} onDuplicate={() => handleDuplicateSkill(skill)} /> ))} )} {userSkills.length > 0 && ( <>
User Skills
{userSkills.map((skill) => ( { setSelectedSkill(skill.name); onItemSelect?.(); if (isMobile) { setSidebarOpen(false); } }} onRename={() => handleOpenRenameDialog(skill)} onDelete={() => handleDeleteSkill(skill)} onDuplicate={() => handleDuplicateSkill(skill)} /> ))} )} )}
{ if (!open && !isDeletePending) { setDeleteDialogSkill(null); } }} > Delete Skill Are you sure you want to delete skill "{deleteDialogSkill?.name}"? Delete {/* Rename Dialog */} !open && setRenameDialogSkill(null)}> Rename Skill Enter a new name for the skill "{renameDialogSkill?.name}" setRenameNewName(e.target.value)} placeholder="New skill name..." className="text-foreground placeholder:text-muted-foreground" onKeyDown={(e) => { if (e.key === 'Enter') { handleRenameSkill(); } }} /> Rename
); }; interface SkillListItemProps { skill: DiscoveredSkill; isSelected: boolean; onSelect: () => void; onDelete: () => void; onRename: () => void; onDuplicate: () => void; } const SkillListItem: React.FC = ({ skill, isSelected, onSelect, onDelete, onRename, onDuplicate, }) => { return (
{ e.stopPropagation(); onRename(); }} > Rename { e.stopPropagation(); onDuplicate(); }} > Duplicate { e.stopPropagation(); onDelete(); }} className="text-destructive focus:text-destructive" > Delete
); };