import { useEffect, useMemo, useState } from "react";
import { Link, useNavigate, useParams } from "@/lib/router";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type {
CompanySkillDetail,
CompanySkillListItem,
CompanySkillTrustLevel,
} from "@paperclipai/shared";
import { companySkillsApi } from "../api/companySkills";
import { useCompany } from "../context/CompanyContext";
import { useBreadcrumbs } from "../context/BreadcrumbContext";
import { useToast } from "../context/ToastContext";
import { queryKeys } from "../lib/queryKeys";
import { EmptyState } from "../components/EmptyState";
import { MarkdownBody } from "../components/MarkdownBody";
import { PageSkeleton } from "../components/PageSkeleton";
import { EntityRow } from "../components/EntityRow";
import { cn } from "../lib/utils";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
ArrowUpRight,
BookOpen,
Boxes,
FolderInput,
RefreshCw,
ShieldAlert,
ShieldCheck,
TerminalSquare,
} from "lucide-react";
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 trustTone(trustLevel: CompanySkillTrustLevel) {
switch (trustLevel) {
case "markdown_only":
return "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300";
case "assets":
return "bg-amber-500/10 text-amber-700 dark:text-amber-300";
case "scripts_executables":
return "bg-red-500/10 text-red-700 dark:text-red-300";
default:
return "bg-muted text-muted-foreground";
}
}
function trustLabel(trustLevel: CompanySkillTrustLevel) {
switch (trustLevel) {
case "markdown_only":
return "Markdown only";
case "assets":
return "Assets";
case "scripts_executables":
return "Scripts";
default:
return trustLevel;
}
}
function compatibilityLabel(detail: CompanySkillDetail | CompanySkillListItem) {
switch (detail.compatibility) {
case "compatible":
return "Compatible";
case "unknown":
return "Unknown";
case "invalid":
return "Invalid";
default:
return detail.compatibility;
}
}
function SkillListItem({
skill,
selected,
}: {
skill: CompanySkillListItem;
selected: boolean;
}) {
return (
{skill.name}
{skill.slug}
{skill.description && (
{skill.description}
)}
{trustLabel(skill.trustLevel)}
{skill.attachedAgentCount} agent{skill.attachedAgentCount === 1 ? "" : "s"}
{skill.fileInventory.length} file{skill.fileInventory.length === 1 ? "" : "s"}
);
}
function SkillDetailPanel({
detail,
isLoading,
}: {
detail: CompanySkillDetail | null | undefined;
isLoading: boolean;
}) {
if (isLoading) {
return ;
}
if (!detail) {
return (
Select a skill
Review its markdown, inspect files, and see which agents have it attached.
);
}
const markdownBody = stripFrontmatter(detail.markdown);
return (
{detail.name}
{detail.slug}
{trustLabel(detail.trustLevel)}
{detail.description && (
{detail.description}
)}
{compatibilityLabel(detail)}
{detail.attachedAgentCount} attached agent{detail.attachedAgentCount === 1 ? "" : "s"}
SKILL.md
{detail.sourceLocator?.startsWith("http") ? (
Open source
) : detail.sourceLocator ? (
{detail.sourceLocator}
) : null}
{markdownBody}
Inventory
{detail.fileInventory.map((entry) => (
{entry.path}
{entry.kind}
))}
Used By Agents
{detail.usedByAgents.length === 0 ? (
No agents are currently attached to this skill.
) : (
{detail.usedByAgents.map((agent) => (
{agent.actualState}
) : undefined}
/>
))}
)}
);
}
export function CompanySkills() {
const { skillId } = useParams<{ skillId?: string }>();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { selectedCompanyId } = useCompany();
const { setBreadcrumbs } = useBreadcrumbs();
const { pushToast } = useToast();
const [source, setSource] = useState("");
useEffect(() => {
setBreadcrumbs([
{ label: "Skills", href: "/skills" },
...(skillId ? [{ label: "Detail" }] : []),
]);
}, [setBreadcrumbs, skillId]);
const {
data: skills,
isLoading,
error,
} = useQuery({
queryKey: queryKeys.companySkills.list(selectedCompanyId ?? ""),
queryFn: () => companySkillsApi.list(selectedCompanyId!),
enabled: Boolean(selectedCompanyId),
});
const selectedSkillId = useMemo(() => {
if (!skillId) return skills?.[0]?.id ?? null;
return skillId;
}, [skillId, skills]);
const {
data: detail,
isLoading: detailLoading,
} = useQuery({
queryKey: queryKeys.companySkills.detail(selectedCompanyId ?? "", selectedSkillId ?? ""),
queryFn: () => companySkillsApi.detail(selectedCompanyId!, selectedSkillId!),
enabled: Boolean(selectedCompanyId && selectedSkillId),
});
const importSkill = useMutation({
mutationFn: (importSource: string) => companySkillsApi.importFromSource(selectedCompanyId!, importSource),
onSuccess: async (result) => {
await queryClient.invalidateQueries({ queryKey: queryKeys.companySkills.list(selectedCompanyId!) });
if (result.imported[0]) {
navigate(`/skills/${result.imported[0].id}`);
}
pushToast({
tone: "success",
title: "Skills imported",
body: `${result.imported.length} skill${result.imported.length === 1 ? "" : "s"} added to the company library.`,
});
if (result.warnings[0]) {
pushToast({
tone: "warn",
title: "Import warnings",
body: result.warnings[0],
});
}
setSource("");
},
onError: (importError) => {
pushToast({
tone: "error",
title: "Skill import failed",
body: importError instanceof Error ? importError.message : "Failed to import skill source.",
});
},
});
if (!selectedCompanyId) {
return ;
}
return (
Company skill library
Manage reusable skills once, attach them anywhere.
Import `SKILL.md` packages from local paths, GitHub repos, or direct URLs. Agents attach by skill shortname, while adapters decide how those skills are installed or mounted.
Markdown-first
`skills.sh` compatible packages stay readable and repo-native.
GitHub aware
Import a repo, a subtree, or a single skill file without a registry.
Trust surfaced
Scripts and executable bundles stay visible instead of being hidden in setup.
{error &&
{error.message}
}
{!isLoading && (skills?.length ?? 0) === 0 ? (
{
const trimmed = source.trim();
if (trimmed) importSkill.mutate(trimmed);
}}
/>
) : (
Library
{skills?.length ?? 0} tracked skill{(skills?.length ?? 0) === 1 ? "" : "s"}
{isLoading ? (
) : (
{(skills ?? []).map((skill) => (
))}
)}
)}
);
}