import { useEffect, useState } from "react"; import { useParams, Link } from "react-router-dom"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { diffLines } from "diff"; import { ChevronLeft, Star, Download, RotateCcw, Trash2 } from "lucide-react"; import { useCompany } from "@/context/CompanyContext"; import { useBreadcrumbs } from "@/context/BreadcrumbContext"; import { useToast } from "@/context/ToastContext"; import { skillRegistryApi } from "@/api/skillRegistry"; import { queryKeys } from "@/lib/queryKeys"; import { cn, relativeTime } from "@/lib/utils"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; import { Skeleton } from "@/components/ui/skeleton"; import { Tabs, TabsContent } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import { PageTabBar } from "@/components/PageTabBar"; import { PageSkeleton } from "@/components/PageSkeleton"; import { EmptyState } from "@/components/EmptyState"; import { StarRating } from "@/components/StarRating"; /* ------------------------------------------------------------------ */ /* VersionDiff component */ /* ------------------------------------------------------------------ */ function VersionDiff({ oldContent, newContent }: { oldContent: string; newContent: string }) { const parts = diffLines(oldContent, newContent); return (
      {parts.map((part, i) => (
        
          {part.value}
        
      ))}
    
); } /* ------------------------------------------------------------------ */ /* SkillDetail page */ /* ------------------------------------------------------------------ */ export function SkillDetail() { const { skillId: rawSkillId } = useParams<{ skillId: string }>(); const skillId = rawSkillId ? decodeURIComponent(rawSkillId) : ""; const { selectedCompany } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); const { pushToast } = useToast(); const queryClient = useQueryClient(); const [detailTab, setDetailTab] = useState("overview"); const [versionA, setVersionA] = useState(""); const [versionB, setVersionB] = useState(""); // Rating form state const [pendingStars, setPendingStars] = useState(0); const [pendingNote, setPendingNote] = useState(""); // Install dialog state — used for both install and update flows const [installDialog, setInstallDialog] = useState<{ skillId: string; isUpdate: boolean; agentSkillsDir?: string; } | null>(null); // Uninstall confirmation dialog state const [uninstallDialog, setUninstallDialog] = useState<{ skillId: string } | null>(null); /* ---------------------------------------------------------------- */ /* Data queries */ /* ---------------------------------------------------------------- */ const { data: skill, isLoading, isError } = useQuery({ queryKey: queryKeys.skillRegistry.detail(skillId), queryFn: () => skillRegistryApi.getById(skillId), enabled: !!skillId, }); const { data: versions = [] } = useQuery({ queryKey: queryKeys.skillRegistry.versions(skillId), queryFn: () => skillRegistryApi.getVersions(skillId), enabled: !!skillId, }); const { data: ratings = [], isLoading: ratingsLoading, isError: ratingsError, } = useQuery({ queryKey: ["skill-ratings", skillId], queryFn: () => skillRegistryApi.getRatings(skillId), enabled: !!skillId, }); /* ---------------------------------------------------------------- */ /* Breadcrumbs */ /* ---------------------------------------------------------------- */ useEffect(() => { setBreadcrumbs([ { label: selectedCompany?.name ?? "Workspace", href: "/dashboard" }, { label: "Skills", href: "../skills" }, { label: skill?.name ?? "Skill" }, ]); }, [selectedCompany?.name, skill?.name, setBreadcrumbs]); /* ---------------------------------------------------------------- */ /* Mutations */ /* ---------------------------------------------------------------- */ const invalidateSkill = () => { queryClient.invalidateQueries({ queryKey: queryKeys.skillRegistry.detail(skillId) }); queryClient.invalidateQueries({ queryKey: queryKeys.skillRegistry.list }); }; const installMutation = useMutation({ mutationFn: ({ agentSkillsDir }: { agentSkillsDir: string }) => skillRegistryApi.install(skillId, agentSkillsDir), onSuccess: () => { invalidateSkill(); setInstallDialog(null); pushToast({ title: "Skill installed", tone: "success" }); }, onError: (err: Error) => { pushToast({ title: "Install failed", body: err.message, tone: "error" }); }, }); const updateMutation = useMutation({ mutationFn: ({ agentSkillsDir }: { agentSkillsDir: string }) => skillRegistryApi.install(skillId, agentSkillsDir), onSuccess: () => { invalidateSkill(); setInstallDialog(null); pushToast({ title: "Skill updated", tone: "success" }); }, onError: (err: Error) => { pushToast({ title: "Update failed", body: err.message, tone: "error" }); }, }); const rollbackMutation = useMutation({ mutationFn: ({ versionId, agentSkillsDir }: { versionId: string; agentSkillsDir: string }) => skillRegistryApi.rollback(skillId, versionId, agentSkillsDir), onSuccess: () => { invalidateSkill(); pushToast({ title: "Rolled back to previous version", tone: "success" }); }, onError: (err: Error) => { pushToast({ title: "Rollback failed", body: err.message, tone: "error" }); }, }); const removeMutation = useMutation({ mutationFn: () => skillRegistryApi.remove(skillId), onSuccess: () => { invalidateSkill(); setUninstallDialog(null); pushToast({ title: "Skill uninstalled", tone: "success" }); }, onError: (err: Error) => { pushToast({ title: "Uninstall failed", body: err.message, tone: "error" }); }, }); const saveRatingMutation = useMutation({ mutationFn: ({ stars, note }: { stars: number; note: string }) => skillRegistryApi.addRating(skillId, { stars, versionId: skill?.activeVersionId ?? undefined, note: note.trim() || undefined, }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["skill-ratings", skillId] }); setPendingStars(0); setPendingNote(""); pushToast({ title: "Rating saved", tone: "success" }); }, onError: (err: Error) => { pushToast({ title: "Failed to save rating", body: err.message, tone: "error" }); }, }); /* ---------------------------------------------------------------- */ /* Loading / error states */ /* ---------------------------------------------------------------- */ if (isLoading) { return (
); } if (isError) { return (

Failed to load skills. Check that the skill registry backend is running and try again.

); } if (!skill) { return (

Skill not found.

); } const isInstalled = !!skill.activeVersionId; const isMutating = installMutation.isPending || updateMutation.isPending || rollbackMutation.isPending || removeMutation.isPending; /* ---------------------------------------------------------------- */ /* Render */ /* ---------------------------------------------------------------- */ return (
{/* Back link */} Skills {/* Header */}

{skill.name}

{skill.sourceId}
{!isInstalled && ( )} {isInstalled && ( <> )}
{/* Description */} {skill.description && (

{skill.description}

)} {/* Detail tabs */} {/* Overview tab */}
Installs {skill.ratingCount ?? "\u2014"}
Average rating {skill.averageRating != null ? ( <> {skill.averageRating.toFixed(1)} ) : ( "No ratings yet" )}
{skill.averageRating != null && skill.ratingCount != null && (
Community ({skill.ratingCount} ratings)
)}
Source {skill.sourceId}
Active version {skill.activeVersionId ? `v${skill.activeVersionId.split("@").pop() ?? skill.activeVersionId}` : "\u2014"}
{/* Usage stats — Phase 12 */} {skill.taskCount != null && skill.taskCount > 0 && (
Tasks completed {skill.taskCount}
)} {skill.avgCostUsd != null && (
Avg task cost ${skill.avgCostUsd.toFixed(4)}
)} {skill.lastUsedAt != null && (
Last used {relativeTime(new Date(skill.lastUsedAt))}
)}
{/* Versions tab */} {versions.length === 0 ? (

No versions fetched yet.

) : (
{versions.map((v) => (
{v.version} {relativeTime(new Date(v.fetchedAt))}
{v.id === skill.activeVersionId && ( Active )}
))}
)}
{/* Diff tab */}
{versionA && versionB ? (

Version diff requires file content endpoint (coming in a future update).

) : (

Select two versions above to compare them.

)}
{/* Ratings tab */} {/* Section 1: Rate this skill */}

Rate this skill