"use client"; import { useEffect, useMemo, useState } from "react"; import Link from "next/link"; import { AlertTriangle, BookOpen, ChevronRight, Layers3, } from "lucide-react"; import { PageHeader } from "@/components/common/page-header"; import { HostBadge } from "@/components/common/host-badge"; import { LearningApplicationBadge } from "@/components/common/learning-application-badge"; import { EmptyState } from "@/components/common/empty-state"; import { DeleteAllButton } from "@/components/common/delete-all-button"; import { PageTabs } from "@/components/common/page-tabs"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { reflexio } from "@/lib/reflexio-client"; import { formatRelative } from "@/lib/format"; import { useRequestHostAttribution } from "@/lib/host-attribution"; import { cn } from "@/lib/utils"; import { agentPlaybookStatusLabel, statusLabel, type AgentPlaybookStatusLabel, type StatusLabel, } from "@/lib/status"; import type { AgentPlaybook, Host, PlaybookApplicationStat, UserPlaybook, } from "@/lib/types"; type SkillKind = "project" | "shared"; type SkillStatus = StatusLabel | AgentPlaybookStatusLabel; type SkillSort = "newest" | "applied"; const ALL_LIFECYCLE_STATUSES: (string | null)[] = [null, "pending", "archived"]; const SHARED_STATUS_META: Record< AgentPlaybookStatusLabel, { label: string; description: string } > = { PENDING: { label: "Auto generated", description: "Auto-generated shared skill. It may be updated automatically.", }, APPROVED: { label: "Persisted", description: "Persisted shared skill. It will not be auto updated.", }, REJECTED: { label: "Rejected", description: "Rejected shared skill. It will not be used in claude-smart.", }, }; interface SkillCard { kind: SkillKind; id: number; createdAt: number; content: string; trigger: string | null; rationale: string | null; status: SkillStatus; scopeId: string; host: Host | null; } function projectSkill( p: UserPlaybook, requestHosts?: ReadonlyMap, ): SkillCard { return { kind: "project", id: p.user_playbook_id, createdAt: p.created_at, content: p.content, trigger: p.trigger, rationale: p.rationale, status: statusLabel(p), scopeId: p.user_id || "unknown", host: requestHosts?.get(p.request_id) ?? null, }; } function sharedSkill(p: AgentPlaybook): SkillCard { return { kind: "shared", id: p.agent_playbook_id, createdAt: p.created_at, content: p.content, trigger: p.trigger, rationale: p.rationale, status: agentPlaybookStatusLabel(p), scopeId: p.agent_version || "default", host: null, }; } function skillStatKey(skill: SkillCard): string { const sourceKind = skill.kind === "shared" ? "agent_playbook" : "user_playbook"; return `playbook:${sourceKind}:${skill.id}`; } export default function SkillsPage() { const [projectSkills, setProjectSkills] = useState(null); const [sharedSkills, setSharedSkills] = useState(null); const [appStats, setAppStats] = useState( null, ); const [error, setError] = useState(null); const [activeKind, setActiveKind] = useState("project"); const [scope, setScope] = useState("__all__"); const [statusFilter, setStatusFilter] = useState("CURRENT"); const [sortBy, setSortBy] = useState("newest"); const [search, setSearch] = useState(""); const attribution = useRequestHostAttribution(); useEffect(() => { let cancelled = false; async function load() { try { const [projectRes, sharedRes, statsRes] = await Promise.all([ reflexio.getUserPlaybooks({ limit: 500, statusFilter: ALL_LIFECYCLE_STATUSES, }), reflexio.getAgentPlaybooks({ limit: 500, statusFilter: ALL_LIFECYCLE_STATUSES, }), fetch("/api/rules/applied?daysBack=30&limit=200", { cache: "no-store", }) .then((r) => r.json()) .catch(() => ({ success: false, stats: [] as PlaybookApplicationStat[], })), ]); if (cancelled) return; setProjectSkills(projectRes.user_playbooks ?? []); setSharedSkills(sharedRes.agent_playbooks ?? []); setAppStats(statsRes.stats ?? []); setError(null); } catch (e) { if (!cancelled) setError(e instanceof Error ? e.message : String(e)); } } load(); return () => { cancelled = true; }; }, []); const statsByRule = useMemo(() => { const map = new Map(); for (const s of appStats ?? []) { map.set(`${s.kind}:${s.source_kind ?? "unknown"}:${s.real_id}`, s); } return map; }, [appStats]); const activeSkills = useMemo(() => { return activeKind === "project" ? (projectSkills ?? []).map((playbook) => projectSkill(playbook, attribution?.hosts), ) : (sharedSkills ?? []).map(sharedSkill); }, [activeKind, attribution, projectSkills, sharedSkills]); const scopes = useMemo(() => { const set = new Set(); for (const p of activeSkills) set.add(p.scopeId); return Array.from(set).sort(); }, [activeSkills]); const filtered = useMemo(() => { const matches = activeSkills.filter((p) => { if (scope !== "__all__" && p.scopeId !== scope) return false; if (statusFilter !== "__all__" && p.status !== statusFilter) return false; if (search) { const s = search.toLowerCase(); const hay = `${p.scopeId} ${p.content} ${p.trigger ?? ""} ${p.rationale ?? ""}`.toLowerCase(); if (!hay.includes(s)) return false; } return true; }); return matches.sort((a, b) => { if (sortBy === "applied") { const aStat = statsByRule.get(skillStatKey(a)); const bStat = statsByRule.get(skillStatKey(b)); const appliedDelta = (bStat?.applied_count ?? 0) - (aStat?.applied_count ?? 0); if (appliedDelta !== 0) return appliedDelta; const recencyDelta = (bStat?.last_applied_at ?? 0) - (aStat?.last_applied_at ?? 0); if (recencyDelta !== 0) return recencyDelta; } return b.createdAt - a.createdAt; }); }, [activeSkills, scope, search, sortBy, statsByRule, statusFilter]); const projectCount = projectSkills?.length ?? 0; const sharedCount = sharedSkills?.length ?? 0; const visibleActiveCount = filtered.length; const activeCount = activeKind === "project" ? projectCount : sharedCount; const loading = projectSkills === null || sharedSkills === null; const hasNoSharedSkills = activeKind === "shared" && sharedCount === 0; const allScopesLabel = activeKind === "project" ? "All user scopes" : "All agents"; const switchKind = (kind: SkillKind) => { setActiveKind(kind); setScope("__all__"); setStatusFilter(kind === "project" ? "CURRENT" : "__all__"); }; return (
setSearch(e.target.value)} placeholder="Search" className="h-9 w-48 text-xs bg-background/80" /> 0 ? ` (${activeCount})` : ""}`} confirmMessage={`Delete ALL ${activeCount} ${activeKind === "project" ? "project-specific skills" : "shared skills"}? This cannot be undone.`} disabled={activeCount === 0} onConfirm={async () => { if (activeKind === "project") { await reflexio.deleteAllUserPlaybooks(); setProjectSkills([]); } else { await reflexio.deleteAllAgentPlaybooks(); setSharedSkills([]); } }} />
} />
switchKind(id as SkillKind)} items={[ { id: "project", label: "Project-specific skills", description: "Repo-local rules learned from direct corrections", count: activeKind === "project" ? visibleActiveCount : projectCount, icon: BookOpen, }, { id: "shared", label: "Shared skills", description: "Rollups available across projects", count: activeKind === "shared" ? visibleActiveCount : sharedCount, icon: Layers3, }, ]} /> {error && (
{error}. Is reflexio running on the configured backend URL?
)} {activeKind === "project" && attribution?.unavailable && !error && (
Skill source details are temporarily unavailable. Skills remain available.
)} {loading && !error ? (
Loading...
) : filtered.length === 0 ? ( ) : (
{filtered.map((p) => { const stat = statsByRule.get(skillStatKey(p)); return (
{p.kind === "project" ? "project-specific" : "shared"} {p.kind === "shared" && ( Agent scope: {p.scopeId} )}
{formatRelative(p.createdAt)}

Trigger {p.trigger || "Always applies"}

Rule:{" "} {p.content}

{p.rationale && (

Why:{" "} {p.rationale}

)} {p.kind === "project" && attribution && !attribution.unavailable && (
)} ); })}
)}
); } function statusFilterLabel(kind: SkillKind, status: string): string { if (status === "__all__") return "All"; if (kind === "shared") { const meta = SHARED_STATUS_META[status as AgentPlaybookStatusLabel]; if (meta) return meta.label; } if (status === "CURRENT") return "Current"; if (status === "PENDING") return "Pending"; if (status === "ARCHIVED") return "Archived"; return status; } function StatusBadge({ kind, status, }: { kind: SkillKind; status: SkillStatus; }) { const sharedMeta = kind === "shared" ? SHARED_STATUS_META[status as AgentPlaybookStatusLabel] : null; const variant = status === "CURRENT" || status === "APPROVED" ? "secondary" : status === "ARCHIVED" || status === "REJECTED" ? "outline" : "default"; return ( {sharedMeta?.label ?? status} ); }