diff --git a/ui/src/pages/SkillBrowser.tsx b/ui/src/pages/SkillBrowser.tsx
index d04e32eb..6819836f 100644
--- a/ui/src/pages/SkillBrowser.tsx
+++ b/ui/src/pages/SkillBrowser.tsx
@@ -1,3 +1,563 @@
+import { useEffect, useMemo, useState } from "react";
+import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
+import {
+ Download,
+ Search,
+ TrendingUp,
+} from "lucide-react";
+import { useCompany } from "@/context/CompanyContext";
+import { useBreadcrumbs } from "@/context/BreadcrumbContext";
+import { useToast } from "@/context/ToastContext";
+import { skillRegistryApi } from "@/api/skillRegistry";
+import { agentsApi } from "@/api/agents";
+import { queryKeys } from "@/lib/queryKeys";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Tabs, TabsContent } from "@/components/ui/tabs";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { SkillCard } from "@/components/SkillCard";
+import { EmptyState } from "@/components/EmptyState";
+import { FilterBar } from "@/components/FilterBar";
+import type { FilterValue } from "@/components/FilterBar";
+import { PageTabBar } from "@/components/PageTabBar";
+import { PageSkeleton } from "@/components/PageSkeleton";
+import { Identity } from "@/components/Identity";
+import { cn } from "@/lib/utils";
+
+type SortBy = "rating" | "name" | "recent";
+
export function SkillBrowser() {
- return
SkillBrowser placeholder
;
+ const { selectedCompany } = useCompany();
+ const { setBreadcrumbs } = useBreadcrumbs();
+ const queryClient = useQueryClient();
+ const { pushToast } = useToast();
+
+ // Tab state
+ const [tab, setTab] = useState<"browse" | "installed" | "trending">("browse");
+
+ // Browse tab filter state
+ const [search, setSearch] = useState("");
+ const [sourceFilter, setSourceFilter] = useState(null);
+ const [categoryFilter, setCategoryFilter] = useState(null);
+ const [sortBy, setSortBy] = useState("rating");
+
+ // Dialog state
+ const [installDialog, setInstallDialog] = useState<{ skillId: string; isUpdate?: boolean } | null>(null);
+ const [agentSkillsDir, setAgentSkillsDir] = useState("");
+ const [uninstallDialog, setUninstallDialog] = useState<{ skillId: string } | null>(null);
+
+ useEffect(() => {
+ setBreadcrumbs([
+ { label: selectedCompany?.name ?? "Workspace", href: "/dashboard" },
+ { label: "Skills" },
+ ]);
+ }, [selectedCompany?.name, setBreadcrumbs]);
+
+ // Data fetching
+ const { data: skills = [], isLoading, isError } = useQuery({
+ queryKey: queryKeys.skillRegistry.list,
+ queryFn: () => skillRegistryApi.list(),
+ });
+
+ const { data: agents = [] } = useQuery({
+ queryKey: queryKeys.agents.list(selectedCompany?.id ?? ""),
+ queryFn: () => agentsApi.list(selectedCompany?.id ?? ""),
+ enabled: !!selectedCompany?.id,
+ });
+
+ // Mutations
+ const fetchMutation = useMutation({
+ mutationFn: () => skillRegistryApi.fetch(),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: queryKeys.skillRegistry.list });
+ pushToast({ title: "Registry refreshed", tone: "success" });
+ },
+ onError: (err: Error) => {
+ pushToast({ title: "Registry refresh failed", body: err.message, tone: "error" });
+ },
+ });
+
+ const installMutation = useMutation({
+ mutationFn: (params: { skillId: string; agentSkillsDir: string }) =>
+ skillRegistryApi.install(params.skillId, params.agentSkillsDir),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: queryKeys.skillRegistry.list });
+ pushToast({ title: "Skill installed", tone: "success" });
+ },
+ onError: (err: Error) => {
+ pushToast({ title: "Install failed", body: err.message, tone: "error" });
+ },
+ });
+
+ const updateMutation = useMutation({
+ mutationFn: (params: { skillId: string; agentSkillsDir: string }) =>
+ skillRegistryApi.install(params.skillId, params.agentSkillsDir),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: queryKeys.skillRegistry.list });
+ pushToast({ title: "Skill updated", tone: "success" });
+ },
+ onError: (err: Error) => {
+ pushToast({ title: "Update failed", body: err.message, tone: "error" });
+ },
+ });
+
+ const rollbackMutation = useMutation({
+ mutationFn: (params: { skillId: string; versionId: string; agentSkillsDir: string }) =>
+ skillRegistryApi.rollback(params.skillId, params.versionId, params.agentSkillsDir),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: queryKeys.skillRegistry.list });
+ 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: (skillId: string) => skillRegistryApi.remove(skillId),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: queryKeys.skillRegistry.list });
+ pushToast({ title: "Skill uninstalled", tone: "success" });
+ },
+ });
+
+ // Derived data for filters
+ const sources = useMemo(
+ () => [...new Set(skills.map((s) => s.sourceId))].sort(),
+ [skills],
+ );
+ const categories = useMemo(
+ () => [...new Set(skills.map((s) => s.category).filter(Boolean) as string[])].sort(),
+ [skills],
+ );
+
+ // Browse tab filtering
+ const filteredSkills = useMemo(() => {
+ let result = skills.filter((s) => !s.removedAt);
+ if (search) {
+ result = result.filter(
+ (s) =>
+ s.name.toLowerCase().includes(search.toLowerCase()) ||
+ s.description?.toLowerCase().includes(search.toLowerCase()),
+ );
+ }
+ if (sourceFilter) result = result.filter((s) => s.sourceId === sourceFilter);
+ if (categoryFilter) result = result.filter((s) => s.category === categoryFilter);
+ result = [...result];
+ if (sortBy === "rating") result.sort((a, b) => (b.averageRating ?? 0) - (a.averageRating ?? 0));
+ else if (sortBy === "name") result.sort((a, b) => a.name.localeCompare(b.name));
+ return result;
+ }, [skills, search, sourceFilter, categoryFilter, sortBy]);
+
+ // Active filter chips
+ const activeFilters = useMemo(() => {
+ const filters: FilterValue[] = [];
+ if (sourceFilter) filters.push({ key: "source", label: "Source", value: sourceFilter });
+ if (categoryFilter) filters.push({ key: "category", label: "Category", value: categoryFilter });
+ return filters;
+ }, [sourceFilter, categoryFilter]);
+
+ const handleClearFilters = () => {
+ setSourceFilter(null);
+ setCategoryFilter(null);
+ };
+
+ const handleRemoveFilter = (key: string) => {
+ if (key === "source") setSourceFilter(null);
+ if (key === "category") setCategoryFilter(null);
+ };
+
+ // Installed tab grouping
+ const installedGroups = useMemo(() => {
+ const installed = skills.filter((s) => s.activeVersionId && !s.removedAt);
+ if (installed.length === 0) return [];
+ return [{ agentId: "all", agentName: "All Agents", skills: installed }];
+ }, [skills]);
+
+ // Trending tab sections
+ const activeSkills = useMemo(() => skills.filter((s) => !s.removedAt), [skills]);
+
+ const gainingTraction = useMemo(
+ () => [...activeSkills].sort((a, b) => (b.ratingCount ?? 0) - (a.ratingCount ?? 0)).slice(0, 6),
+ [activeSkills],
+ );
+
+ const recentlyUpdated = useMemo(
+ () => [...activeSkills].sort((a, b) => b.id.localeCompare(a.id)).slice(0, 6),
+ [activeSkills],
+ );
+
+ const youMightLike = useMemo(() => {
+ const installedCategories = new Set(
+ activeSkills
+ .filter((s) => s.activeVersionId)
+ .map((s) => s.category)
+ .filter(Boolean),
+ );
+ if (installedCategories.size === 0)
+ return activeSkills.filter((s) => !s.activeVersionId).slice(0, 6);
+ return activeSkills
+ .filter((s) => !s.activeVersionId && s.category && installedCategories.has(s.category))
+ .slice(0, 6);
+ }, [activeSkills]);
+
+ const handleRollback = (skillId: string) => {
+ // Rollback requires a versionId — without version selection UI, use a no-op for now.
+ // Full rollback flow is in Plan 03 (SkillDetail page).
+ pushToast({ title: "Select a version from the skill detail page to roll back.", tone: "info" });
+ void skillId;
+ };
+
+ const handleInstallForAgent = (agentId: string) => {
+ if (!installDialog) return;
+ const dir = agentSkillsDir.trim() || `/agents/${agentId}/.claude/skills`;
+ if (installDialog.isUpdate) {
+ updateMutation.mutate({ skillId: installDialog.skillId, agentSkillsDir: dir });
+ } else {
+ installMutation.mutate({ skillId: installDialog.skillId, agentSkillsDir: dir });
+ }
+ setInstallDialog(null);
+ setAgentSkillsDir("");
+ };
+
+ const tabItems = [
+ { value: "browse", label: "Browse" },
+ { value: "installed", label: "Installed" },
+ { value: "trending", label: "Trending" },
+ ];
+
+ return (
+
+ {/* Header */}
+
+
Skills
+
+
+
+ {/* Error state */}
+ {isError && (
+
+ Failed to load skills. Check that the skill registry backend is running and try again.
+
+ )}
+
+ {/* Loading state */}
+ {isLoading && }
+
+ {/* Tabs */}
+ {!isLoading && (
+ setTab(v as typeof tab)}>
+ setTab(v as typeof tab)} align="start" />
+
+ {/* Browse tab */}
+
+ {/* Toolbar */}
+
+ setSearch(e.target.value)}
+ />
+
+
+
+
+
+ {/* Active filter chips */}
+
+
+ {/* Skill grid */}
+ {filteredSkills.length > 0 && (
+
+ {filteredSkills.map((skill) => (
+ setInstallDialog({ skillId: skill.id })}
+ onUpdate={() => setInstallDialog({ skillId: skill.id, isUpdate: true })}
+ onRollback={() => handleRollback(skill.id)}
+ onUninstall={() => setUninstallDialog({ skillId: skill.id })}
+ />
+ ))}
+
+ )}
+
+ {/* Browse empty state */}
+ {filteredSkills.length === 0 && (
+ fetchMutation.mutate()}
+ />
+ )}
+
+
+ {/* Installed tab */}
+
+ {installedGroups.length === 0 && (
+ setTab("browse")}
+ />
+ )}
+ {installedGroups.map((group) => (
+
+
+
+ {group.skills.length}
+
+
+ {group.skills.map((skill) => (
+ handleRollback(skill.id)}
+ onUninstall={() => setUninstallDialog({ skillId: skill.id })}
+ />
+ ))}
+
+
+ ))}
+
+
+ {/* Trending tab */}
+
+ {skills.length === 0 && (
+
+ )}
+ {skills.length > 0 && (
+ <>
+
+
+ Gaining Traction
+
+
+ {gainingTraction.map((skill) => (
+ setInstallDialog({ skillId: skill.id })}
+ onUpdate={() => setInstallDialog({ skillId: skill.id, isUpdate: true })}
+ onRollback={() => handleRollback(skill.id)}
+ onUninstall={() => setUninstallDialog({ skillId: skill.id })}
+ />
+ ))}
+
+
+
+
+ Recently Updated
+
+
+ {recentlyUpdated.map((skill) => (
+ setInstallDialog({ skillId: skill.id })}
+ onUpdate={() => setInstallDialog({ skillId: skill.id, isUpdate: true })}
+ onRollback={() => handleRollback(skill.id)}
+ onUninstall={() => setUninstallDialog({ skillId: skill.id })}
+ />
+ ))}
+
+
+
+
+ You Might Like
+
+
+ {youMightLike.map((skill) => (
+ setInstallDialog({ skillId: skill.id })}
+ onUpdate={() => setInstallDialog({ skillId: skill.id, isUpdate: true })}
+ onRollback={() => handleRollback(skill.id)}
+ onUninstall={() => setUninstallDialog({ skillId: skill.id })}
+ />
+ ))}
+
+
+ >
+ )}
+
+
+ )}
+
+ {/* Agent selector dialog (install / update) */}
+
+
+ {/* Uninstall confirmation dialog */}
+
+
+ );
}