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
{saveRatingMutation.isError && (
{(saveRatingMutation.error as Error).message}
)}
{/* Section 2: Your Rating History */}
Your Rating History
{ratingsError && (
Failed to load ratings.
)}
{ratingsLoading && (
)}
{!ratingsLoading && !ratingsError && ratings.length === 0 && (
)}
{!ratingsLoading && !ratingsError && ratings.length > 0 && (
{ratings.map((r) => (
{r.versionId && (
{r.versionId.slice(-7)}
)}
{r.note && (
{r.note}
)}
{relativeTime(new Date(r.createdAt))}
))}
)}
{/* Section 3: Community Ratings */}
Community Ratings
{skill.averageRating != null ? (
Average
{skill.averageRating.toFixed(1)}
Ratings
{skill.ratingCount}
) : (
No community ratings available.
)}
{/* Install / Update dialog */}
{/* Uninstall confirmation dialog */}
);
}