import { useEffect, useLayoutEffect, useState, useMemo, useCallback } from "react"; import { Package, Search, Wrench, X, Cpu, Globe, Shield, ShieldCheck, ShieldAlert, ShieldQuestion, Eye, Paintbrush, Brain, Blocks, Code, Zap, Filter, Download, RefreshCw, FileText, ExternalLink, CheckCircle2, AlertTriangle, Sparkles, Loader2, } from "lucide-react"; import { api } from "@/lib/api"; import type { SkillInfo, ToolsetInfo, SkillHubResult, SkillHubSource, SkillHubInstalledEntry, SkillHubPreview, SkillHubScan, } from "@/lib/api"; import { ToolsetConfigDrawer } from "@/components/ToolsetConfigDrawer"; import { useToast } from "@nous-research/ui/hooks/use-toast"; import { Toast } from "@nous-research/ui/ui/components/toast"; import { Card, CardContent, CardHeader, CardTitle } from "@nous-research/ui/ui/components/card"; import { Badge } from "@nous-research/ui/ui/components/badge"; import { Button } from "@nous-research/ui/ui/components/button"; import { ListItem } from "@nous-research/ui/ui/components/list-item"; import { Spinner } from "@nous-research/ui/ui/components/spinner"; import { Switch } from "@nous-research/ui/ui/components/switch"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@nous-research/ui/ui/components/dialog"; import { cn } from "@/lib/utils"; import { Input } from "@nous-research/ui/ui/components/input"; import { useI18n } from "@/i18n"; import { usePageHeader } from "@/contexts/usePageHeader"; import { PluginSlot } from "@/plugins"; /* ------------------------------------------------------------------ */ /* Types & helpers */ /* ------------------------------------------------------------------ */ const CATEGORY_LABELS: Record = { mlops: "MLOps", "mlops/cloud": "MLOps / Cloud", "mlops/evaluation": "MLOps / Evaluation", "mlops/inference": "MLOps / Inference", "mlops/models": "MLOps / Models", "mlops/training": "MLOps / Training", "mlops/vector-databases": "MLOps / Vector DBs", mcp: "MCP", "red-teaming": "Red Teaming", ocr: "OCR", p5js: "p5.js", ai: "AI", ux: "UX", ui: "UI", }; function prettyCategory( raw: string | null | undefined, generalLabel: string, ): string { if (!raw) return generalLabel; if (CATEGORY_LABELS[raw]) return CATEGORY_LABELS[raw]; return raw .split(/[-_/]/) .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) .join(" "); } const TOOLSET_ICONS: Record< string, React.ComponentType<{ className?: string }> > = { computer: Cpu, web: Globe, security: Shield, vision: Eye, design: Paintbrush, ai: Brain, integration: Blocks, code: Code, automation: Zap, }; function toolsetIcon( name: string, ): React.ComponentType<{ className?: string }> { const lower = name.toLowerCase(); for (const [key, icon] of Object.entries(TOOLSET_ICONS)) { if (lower.includes(key)) return icon; } return Wrench; } /* ------------------------------------------------------------------ */ /* Component */ /* ------------------------------------------------------------------ */ export default function SkillsPage() { const [skills, setSkills] = useState([]); const [toolsets, setToolsets] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(""); const [view, setView] = useState<"skills" | "toolsets" | "hub">("skills"); const [activeCategory, setActiveCategory] = useState(null); const [togglingSkills, setTogglingSkills] = useState>(new Set()); const [configToolset, setConfigToolset] = useState(null); const { toast, showToast } = useToast(); const { t } = useI18n(); const { setAfterTitle, setEnd } = usePageHeader(); useEffect(() => { Promise.all([api.getSkills(), api.getToolsets()]) .then(([s, tsets]) => { setSkills(s); setToolsets(tsets); }) .catch(() => showToast(t.common.loading, "error")) .finally(() => setLoading(false)); }, []); /* ---- Toggle skill ---- */ const handleToggleSkill = async (skill: SkillInfo) => { setTogglingSkills((prev) => new Set(prev).add(skill.name)); try { await api.toggleSkill(skill.name, !skill.enabled); setSkills((prev) => prev.map((s) => s.name === skill.name ? { ...s, enabled: !s.enabled } : s, ), ); showToast( `${skill.name} ${skill.enabled ? t.common.disabled : t.common.enabled}`, "success", ); } catch { showToast(`${t.common.failedToToggle} ${skill.name}`, "error"); } finally { setTogglingSkills((prev) => { const next = new Set(prev); next.delete(skill.name); return next; }); } }; /* ---- Refresh toolsets after a config change ---- */ const refreshToolsets = async () => { try { const tsets = await api.getToolsets(); setToolsets(tsets); } catch { /* non-fatal: the drawer already toasted on the failing write */ } }; /* ---- Derived data ---- */ const lowerSearch = search.toLowerCase(); const isSearching = search.trim().length > 0; const searchMatchedSkills = useMemo(() => { if (!isSearching) return []; return skills.filter( (s) => s.name.toLowerCase().includes(lowerSearch) || s.description.toLowerCase().includes(lowerSearch) || (s.category ?? "").toLowerCase().includes(lowerSearch), ); }, [skills, isSearching, lowerSearch]); const activeSkills = useMemo(() => { if (isSearching) return []; if (!activeCategory) return [...skills].sort((a, b) => a.name.localeCompare(b.name)); return skills .filter((s) => activeCategory === "__none__" ? !s.category : s.category === activeCategory, ) .sort((a, b) => a.name.localeCompare(b.name)); }, [skills, activeCategory, isSearching]); const allCategories = useMemo(() => { const cats = new Map(); for (const s of skills) { const key = s.category || "__none__"; cats.set(key, (cats.get(key) || 0) + 1); } return [...cats.entries()] .sort((a, b) => { if (a[0] === "__none__") return -1; if (b[0] === "__none__") return 1; return a[0].localeCompare(b[0]); }) .map(([key, count]) => ({ key, name: prettyCategory(key === "__none__" ? null : key, t.common.general), count, })); }, [skills, t]); const enabledCount = skills.filter((s) => s.enabled).length; useLayoutEffect(() => { if (loading) { setAfterTitle(null); setEnd(null); return; } setAfterTitle( {t.skills.enabledOf .replace("{enabled}", String(enabledCount)) .replace("{total}", String(skills.length))} , ); setEnd(
setSearch(e.target.value)} /> {search && ( )}
, ); return () => { setAfterTitle(null); setEnd(null); }; }, [enabledCount, loading, search, setAfterTitle, setEnd, skills.length, t]); const filteredToolsets = useMemo(() => { return toolsets.filter( (ts) => !search || ts.name.toLowerCase().includes(lowerSearch) || ts.label.toLowerCase().includes(lowerSearch) || ts.description.toLowerCase().includes(lowerSearch), ); }, [toolsets, search, lowerSearch]); /* ---- Loading ---- */ if (loading) { return (
); } return (
{isSearching ? (
{t.skills.title} {t.skills.resultCount .replace("{count}", String(searchMatchedSkills.length)) .replace( "{s}", searchMatchedSkills.length !== 1 ? "s" : "", )}
{searchMatchedSkills.length === 0 ? (

{t.skills.noSkillsMatch}

) : (
{searchMatchedSkills.map((skill) => ( handleToggleSkill(skill)} noDescriptionLabel={t.skills.noDescription} /> ))}
)}
) : view === "skills" ? ( /* Skills list */
{activeCategory ? prettyCategory( activeCategory === "__none__" ? null : activeCategory, t.common.general, ) : t.skills.all} {t.skills.skillCount .replace("{count}", String(activeSkills.length)) .replace("{s}", activeSkills.length !== 1 ? "s" : "")}
{activeSkills.length === 0 ? (

{skills.length === 0 ? t.skills.noSkills : t.skills.noSkillsMatch}

) : (
{activeSkills.map((skill) => ( handleToggleSkill(skill)} noDescriptionLabel={t.skills.noDescription} /> ))}
)}
) : view === "toolsets" ? ( /* Toolsets grid */ <> {filteredToolsets.length === 0 ? ( {t.skills.noToolsetsMatch} ) : (
{filteredToolsets.map((ts) => { const TsIcon = toolsetIcon(ts.name); const labelText = ts.label.trim() || ts.name; return (
{labelText} {ts.enabled ? t.common.active : t.common.inactive}

{ts.description}

{ts.enabled && !ts.configured && (

{t.skills.setupNeeded}

)} {ts.tools.length > 0 && (
{ts.tools.map((tool) => ( {tool} ))}
)} {ts.tools.length === 0 && ( {ts.enabled ? t.skills.toolsetLabel.replace( "{name}", ts.name, ) : t.skills.disabledForCli} )}
); })}
)} ) : ( )}
{configToolset && ( setConfigToolset(null)} onChanged={() => void refreshToolsets()} /> )}
); } function SkillRow({ skill, toggling, onToggle, noDescriptionLabel, }: SkillRowProps) { return (
{skill.name}

{skill.description || noDescriptionLabel}

); } function PanelItem({ active, icon: Icon, label, onClick }: PanelItemProps) { return ( {label} ); } interface PanelItemProps { active: boolean; icon: React.ComponentType<{ className?: string }>; label: string; onClick: () => void; } interface SkillRowProps { noDescriptionLabel: string; onToggle: () => void; skill: SkillInfo; toggling: boolean; } /* ------------------------------------------------------------------ */ /* Hub browser — search the skill hub, preview, scan, install */ /* ------------------------------------------------------------------ */ /** Map a trust level to a Badge tone + label + icon. */ function trustVisual(level: string): { tone: "success" | "secondary" | "warning" | "outline"; label: string; } { switch (level) { case "trusted": return { tone: "success", label: "trusted" }; case "builtin": return { tone: "secondary", label: "builtin" }; case "community": return { tone: "warning", label: "community" }; default: return { tone: "outline", label: level || "unknown" }; } } /** Map a scan verdict to tone + icon. */ function verdictVisual(verdict: string): { tone: "success" | "warning" | "destructive"; Icon: React.ComponentType<{ className?: string }>; label: string; } { switch (verdict) { case "safe": return { tone: "success", Icon: ShieldCheck, label: "Safe" }; case "caution": return { tone: "warning", Icon: ShieldAlert, label: "Caution" }; case "dangerous": return { tone: "destructive", Icon: ShieldAlert, label: "Dangerous" }; default: return { tone: "warning", Icon: ShieldQuestion, label: verdict }; } } const SEVERITY_TONE: Record = { critical: "destructive", high: "destructive", medium: "warning", low: "secondary", }; function HubBrowser({ showToast, }: { showToast: (msg: string, kind: "success" | "error") => void; }) { const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [searching, setSearching] = useState(false); const [searched, setSearched] = useState(false); const [sourceCounts, setSourceCounts] = useState>({}); const [timedOut, setTimedOut] = useState([]); const [searchMs, setSearchMs] = useState(null); // Landing state: which hubs are wired up + featured skills. const [sources, setSources] = useState([]); const [featured, setFeatured] = useState([]); const [sourcesLoading, setSourcesLoading] = useState(true); // identifier -> installed entry (drives "Installed" badges). const [installed, setInstalled] = useState>({}); // Live action log for the most recent install/update. const [action, setAction] = useState(null); const [actionLog, setActionLog] = useState([]); const [actionRunning, setActionRunning] = useState(false); // Detail dialog (preview + scan for a single skill). const [detail, setDetail] = useState(null); /* ---- Load connected hubs + featured skills on mount ---- */ useEffect(() => { let cancelled = false; api .getSkillHubSources() .then((r) => { if (cancelled) return; setSources(r.sources); setFeatured(r.featured); setInstalled(r.installed); }) .catch(() => { /* leave landing minimal on failure */ }) .finally(() => { if (!cancelled) setSourcesLoading(false); }); return () => { cancelled = true; }; }, []); /* ---- Search ---- */ const runSearch = useCallback(async () => { const q = query.trim(); if (!q) return; setSearching(true); setSearched(true); const t0 = performance.now(); try { const r = await api.searchSkillsHub(q); setResults(r.results); setSourceCounts(r.source_counts || {}); setTimedOut(r.timed_out || []); setInstalled((prev) => ({ ...prev, ...(r.installed || {}) })); } catch (e) { showToast(`Hub search failed: ${e}`, "error"); setResults([]); setSourceCounts({}); setTimedOut([]); } finally { setSearchMs(Math.round(performance.now() - t0)); setSearching(false); } }, [query, showToast]); /* ---- Poll a spawned action's log until it exits ---- */ useEffect(() => { if (!action) return; let cancelled = false; let timer: ReturnType | null = null; const poll = async () => { try { const st = await api.getActionStatus(action, 200); if (cancelled) return; setActionLog(st.lines); setActionRunning(st.running); if (st.running) { timer = setTimeout(poll, 1200); } else { // Install finished — refresh installed-state so badges update. api .getSkillHubSources() .then((r) => !cancelled && setInstalled(r.installed)) .catch(() => {}); } } catch { if (!cancelled) setActionRunning(false); } }; poll(); return () => { cancelled = true; if (timer) clearTimeout(timer); }; }, [action]); const install = useCallback( async (identifier: string) => { try { const res = await api.installSkillFromHub(identifier); showToast(`Installing ${identifier}…`, "success"); setActionLog([]); setActionRunning(true); setAction(res.name); setDetail(null); } catch (e) { showToast(`Install failed: ${e}`, "error"); } }, [showToast], ); const updateAll = useCallback(async () => { try { const res = await api.updateSkillsFromHub(); showToast("Updating installed skills…", "success"); setActionLog([]); setActionRunning(true); setAction(res.name); } catch (e) { showToast(`Update failed: ${e}`, "error"); } }, [showToast]); const isInstalled = useCallback( (identifier: string) => Boolean(installed[identifier]), [installed], ); const showLanding = !searched && !searching; return (
{/* ── Search bar ── */}
setQuery(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") void runSearch(); }} />
{/* Connected hubs strip — proves the tab is wired up. */}
{/* ── Install/update action log ── */} {action && (
{action} {actionRunning ? ( running ) : ( done )} {!actionRunning && ( )}
              {actionLog.length ? actionLog.join("\n") : "Starting…"}
            
)} {/* ── Landing: featured skills (before any search) ── */} {showLanding && ( <> {sourcesLoading ? (
) : featured.length > 0 ? (
Featured skills from the Hermes index — search above for thousands more
{featured.map((r) => ( setDetail(r)} onInstall={() => void install(r.identifier)} /> ))}
) : ( Search the hub above to browse installable skills from the connected sources. )} )} {/* ── Searching spinner ── */} {searching && (
)} {/* ── Search results ── */} {!searching && searched && ( <> {results.length === 0 ? ( No matching skills found in the hub. ) : ( results.map((r) => ( setDetail(r)} onInstall={() => void install(r.identifier)} /> )) )} )} {/* ── Detail dialog: preview + scan ── */} {detail && ( setDetail(null)} onInstall={() => void install(detail.identifier)} showToast={showToast} /> )}
); } /* ---- Connected hubs strip ---- */ function ConnectedHubs({ sources, loading, }: { sources: SkillHubSource[]; loading: boolean; }) { if (loading) { return (

Connecting to skill hubs…

); } if (sources.length === 0) { return (

Results come from the same sources as{" "} hermes skills search.

); } return (
Connected hubs: {sources.map((s) => { const down = (s.id === "hermes-index" && s.available === false) || (s.id === "github" && s.rate_limited === true); return ( {s.label} {s.id === "github" && s.rate_limited ? " (rate-limited)" : ""} ); })}
); } /* ---- Search result-count + per-source breakdown ---- */ function SearchMeta({ count, sourceCounts, timedOut, ms, }: { count: number; sourceCounts: Record; timedOut: string[]; ms: number | null; }) { const entries = Object.entries(sourceCounts).filter(([, n]) => n > 0); return (
{count} result{count !== 1 ? "s" : ""} {ms != null && {(ms / 1000).toFixed(1)}s} {entries.length > 0 && ( {entries.map(([sid, n]) => ( {sid}:{n} ))} )} {timedOut.length > 0 && ( {timedOut.join(", ")} timed out )}
); } /* ---- One result card ---- */ function HubResultCard({ result, installed, onOpen, onInstall, }: { result: SkillHubResult; installed: boolean; onOpen: () => void; onInstall: () => void; }) { const trust = trustVisual(result.trust_level); return (
{installed ? ( ) : ( )}
); } /* ---- Detail dialog: SKILL.md preview + on-demand security scan ---- */ function SkillDetailDialog({ result, installed, onClose, onInstall, showToast, }: { result: SkillHubResult; installed: boolean; onClose: () => void; onInstall: () => void; showToast: (msg: string, kind: "success" | "error") => void; }) { const [tab, setTab] = useState<"readme" | "scan">("readme"); const [preview, setPreview] = useState(null); const [previewLoading, setPreviewLoading] = useState(true); const [scan, setScan] = useState(null); const [scanning, setScanning] = useState(false); const trust = trustVisual(result.trust_level); useEffect(() => { let cancelled = false; setPreviewLoading(true); api .previewSkillFromHub(result.identifier) .then((p) => !cancelled && setPreview(p)) .catch((e) => { if (!cancelled) showToast(`Preview failed: ${e}`, "error"); }) .finally(() => !cancelled && setPreviewLoading(false)); return () => { cancelled = true; }; }, [result.identifier, showToast]); const runScan = useCallback(async () => { setScanning(true); setTab("scan"); try { const s = await api.scanSkillFromHub(result.identifier); setScan(s); } catch (e) { showToast(`Scan failed: ${e}`, "error"); } finally { setScanning(false); } }, [result.identifier, showToast]); return ( !o && onClose()}> {result.name} {trust.label} {result.source} {installed && ( installed )} Preview the SKILL.md source and run a security scan for {result.name}{" "} before installing.

{result.description}

{result.identifier}

{/* Action row */}
{result.repo && ( {result.repo} )} {installed ? ( ) : ( )}
{/* Body */}
{tab === "readme" ? ( previewLoading ? (
) : preview ? (
{preview.tags.length > 0 && (
{preview.tags.map((tag) => ( {tag} ))}
)} {preview.files.length > 0 && (
Files:{" "} {preview.files.join(" ")}
)}
                  {(preview.skill_md || "").trim() || "(SKILL.md is empty)"}
                
) : (

Couldn't load the skill source.

) ) : ( )}
); } /* ---- Visual security-scan result ---- */ function ScanPanel({ scan, scanning, }: { scan: SkillHubScan | null; scanning: boolean; }) { if (scanning && !scan) { return (
Fetching, quarantining, and scanning…
); } if (!scan) { return (

Run a security scan to inspect this skill for risky patterns before installing.

); } const v = verdictVisual(scan.verdict); const policyTone = scan.policy === "allow" ? "success" : scan.policy === "ask" ? "warning" : "destructive"; const policyLabel = scan.policy === "allow" ? "Install allowed" : scan.policy === "ask" ? "Needs confirmation" : "Install blocked"; return (
{/* Verdict header */}
Verdict: {v.label} {scan.verdict}
{scan.trust_level} source · {scan.findings.length} finding {scan.findings.length !== 1 ? "s" : ""}
{policyLabel}
{/* Severity tally */}
{(["critical", "high", "medium", "low"] as const).map((sev) => { const n = scan.severity_counts[sev] || 0; if (n === 0) return null; return ( {n} {sev} ); })} {scan.findings.length === 0 && ( No risky patterns detected )}

{scan.policy_reason}

{/* Findings */} {scan.findings.length > 0 && (
{scan.findings.map((f, i) => (
{f.severity}
{f.category} {f.file}:{f.line}

{f.description}

))}
)}
); }