import { useEffect, useState, type ReactNode } from "react"; import type { TFunction } from "i18next"; import { Check, CircleAlert, Copy, KeyRound, Loader2, PowerOff, RefreshCw, Search, Terminal, Trash2, } from "lucide-react"; import { useTranslation } from "react-i18next"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Sheet, SheetContent, SheetDescription, SheetTitle } from "@/components/ui/sheet"; import { SkillsMarketplace } from "@/components/settings/SkillsMarketplace"; import { deleteSkill, fetchSkillDetail, updateSkillEnabled } from "@/lib/api"; import { notifySkillsChanged } from "@/lib/skill-events"; import type { SkillDetail, SkillSummary } from "@/lib/types"; import { cn } from "@/lib/utils"; import { useClient } from "@/providers/ClientProvider"; export function SkillsCatalogSettings({ skills }: { skills: SkillSummary[] }) { const { t } = useTranslation(); const availableCount = skills.filter( (skill) => skill.enabled !== false && skill.available, ).length; const [selectedSkill, setSelectedSkill] = useState(null); const [view, setView] = useState<"installed" | "discover">("installed"); const [installedQuery, setInstalledQuery] = useState(""); const [installedFilter, setInstalledFilter] = useState<"all" | "enabled" | "disabled">( "all", ); const normalizedQuery = installedQuery.trim().toLowerCase(); const filteredSkills = skills.filter((skill) => { const enabled = skill.enabled !== false; if (installedFilter === "enabled" && !enabled) return false; if (installedFilter === "disabled" && enabled) return false; return ( !normalizedQuery || skill.name.toLowerCase().includes(normalizedQuery) || skill.description.toLowerCase().includes(normalizedQuery) ); }); const groupedSkills = [ { key: "workspace", label: t("settings.skills.customGroup", { defaultValue: "Custom" }), skills: filteredSkills.filter((skill) => skill.source === "workspace"), }, { key: "builtin", label: t("settings.skills.builtinGroup", { defaultValue: "Built-in" }), skills: filteredSkills.filter((skill) => skill.source === "builtin"), }, { key: "other", label: t("settings.skills.otherGroup", { defaultValue: "Other" }), skills: filteredSkills.filter( (skill) => skill.source !== "workspace" && skill.source !== "builtin", ), }, ].filter((group) => group.skills.length); const disabledCount = skills.filter((skill) => skill.enabled === false).length; return (

{t("settings.skills.description", { defaultValue: "Review installed skills or discover new capabilities from the skills.sh catalog.", })}

{t("settings.skills.caption", { available: availableCount, total: skills.length, defaultValue: "{{available}} available ยท {{total}} total", })}
{(["installed", "discover"] as const).map((item) => ( ))}
{view === "installed" ? (
setInstalledQuery(event.target.value)} placeholder={t("settings.skills.searchInstalled", { defaultValue: "Search installed skills", })} aria-label={t("settings.skills.searchInstalled", { defaultValue: "Search installed skills", })} className="h-9 rounded-[11px] bg-background pl-9 text-[13px]" />
{([ ["all", t("settings.skills.filterAll", { defaultValue: "All" }), skills.length], [ "enabled", t("settings.skills.filterEnabled", { defaultValue: "Enabled" }), skills.length - disabledCount, ], [ "disabled", t("settings.skills.filterDisabled", { defaultValue: "Disabled" }), disabledCount, ], ] as const).map(([filter, label, count]) => ( ))}
{groupedSkills.length ? (
{groupedSkills.map((group) => (

{group.label}

{group.skills.length}
{group.skills.map((skill) => ( ))}
))}
) : (
{t("settings.skills.noMatching", { defaultValue: "No matching skills.", })}
)}
) : ( )} { if (!open) setSelectedSkill(null); }} />
); } function VercelMark() { return ( ); } function SkillCatalogRow({ skill, onSelect, }: { skill: SkillSummary; onSelect: (skill: SkillSummary) => void; }) { const { t } = useTranslation(); const enabled = skill.enabled !== false; const StatusIcon = !enabled ? PowerOff : skill.available ? Check : CircleAlert; const statusLabel = !enabled ? t("settings.skills.statusDisabled", { defaultValue: "Disabled" }) : skill.available ? t("settings.skills.statusEnabled", { defaultValue: "Enabled" }) : t("settings.skills.statusNeedsSetup", { defaultValue: "Needs setup" }); return ( ); } function SkillDetailSheet({ skill, open, onOpenChange, }: { skill: SkillSummary | null; open: boolean; onOpenChange: (open: boolean) => void; }) { const { token } = useClient(); const { t } = useTranslation(); const [detail, setDetail] = useState(null); const [loading, setLoading] = useState(false); const [loadFailed, setLoadFailed] = useState(false); const [actionBusy, setActionBusy] = useState(false); const [actionError, setActionError] = useState(""); const [deleteOpen, setDeleteOpen] = useState(false); const [refreshKey, setRefreshKey] = useState(0); useEffect(() => { if (!open || !skill) return; let cancelled = false; setDetail(null); setLoading(true); setLoadFailed(false); setActionError(""); setDeleteOpen(false); fetchSkillDetail(token, skill.name) .then((payload) => { if (!cancelled) setDetail(payload); }) .catch(() => { if (!cancelled) setLoadFailed(true); }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [open, refreshKey, skill, token]); if (!skill) return null; const activeSkill = detail ?? skill; const enabled = activeSkill.enabled !== false; const deletable = activeSkill.deletable ?? activeSkill.source === "workspace"; const sourceLabel = skillSourceLabel(activeSkill.source, t); const statusLabel = !enabled ? t("settings.skills.statusDisabled", { defaultValue: "Disabled" }) : activeSkill.available ? t("settings.skills.statusEnabled", { defaultValue: "Enabled" }) : t("settings.skills.statusNeedsSetup", { defaultValue: "Needs setup" }); const toggleEnabled = async () => { setActionBusy(true); setActionError(""); try { const payload = await updateSkillEnabled(token, activeSkill.name, !enabled); notifySkillsChanged(payload); const updated = payload.skills.find((item) => item.name === activeSkill.name); if (updated) { setDetail((current) => current ? { ...current, ...updated } : current); } } catch (reason) { setActionError( reason instanceof Error ? reason.message : t("settings.skills.updateFailed", { defaultValue: "Could not update this skill.", }), ); } finally { setActionBusy(false); } }; const removeSkill = async () => { setDeleteOpen(false); setActionBusy(true); setActionError(""); try { const payload = await deleteSkill(token, activeSkill.name); notifySkillsChanged(payload); onOpenChange(false); } catch (reason) { setActionError( reason instanceof Error ? reason.message : t("settings.skills.deleteFailed", { defaultValue: "Could not delete this skill.", }), ); } finally { setActionBusy(false); } }; return ( <>
{activeSkill.name} {t("settings.skills.detailDescription", { name: activeSkill.name, defaultValue: "Details for {{name}}.", })}
{sourceLabel} {statusLabel}

{activeSkill.description}

{loading ? (
{t("settings.skills.loadingDetail", { defaultValue: "Loading skill details..." })}
) : loadFailed ? (
{t("settings.skills.loadFailed", { defaultValue: "Could not load skill details." })}
) : (

{t("settings.skills.enabledControl", { defaultValue: "Use this skill" })}

{t("settings.skills.enabledDescription", { defaultValue: "Allow the agent to load this skill when its requirements are ready.", })}

{actionError ? (
{actionError}
) : null} {detail && enabled ? ( setRefreshKey((value) => value + 1)} /> ) : null} {detail ? : null} {deletable ? (

{t("settings.skills.deleteTitle", { defaultValue: "Delete skill" })}

{t("settings.skills.deleteDescription", { defaultValue: "Remove this skill from the current workspace.", })}

) : null}
)}
{t("settings.skills.deleteConfirmTitle", { name: activeSkill.name, defaultValue: "Delete {{name}}?", })} {t("settings.skills.deleteConfirmDescription", { defaultValue: "This removes the skill files from the current workspace. This action cannot be undone.", })} {t("common.cancel", { defaultValue: "Cancel" })} void removeSkill()} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > {t("settings.skills.deleteConfirmAction", { defaultValue: "Delete skill" })} ); } function RawInstructionsBlock({ markdown }: { markdown: string }) { const { t } = useTranslation(); const content = markdown || t("settings.skills.rawInstructionsEmpty", { defaultValue: "No raw instructions.", }); return (
{t("settings.skills.instructionsTitle", { defaultValue: "Skill instructions" })} SKILL.md
          {content}
        
); } function RequirementsSection({ detail, onRefresh, }: { detail: SkillDetail; onRefresh: () => void; }) { const { t } = useTranslation(); const [copiedCommand, setCopiedCommand] = useState(null); const { missing_bins, missing_env } = detail.requirements; const hasMissing = missing_bins.length > 0 || missing_env.length > 0; if (!hasMissing) return null; const installOptions = detail.install_options ?? []; const copyCommand = async (command: string) => { try { await navigator.clipboard.writeText(command); setCopiedCommand(command); } catch { setCopiedCommand(null); } }; return (

{t("settings.skills.setupRequired", { defaultValue: "Setup required" })}

{t("settings.skills.setupDescription", { defaultValue: "Install the missing dependency on the machine running nanobot, then check again.", })}

{installOptions.map((option) => (
{option.command}
))} {!installOptions.length && missing_bins.length ? ( } label={t("settings.skills.missingCommands", { defaultValue: "Missing CLI" })} items={missing_bins} /> ) : null} {missing_env.length ? ( } label={t("settings.skills.missingEnvironment", { defaultValue: "Missing ENV" })} items={missing_env} /> ) : null}
); } function SetupRequirement({ label, items, icon, }: { label: string; items: string[]; icon: ReactNode; }) { return (
{icon} {label} {items.map((item) => ( {item} ))}
); } function Pill({ children, tone = "muted", }: { children: ReactNode; tone?: "muted" | "success" | "warning"; }) { return ( {children} ); } function skillSourceLabel(source: string, t: TFunction): string { if (source === "workspace") { return t("settings.skills.sourceWorkspace", { defaultValue: "Custom" }); } if (source === "builtin") { return t("settings.skills.sourceBuiltin", { defaultValue: "Built-in" }); } return source; }