import React, { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Sparkles, Trash2, ExternalLink } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { useInstalledSkills, useToggleSkillApp, useUninstallSkill, useScanUnmanagedSkills, useImportSkillsFromApps, type InstalledSkill, type AppType, } from "@/hooks/useSkills"; import { ConfirmDialog } from "@/components/ConfirmDialog"; import { settingsApi } from "@/lib/api"; import { toast } from "sonner"; interface UnifiedSkillsPanelProps { onOpenDiscovery: () => void; } /** * 统一 Skills 管理面板 * v3.10.0 新架构:所有 Skills 统一管理,每个 Skill 通过开关控制应用到哪些客户端 */ export interface UnifiedSkillsPanelHandle { openDiscovery: () => void; openImport: () => void; } const UnifiedSkillsPanel = React.forwardRef< UnifiedSkillsPanelHandle, UnifiedSkillsPanelProps >(({ onOpenDiscovery }, ref) => { const { t } = useTranslation(); const [confirmDialog, setConfirmDialog] = useState<{ isOpen: boolean; title: string; message: string; onConfirm: () => void; } | null>(null); const [importDialogOpen, setImportDialogOpen] = useState(false); // Queries and Mutations const { data: skills, isLoading } = useInstalledSkills(); const toggleAppMutation = useToggleSkillApp(); const uninstallMutation = useUninstallSkill(); const { data: unmanagedSkills, refetch: scanUnmanaged } = useScanUnmanagedSkills(); const importMutation = useImportSkillsFromApps(); // Count enabled skills per app const enabledCounts = useMemo(() => { const counts = { claude: 0, codex: 0, gemini: 0, opencode: 0 }; if (!skills) return counts; skills.forEach((skill) => { if (skill.apps.claude) counts.claude++; if (skill.apps.codex) counts.codex++; if (skill.apps.gemini) counts.gemini++; if (skill.apps.opencode) counts.opencode++; }); return counts; }, [skills]); const handleToggleApp = async ( id: string, app: AppType, enabled: boolean, ) => { try { await toggleAppMutation.mutateAsync({ id, app, enabled }); } catch (error) { toast.error(t("common.error"), { description: String(error), }); } }; const handleUninstall = (skill: InstalledSkill) => { setConfirmDialog({ isOpen: true, title: t("skills.uninstall"), message: t("skills.uninstallConfirm", { name: skill.name }), onConfirm: async () => { try { await uninstallMutation.mutateAsync(skill.id); setConfirmDialog(null); toast.success(t("skills.uninstallSuccess", { name: skill.name }), { closeButton: true, }); } catch (error) { toast.error(t("common.error"), { description: String(error), }); } }, }); }; const handleOpenImport = async () => { try { const result = await scanUnmanaged(); if (!result.data || result.data.length === 0) { toast.success(t("skills.noUnmanagedFound"), { closeButton: true }); return; } setImportDialogOpen(true); } catch (error) { toast.error(t("common.error"), { description: String(error), }); } }; const handleImport = async (directories: string[]) => { try { const imported = await importMutation.mutateAsync(directories); setImportDialogOpen(false); toast.success(t("skills.importSuccess", { count: imported.length }), { closeButton: true, }); } catch (error) { toast.error(t("common.error"), { description: String(error), }); } }; React.useImperativeHandle(ref, () => ({ openDiscovery: onOpenDiscovery, openImport: handleOpenImport, })); return (
{/* Info Section */}
{t("skills.installed", { count: skills?.length || 0 })} ·{" "} {t("skills.apps.claude")}: {enabledCounts.claude} ·{" "} {t("skills.apps.codex")}: {enabledCounts.codex} ·{" "} {t("skills.apps.gemini")}: {enabledCounts.gemini} ·{" "} {t("skills.apps.opencode")}: {enabledCounts.opencode}
{/* Content - Scrollable */}
{isLoading ? (
{t("skills.loading")}
) : !skills || skills.length === 0 ? (

{t("skills.noInstalled")}

{t("skills.noInstalledDescription")}

) : (
{skills.map((skill) => ( handleUninstall(skill)} /> ))}
)}
{/* Confirm Dialog */} {confirmDialog && ( setConfirmDialog(null)} /> )} {/* Import Dialog */} {importDialogOpen && unmanagedSkills && ( setImportDialogOpen(false)} /> )}
); }); UnifiedSkillsPanel.displayName = "UnifiedSkillsPanel"; /** * 已安装 Skill 列表项组件 */ interface InstalledSkillListItemProps { skill: InstalledSkill; onToggleApp: (id: string, app: AppType, enabled: boolean) => void; onUninstall: () => void; } const InstalledSkillListItem: React.FC = ({ skill, onToggleApp, onUninstall, }) => { const { t } = useTranslation(); const openDocs = async () => { if (!skill.readmeUrl) return; try { await settingsApi.openExternal(skill.readmeUrl); } catch { // ignore } }; // 生成来源标签 const sourceLabel = useMemo(() => { if (skill.repoOwner && skill.repoName) { return `${skill.repoOwner}/${skill.repoName}`; } return t("skills.local"); }, [skill.repoOwner, skill.repoName, t]); return (
{/* 左侧:Skill 信息 */}

{skill.name}

{skill.readmeUrl && ( )}
{skill.description && (

{skill.description}

)}

{sourceLabel}

{/* 中间:应用开关 */}
onToggleApp(skill.id, "claude", checked) } />
onToggleApp(skill.id, "codex", checked) } />
onToggleApp(skill.id, "gemini", checked) } />
onToggleApp(skill.id, "opencode", checked) } />
{/* 右侧:删除按钮 */}
); }; /** * 导入 Skills 对话框 */ interface ImportSkillsDialogProps { skills: Array<{ directory: string; name: string; description?: string; foundIn: string[]; }>; onImport: (directories: string[]) => void; onClose: () => void; } const ImportSkillsDialog: React.FC = ({ skills, onImport, onClose, }) => { const { t } = useTranslation(); const [selected, setSelected] = useState>( new Set(skills.map((s) => s.directory)), ); const toggleSelect = (directory: string) => { const newSelected = new Set(selected); if (newSelected.has(directory)) { newSelected.delete(directory); } else { newSelected.add(directory); } setSelected(newSelected); }; const handleImport = () => { onImport(Array.from(selected)); }; return (

{t("skills.import")}

{t("skills.importDescription")}

{skills.map((skill) => ( ))}
); }; export default UnifiedSkillsPanel;