import { useEffect, useMemo, useState, type SVGProps } from "react"; import { Link, useNavigate, useParams, useSearchParams } from "@/lib/router"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { Agent, CatalogSkill, CatalogSkillFileDetail, CompanySkillCompatibility, CompanySkillCreateRequest, CompanySkillDetail, CompanySkillFileDetail, CompanySkillFileInventoryEntry, CompanySkillListItem, CompanySkillProjectScanResult, CompanySkillSourceBadge, CompanySkillTrustLevel, CompanySkillUpdateStatus, } from "@paperclipai/shared"; import { companySkillsApi } from "../api/companySkills"; import { agentsApi } from "../api/agents"; import { useCompany } from "../context/CompanyContext"; import { useBreadcrumbs } from "../context/BreadcrumbContext"; import { useToastActions } from "../context/ToastContext"; import { queryKeys } from "../lib/queryKeys"; import { EmptyState } from "../components/EmptyState"; import { MarkdownBody } from "../components/MarkdownBody"; import { MarkdownEditor } from "../components/MarkdownEditor"; import { PageSkeleton } from "../components/PageSkeleton"; import { CopyText } from "../components/CopyText"; import { Identity } from "../components/Identity"; import { useAdapterCapabilities } from "../adapters/use-adapter-capabilities"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { Checkbox } from "@/components/ui/checkbox"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { cn } from "../lib/utils"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { AlertTriangle, ArrowUpCircle, Boxes, Check, ChevronDown, ChevronRight, Code2, Download, Eye, Filter, FileCode2, FileText, Folder, FolderOpen, Github, Globe, HelpCircle, Link2, ExternalLink, Paperclip, Pencil, Plus, Copy, RefreshCw, Save, Search, Server, ShieldCheck, Trash2, Users, XOctagon, } from "lucide-react"; type SkillTreeNode = { name: string; path: string | null; kind: "dir" | "file"; fileKind?: CompanySkillFileInventoryEntry["kind"]; children: SkillTreeNode[]; }; const SKILL_TREE_BASE_INDENT = 16; const SKILL_TREE_STEP_INDENT = 24; const SKILL_TREE_ROW_HEIGHT_CLASS = "min-h-9"; function VercelMark(props: SVGProps) { return ( ); } function stripFrontmatter(markdown: string) { const normalized = markdown.replace(/\r\n/g, "\n"); if (!normalized.startsWith("---\n")) return normalized.trim(); const closing = normalized.indexOf("\n---\n", 4); if (closing < 0) return normalized.trim(); return normalized.slice(closing + 5).trim(); } function splitFrontmatter(markdown: string): { frontmatter: string | null; body: string } { const normalized = markdown.replace(/\r\n/g, "\n"); if (!normalized.startsWith("---\n")) { return { frontmatter: null, body: normalized }; } const closing = normalized.indexOf("\n---\n", 4); if (closing < 0) { return { frontmatter: null, body: normalized }; } return { frontmatter: normalized.slice(4, closing).trim(), body: normalized.slice(closing + 5).trimStart(), }; } function mergeFrontmatter(markdown: string, body: string) { const parsed = splitFrontmatter(markdown); if (!parsed.frontmatter) return body; return ["---", parsed.frontmatter, "---", "", body].join("\n"); } function buildTree(entries: CompanySkillFileInventoryEntry[]) { const root: SkillTreeNode = { name: "", path: null, kind: "dir", children: [] }; for (const entry of entries) { const segments = entry.path.split("/").filter(Boolean); let current = root; let currentPath = ""; for (const [index, segment] of segments.entries()) { currentPath = currentPath ? `${currentPath}/${segment}` : segment; const isLeaf = index === segments.length - 1; let next = current.children.find((child) => child.name === segment); if (!next) { next = { name: segment, path: isLeaf ? entry.path : currentPath, kind: isLeaf ? "file" : "dir", fileKind: isLeaf ? entry.kind : undefined, children: [], }; current.children.push(next); } current = next; } } function sortNode(node: SkillTreeNode) { node.children.sort((left, right) => { if (left.kind !== right.kind) return left.kind === "dir" ? -1 : 1; if (left.name === "SKILL.md") return -1; if (right.name === "SKILL.md") return 1; return left.name.localeCompare(right.name); }); node.children.forEach(sortNode); } sortNode(root); return root.children; } function sourceMeta(sourceBadge: CompanySkillSourceBadge, sourceLabel: string | null) { const normalizedLabel = sourceLabel?.toLowerCase() ?? ""; const isSkillsShManaged = normalizedLabel.includes("skills.sh") || normalizedLabel.includes("vercel-labs/skills"); switch (sourceBadge) { case "skills_sh": return { icon: VercelMark, label: sourceLabel ?? "skills.sh", managedLabel: "skills.sh managed" }; case "github": return isSkillsShManaged ? { icon: VercelMark, label: sourceLabel ?? "skills.sh", managedLabel: "skills.sh managed" } : { icon: Github, label: sourceLabel ?? "GitHub", managedLabel: "GitHub managed" }; case "gitea": return { icon: Server, label: sourceLabel ?? "Gitea", managedLabel: "Gitea managed" }; case "url": return { icon: Link2, label: sourceLabel ?? "URL", managedLabel: "URL managed" }; case "local": return { icon: Folder, label: sourceLabel ?? "Folder", managedLabel: "Folder managed" }; case "paperclip": return { icon: Paperclip, label: sourceLabel ?? "Paperclip", managedLabel: "Paperclip managed" }; default: return { icon: Boxes, label: sourceLabel ?? "Catalog", managedLabel: "Catalog managed" }; } } function shortRef(ref: string | null | undefined) { if (!ref) return null; return ref.slice(0, 7); } function middleTruncate(value: string, maxLength = 72) { if (value.length <= maxLength) return value; const edgeLength = Math.floor((maxLength - 3) / 2); return `${value.slice(0, edgeLength)}...${value.slice(value.length - edgeLength)}`; } function formatProjectScanSummary(result: CompanySkillProjectScanResult) { const parts = [ `${result.discovered} found`, `${result.imported.length} imported`, `${result.updated.length} updated`, ]; if (result.conflicts.length > 0) parts.push(`${result.conflicts.length} conflicts`); if (result.skipped.length > 0) parts.push(`${result.skipped.length} skipped`); return `${parts.join(", ")} across ${result.scannedWorkspaces} workspace${result.scannedWorkspaces === 1 ? "" : "s"}.`; } function fileIcon(kind: CompanySkillFileInventoryEntry["kind"]) { if (kind === "script" || kind === "reference") return FileCode2; return FileText; } function encodeSkillFilePath(filePath: string) { return filePath.split("/").map((segment) => encodeURIComponent(segment)).join("/"); } function decodeSkillFilePath(filePath: string | undefined) { if (!filePath) return "SKILL.md"; return filePath .split("/") .filter(Boolean) .map((segment) => { try { return decodeURIComponent(segment); } catch { return segment; } }) .join("/"); } function parseSkillRoute(routePath: string | undefined) { const segments = (routePath ?? "").split("/").filter(Boolean); if (segments.length === 0) { return { skillId: null, filePath: "SKILL.md" }; } const [rawSkillId, rawMode, ...rest] = segments; const skillId = rawSkillId ? decodeURIComponent(rawSkillId) : null; if (!skillId) { return { skillId: null, filePath: "SKILL.md" }; } if (rawMode === "files") { return { skillId, filePath: decodeSkillFilePath(rest.join("/")), }; } return { skillId, filePath: "SKILL.md" }; } function skillRoute(skillId: string, filePath?: string | null) { return filePath ? `/skills/${skillId}/files/${encodeSkillFilePath(filePath)}` : `/skills/${skillId}`; } function catalogSkillRoute(catalogRef: string) { return `/skills?view=catalog&catalog=${encodeURIComponent(catalogRef)}`; } function parentDirectoryPaths(filePath: string) { const segments = filePath.split("/").filter(Boolean); const parents: string[] = []; for (let index = 0; index < segments.length - 1; index += 1) { parents.push(segments.slice(0, index + 1).join("/")); } return parents; } type SourceFilter = "all" | "company" | "bundled" | "optional" | "external"; const SOURCE_FILTER_LABELS: Record = { all: "All", company: "Company", bundled: "Bundled", optional: "Optional", external: "External", }; function readonlyMetadataValue(metadata: Record | null | undefined, key: string): string | null { if (!metadata || typeof metadata !== "object") return null; const raw = (metadata as Record)[key]; if (typeof raw !== "string") return null; const trimmed = raw.trim(); return trimmed.length > 0 ? trimmed : null; } function readonlyMetadataKind(metadata: Record | null | undefined): "bundled" | "optional" | null { const value = readonlyMetadataValue(metadata, "sourceKind") ?? readonlyMetadataValue(metadata, "catalogKind"); if (value === "bundled") return "bundled"; if (value === "optional") return "optional"; return null; } function classifySource(skill: { sourceBadge: CompanySkillSourceBadge; sourceType: string; catalogKind?: "bundled" | "optional" | null; metadata?: Record | null; }): SourceFilter { if (skill.sourceBadge === "paperclip") return "company"; if (skill.sourceType === "local_path" && !skill.sourceBadge.toString().includes("github")) { return "company"; } if (skill.sourceType === "catalog" || skill.sourceBadge === "catalog") { const kind = skill.catalogKind ?? readonlyMetadataKind(skill.metadata); if (kind === "bundled") return "bundled"; if (kind === "optional") return "optional"; return "company"; } if (skill.sourceBadge === "github" || skill.sourceBadge === "skills_sh" || skill.sourceBadge === "gitea" || skill.sourceBadge === "url" || skill.sourceBadge === "local") { return "external"; } return "company"; } function SourceFilterMenu({ counts, value, onChange, }: { counts: Record; value: SourceFilter; onChange: (next: SourceFilter) => void; }) { const filters: SourceFilter[] = ["all", "company", "bundled", "optional", "external"]; const activeFilterCount = value === "all" ? 0 : 1; return ( Source onChange(next as SourceFilter)}> {filters.map((filter) => ( {SOURCE_FILTER_LABELS[filter]} {counts[filter] ?? 0} ))} ); } function CatalogFilterMenu({ kindFilter, categoryFilter, categories, onKindChange, onCategoryChange, }: { kindFilter: "all" | "bundled" | "optional"; categoryFilter: string; categories: string[]; onKindChange: (next: "all" | "bundled" | "optional") => void; onCategoryChange: (next: string) => void; }) { const activeFilterCount = (kindFilter === "all" ? 0 : 1) + (categoryFilter ? 1 : 0); return ( Type onKindChange(next as "all" | "bundled" | "optional")}> All Bundled Optional Category onCategoryChange(next === "__all__" ? "" : next)}> All categories {categories.map((category) => ( {category} ))} ); } function TrustChip({ level }: { level: CompanySkillTrustLevel }) { const map = { markdown_only: { icon: ShieldCheck, label: "Markdown only", tooltip: "Text only — no scripts, no binaries, no assets.", className: "border-border bg-muted/40 text-muted-foreground", }, assets: { icon: Folder, label: "Includes assets", tooltip: "Ships images, fonts, or other non-script files.", className: "border-cyan-500/30 bg-cyan-500/10 text-cyan-200", }, scripts_executables: { icon: AlertTriangle, label: "Includes scripts", tooltip: "Ships executable scripts. Review before installing.", className: "border-amber-500/40 bg-amber-500/10 text-amber-200", }, } as const; const config = map[level] ?? map.markdown_only; const Icon = config.icon; return ( {config.tooltip} ); } function CompatChip({ compatibility }: { compatibility: CompanySkillCompatibility }) { if (compatibility === "compatible") return null; const map = { unknown: { icon: HelpCircle, label: "Unknown format", tooltip: "Paperclip could not validate this skill as Agent Skills markdown. Install at your own risk.", className: "border-yellow-500/40 bg-yellow-500/10 text-yellow-200", }, invalid: { icon: XOctagon, label: "Invalid", tooltip: "This skill cannot be installed — content is not valid Agent Skills markdown.", className: "border-destructive/40 bg-destructive/10 text-destructive", }, } as const; const config = map[compatibility]; const Icon = config.icon; return ( {config.tooltip} ); } function ProvenanceBadge({ packageName, packageVersion }: { packageName: string | null; packageVersion: string | null }) { if (!packageName) return null; return ( Installed from the app-shipped skills catalog. Provenance is signed by package version and content hash. ); } function formatBytes(bytes: number) { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } function NewSkillForm({ onCreate, isPending, onCancel, }: { onCreate: (payload: CompanySkillCreateRequest) => void; isPending: boolean; onCancel: () => void; }) { const [name, setName] = useState(""); const [slug, setSlug] = useState(""); const [description, setDescription] = useState(""); return (
setName(event.target.value)} placeholder="Skill name" className="h-9 rounded-none border-0 border-b border-border px-0 shadow-none focus-visible:ring-0" /> setSlug(event.target.value)} placeholder="optional-shortname" className="h-9 rounded-none border-0 border-b border-border px-0 shadow-none focus-visible:ring-0" />