import { useEffect, useState } from 'react'; import { api } from '@renderer/api'; import { Button } from '@renderer/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@renderer/components/ui/dialog'; import { Input } from '@renderer/components/ui/input'; import { Label } from '@renderer/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@renderer/components/ui/select'; import { useStore } from '@renderer/store'; import { FileSearch, FolderOpen, X } from 'lucide-react'; import { getSuggestedSkillFolderNameFromPath } from './skillFolderNameUtils'; import { SkillReviewDialog } from './SkillReviewDialog'; import { resolveSkillProjectPath } from './skillProjectUtils'; import { validateSkillFolderName, validateSkillImportSourceDir } from './skillValidationUtils'; import type { SkillReviewPreview } from '@shared/types/extensions'; function getFriendlyImportError(message: string): string { if (message.includes('valid skill file')) { return 'This folder does not look like a skill yet. It needs a SKILL.md, Skill.md, or skill.md file.'; } if (message.includes('symbolic links')) { return 'This folder contains symbolic links. Import the real files instead of links.'; } if (message.includes('too many files')) { return 'This skill folder is too large to import at once. Remove extra files and try again.'; } if (message.includes('too large')) { return 'This skill folder is too large to import safely. Trim large assets and try again.'; } if (message.includes('Invalid folder name')) { return 'Pick a simpler destination folder name using letters, numbers, dots, dashes, or underscores.'; } if (message.includes('must be a directory')) { return 'Choose a folder to import, not a single file.'; } return message; } interface SkillImportDialogProps { open: boolean; projectPath: string | null; projectLabel: string | null; onClose: () => void; onImported: (skillId: string | null) => void; } export const SkillImportDialog = ({ open, projectPath, projectLabel, onClose, onImported, }: SkillImportDialogProps): React.JSX.Element => { const previewSkillImport = useStore((s) => s.previewSkillImport); const applySkillImport = useStore((s) => s.applySkillImport); const [sourceDir, setSourceDir] = useState(''); const [folderName, setFolderName] = useState(''); const [folderNameEdited, setFolderNameEdited] = useState(false); const [scope, setScope] = useState<'user' | 'project'>('user'); const [rootKind, setRootKind] = useState<'claude' | 'cursor' | 'agents'>('claude'); const [preview, setPreview] = useState(null); const [reviewOpen, setReviewOpen] = useState(false); const [reviewLoading, setReviewLoading] = useState(false); const [importLoading, setImportLoading] = useState(false); const [mutationError, setMutationError] = useState(null); useEffect(() => { if (!open) return; setSourceDir(''); setFolderName(''); setFolderNameEdited(false); setScope(projectPath ? 'project' : 'user'); setRootKind('claude'); setPreview(null); setReviewOpen(false); setReviewLoading(false); setImportLoading(false); setMutationError(null); }, [open, projectPath]); useEffect(() => { if (open) { return; } setPreview(null); setReviewOpen(false); setReviewLoading(false); setImportLoading(false); setMutationError(null); }, [open]); useEffect(() => { if (!open || folderNameEdited) { return; } setFolderName(sourceDir.trim() ? getSuggestedSkillFolderNameFromPath(sourceDir) : ''); }, [folderNameEdited, open, sourceDir]); useEffect(() => { if (open && scope === 'project' && !projectPath) { setScope('user'); } }, [open, projectPath, scope]); async function handleChooseFolder(): Promise { const selected = await api.config.selectFolders(); const first = selected[0]; if (!first) return; setSourceDir(first); } async function handleReview(): Promise { const normalizedSourceDir = sourceDir.trim(); const normalizedFolderName = folderName.trim(); const sourceDirError = validateSkillImportSourceDir(sourceDir); if (sourceDirError) { setMutationError(sourceDirError); return; } const folderNameError = normalizedFolderName.length > 0 ? validateSkillFolderName(normalizedFolderName) : null; if (folderNameError) { setMutationError(folderNameError); return; } setReviewLoading(true); setMutationError(null); try { const nextPreview = await previewSkillImport({ sourceDir: normalizedSourceDir, folderName: normalizedFolderName || undefined, scope, rootKind, projectPath: resolveSkillProjectPath(scope, projectPath), }); setPreview(nextPreview); setReviewOpen(true); } catch (error) { setMutationError( getFriendlyImportError( error instanceof Error ? error.message : 'Failed to review import changes' ) ); } finally { setReviewLoading(false); } } async function handleConfirmImport(): Promise { const normalizedSourceDir = sourceDir.trim(); const normalizedFolderName = folderName.trim(); setImportLoading(true); setMutationError(null); try { const detail = await applySkillImport({ sourceDir: normalizedSourceDir, folderName: normalizedFolderName || undefined, scope, rootKind, projectPath: resolveSkillProjectPath(scope, projectPath), reviewPlanId: preview?.planId, }); setReviewOpen(false); onImported(detail?.item.id ?? null); onClose(); } catch (error) { setMutationError( getFriendlyImportError(error instanceof Error ? error.message : 'Failed to import skill') ); } finally { setImportLoading(false); } } return ( <> !next && onClose()}>
Import skill Pick an existing skill folder, review what will be copied, then import it into one of your supported skill locations.

1. Choose a skill folder

This should be a folder that already contains a `SKILL.md`, `Skill.md`, or `skill.md` file.

setSourceDir(event.target.value)} />
{ setFolderNameEdited(true); setFolderName(event.target.value); }} placeholder="Defaults to source folder name" />

2. Decide where it belongs

Personal skills work everywhere. Project skills only show up for one codebase.

{mutationError && (
{mutationError}
)}

Review the copied files first, then confirm the import in the next step.

setReviewOpen(false)} onConfirm={() => void handleConfirmImport()} confirmLabel="Import Skill" reviewLabel="Importing this skill" backLabel="Back To Import" /> ); };