import { useEffect, useMemo, useRef, useState } from 'react'; import { api } from '@renderer/api'; import { Badge } from '@renderer/components/ui/badge'; import { Button } from '@renderer/components/ui/button'; import { Popover, PopoverContent, PopoverTrigger } from '@renderer/components/ui/popover'; import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'; import { useStore } from '@renderer/store'; import { useShallow } from 'zustand/react/shallow'; import { AlertTriangle, ArrowUpAZ, ArrowUpDown, BookOpen, Check, CheckCircle2, Clock3, Download, Plus, Search, } from 'lucide-react'; import { SearchInput } from '../common/SearchInput'; import { SkillDetailDialog } from './SkillDetailDialog'; import { SkillEditorDialog } from './SkillEditorDialog'; import { SkillImportDialog } from './SkillImportDialog'; import type { SkillsSortState } from '@renderer/hooks/useExtensionsTabState'; import type { SkillCatalogItem, SkillDetail } from '@shared/types/extensions'; const SUCCESS_BANNER_MS = 2500; const NEW_SKILL_HIGHLIGHT_MS = 4000; const USER_SKILLS_CATALOG_KEY = '__user__'; type SkillsQuickFilter = 'all' | 'project' | 'personal' | 'needs-attention' | 'has-scripts'; interface SkillsPanelProps { projectPath: string | null; projectLabel: string | null; skillsSearchQuery: string; setSkillsSearchQuery: (value: string) => void; skillsSort: SkillsSortState; setSkillsSort: (value: SkillsSortState) => void; selectedSkillId: string | null; setSelectedSkillId: (id: string | null) => void; } function sortSkills(skills: SkillCatalogItem[], sort: SkillsSortState): SkillCatalogItem[] { const next = [...skills]; next.sort((a, b) => { if (sort === 'recent-desc') { return b.modifiedAt - a.modifiedAt || a.name.localeCompare(b.name); } return a.name.localeCompare(b.name) || b.modifiedAt - a.modifiedAt; }); return next; } function formatRootKind(rootKind: SkillCatalogItem['rootKind']): string { return `.${rootKind}`; } function getScopeLabel(skill: SkillCatalogItem): string { return skill.scope === 'project' ? 'This project' : 'Personal'; } function getInvocationLabel(skill: SkillCatalogItem): string { return skill.invocationMode === 'manual-only' ? 'Only runs when you explicitly ask for it' : 'Claude can use this automatically when it fits'; } function getSkillStatus(skill: SkillCatalogItem): string { if (!skill.isValid) { return 'Needs attention before you rely on it'; } if (skill.flags.hasScripts) { return 'Includes scripts, so review it carefully'; } return 'Ready to use'; } export const SkillsPanel = ({ projectPath, projectLabel, skillsSearchQuery, setSkillsSearchQuery, skillsSort, setSkillsSort, selectedSkillId, setSelectedSkillId, }: SkillsPanelProps): React.JSX.Element => { const catalogKey = projectPath ?? USER_SKILLS_CATALOG_KEY; const fetchSkillsCatalog = useStore((s) => s.fetchSkillsCatalog); const fetchSkillDetail = useStore((s) => s.fetchSkillDetail); const skillsLoading = useStore((s) => s.skillsCatalogLoadingByProjectPath[catalogKey] ?? false); const skillsError = useStore((s) => s.skillsCatalogErrorByProjectPath[catalogKey] ?? null); const detailById = useStore(useShallow((s) => s.skillsDetailsById)); const userSkills = useStore(useShallow((s) => s.skillsUserCatalog)); const projectSkills = useStore( useShallow((s) => (projectPath ? (s.skillsProjectCatalogByProjectPath[projectPath] ?? []) : [])) ); const [createOpen, setCreateOpen] = useState(false); const [editOpen, setEditOpen] = useState(false); const [editingDetail, setEditingDetail] = useState(null); const [importOpen, setImportOpen] = useState(false); const [sortMenuOpen, setSortMenuOpen] = useState(false); const [quickFilter, setQuickFilter] = useState('all'); const [successMessage, setSuccessMessage] = useState(null); const [highlightedSkillId, setHighlightedSkillId] = useState(null); const selectedSkillIdRef = useRef(selectedSkillId); selectedSkillIdRef.current = selectedSkillId; const mergedSkills = useMemo( () => [...projectSkills, ...userSkills], [projectSkills, userSkills] ); const selectedDetail = selectedSkillId ? (detailById[selectedSkillId] ?? null) : null; useEffect(() => { if (!selectedSkillId) return; if (mergedSkills.some((skill) => skill.id === selectedSkillId)) return; setSelectedSkillId(null); }, [mergedSkills, selectedSkillId, setSelectedSkillId]); useEffect(() => { if (!successMessage) return; const timeoutId = window.setTimeout(() => setSuccessMessage(null), SUCCESS_BANNER_MS); return () => window.clearTimeout(timeoutId); }, [successMessage]); useEffect(() => { if (!highlightedSkillId) return; const timeoutId = window.setTimeout(() => setHighlightedSkillId(null), NEW_SKILL_HIGHLIGHT_MS); return () => window.clearTimeout(timeoutId); }, [highlightedSkillId]); useEffect(() => { const skillsApi = api.skills; if (!skillsApi) return; let watchId: string | null = null; let disposed = false; void skillsApi.startWatching(projectPath ?? undefined).then((id) => { if (disposed) { void skillsApi.stopWatching(id); return; } watchId = id; }); const changeCleanup = skillsApi.onChanged((event) => { const shouldRefresh = event.scope === 'user' || (event.scope === 'project' && event.projectPath === (projectPath ?? null)); if (!shouldRefresh) return; void fetchSkillsCatalog(projectPath ?? undefined); if (selectedSkillIdRef.current) { void fetchSkillDetail(selectedSkillIdRef.current, projectPath ?? undefined).catch( () => undefined ); } }); return () => { disposed = true; changeCleanup(); if (watchId) { void skillsApi.stopWatching(watchId); } }; }, [fetchSkillDetail, fetchSkillsCatalog, projectPath]); const visibleSkills = useMemo(() => { const q = skillsSearchQuery.trim().toLowerCase(); const filteredByQuery = q ? mergedSkills.filter( (skill) => skill.name.toLowerCase().includes(q) || skill.description.toLowerCase().includes(q) || skill.folderName.toLowerCase().includes(q) ) : mergedSkills; const filtered = quickFilter === 'all' ? filteredByQuery : filteredByQuery.filter((skill) => { switch (quickFilter) { case 'project': return skill.scope === 'project'; case 'personal': return skill.scope === 'user'; case 'needs-attention': return !skill.isValid; case 'has-scripts': return skill.flags.hasScripts; default: return true; } }); return sortSkills(filtered, skillsSort); }, [mergedSkills, quickFilter, skillsSearchQuery, skillsSort]); const visibleProjectSkills = useMemo( () => visibleSkills.filter((skill) => skill.scope === 'project'), [visibleSkills] ); const visibleUserSkills = useMemo( () => visibleSkills.filter((skill) => skill.scope === 'user'), [visibleSkills] ); const isRefreshing = skillsLoading && mergedSkills.length > 0; return (

Teach Claude repeatable work

Skills are reusable instructions that help Claude handle the same kind of task more consistently.{' '} {projectPath ? `You are seeing skills for ${projectLabel ?? projectPath} plus your personal skills.` : 'You are seeing only your personal skills right now.'}

Use personal skills for habits you want everywhere. Use project skills for workflows that only make sense inside one codebase.

Sort skills
{mergedSkills.length} total {projectSkills.length} project {userSkills.length} personal
{( [ ['all', 'All skills'], ['project', 'Project'], ['personal', 'Personal'], ['needs-attention', 'Needs attention'], ['has-scripts', 'Has scripts'], ] as [SkillsQuickFilter, string][] ).map(([value, label]) => ( ))}
{skillsError && (
{skillsError}
)} {successMessage && (
{successMessage}
)} {isRefreshing && (
Refreshing skills...
)} {skillsLoading && visibleSkills.length === 0 && (
Loading skills...
)} {!skillsLoading && !skillsError && visibleSkills.length === 0 && (

{skillsSearchQuery ? 'No skills match your search' : 'No skills yet'}

{skillsSearchQuery ? 'Try a different search term or switch filters.' : 'Create your first skill to teach Claude a repeatable workflow, or import one you already use.'}

)} {visibleSkills.length > 0 && (
{visibleProjectSkills.length > 0 && (

Project skills

Workflows that only make sense for this codebase.

{visibleProjectSkills.length}
{visibleProjectSkills.map((skill) => ( ))}
)} {visibleUserSkills.length > 0 && (

Personal skills

Habits and instructions you want Claude to remember everywhere.

{visibleUserSkills.length}
{visibleUserSkills.map((skill) => ( ))}
)}
)} setSelectedSkillId(null)} projectPath={projectPath} onEdit={() => { if (!selectedDetail) return; setEditingDetail(selectedDetail); setSelectedSkillId(null); setEditOpen(true); }} onDeleted={() => setSelectedSkillId(null)} /> setCreateOpen(false)} onSaved={(skillId) => { setCreateOpen(false); setSuccessMessage('Skill created successfully.'); setHighlightedSkillId(skillId); setSelectedSkillId(null); }} /> { setEditOpen(false); setEditingDetail(null); }} onSaved={(skillId) => { setEditOpen(false); setEditingDetail(null); setSuccessMessage('Skill saved successfully.'); setSelectedSkillId(skillId); }} /> setImportOpen(false)} onImported={(skillId) => { setImportOpen(false); setSuccessMessage('Skill imported successfully.'); setSelectedSkillId(skillId); }} />
); };