import { useCallback, useEffect, useMemo, useState } from 'react' import { BookMarked, Download, Eye, GitFork, Pencil, Plus, Store, Trash2, Upload } from 'lucide-react' import { toast } from 'sonner' import { AppShell } from '@/components/AppShell' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet' import { Textarea } from '@/components/ui/textarea' import { MarkdownEditor } from '@/components/MarkdownEditor' import { api } from '@/lib/api' import { useAuth } from '@/store/auth' interface ActionDto { name: string risk: string description?: string | null } interface GoldenTest { input: string expected: string } interface SkillSummary { id: string skillKey: string name: string version: string summary: string | null roles: string[] visibility: string minTier: string status: string origin: string organizationId: string | null goldenTestCount: number actions: ActionDto[] } interface SkillDetail { skill: SkillSummary inputs: string | null outputs: string | null tools: string[] context: string[] goldenTests: GoldenTest[] body: string } interface MarketplaceEntry { skill: SkillSummary alreadyInLibrary: boolean } type Mode = 'new' | 'version' | 'edit' interface FormState { mode: Mode skillKey: string name: string version: string summary: string roles: string inputs: string outputs: string tools: string context: string visibility: string minTier: string body: string actions: ActionDto[] goldenTests: GoldenTest[] } const COMMON_ROLES = 'product-owner, engineer, qa, designer, analyst' const RISKS = ['read', 'draft', 'publish', 'destructive'] const VISIBILITIES = ['public', 'private'] const TIERS = ['free', 'team', 'scale', 'enterprise'] const emptyForm = (): FormState => ({ mode: 'new', skillKey: '', name: '', version: '1.0.0', summary: '', roles: '', inputs: '', outputs: '', tools: '', context: '', visibility: 'private', minTier: 'free', body: '', actions: [], goldenTests: [], }) /** Bump the last numeric segment of a semver-ish string (1.0.0 → 1.0.1). */ function bumpPatch(version: string): string { const parts = version.split('.') for (let i = parts.length - 1; i >= 0; i--) { const n = Number(parts[i]) if (Number.isInteger(n)) { parts[i] = String(n + 1) return parts.join('.') } } return `${version}.1` } const csv = (s: string): string[] => s.split(',').map((x) => x.trim()).filter(Boolean) /** Reconstruct a readable SKILL.md (frontmatter + prompt body + actions/golden tests) for the viewer. */ function skillToMarkdown(d: SkillDetail): string { const s = d.skill const fm = [ `id: ${s.skillKey}`, `name: ${s.name}`, `version: ${s.version}`, s.summary ? `summary: ${s.summary}` : null, s.roles.length ? `roles: [${s.roles.join(', ')}]` : null, d.inputs ? `inputs: ${d.inputs}` : null, d.outputs ? `outputs: ${d.outputs}` : null, d.tools.length ? `tools: [${d.tools.join(', ')}]` : null, d.context.length ? `context: [${d.context.join(', ')}]` : null, `visibility: ${s.visibility === 'PrivateToOrg' ? 'private' : 'public'}`, `min_tier: ${s.minTier.toLowerCase()}`, ].filter(Boolean) const actions = s.actions.length ? `\n\n## Actions\n${s.actions.map((a) => `- **${a.name}** (${a.risk.toLowerCase()})${a.description ? ` — ${a.description}` : ''}`).join('\n')}` : '' const golden = d.goldenTests.length ? `\n\n## Golden tests\n${d.goldenTests.map((g, i) => `${i + 1}. input: \`${g.input}\` → expected: ${g.expected}`).join('\n')}` : '' return `---\n${fm.join('\n')}\n---\n\n${d.body}${actions}${golden}` } /** The org's skill library: builtin starter skills + skills the company authors and versions itself. */ export function SkillsPage() { const organizationId = useAuth((s) => s.organizationId) const [tab, setTab] = useState<'library' | 'marketplace'>('library') const [skills, setSkills] = useState([]) const [marketplace, setMarketplace] = useState([]) const [form, setForm] = useState(null) const [preview, setPreview] = useState<{ title: string; content: string } | null>(null) const [busy, setBusy] = useState(false) const load = useCallback(async () => { if (!organizationId) return try { const [lib, market] = await Promise.all([ api.get(`/api/skills?organizationId=${organizationId}`), api.get(`/api/skills/marketplace?organizationId=${organizationId}`), ]) setSkills(lib) setMarketplace(market) } catch (err) { toast.error((err as Error).message) } }, [organizationId]) useEffect(() => { void load() }, [load]) // Group every version under its key; newest (and the org's own) first comes from the API order. const groups = useMemo(() => { const byKey = new Map() for (const s of skills) { const list = byKey.get(s.skillKey) ?? [] list.push(s) byKey.set(s.skillKey, list) } // If an org has forked a builtin but kept the same version, the org's own copy shadows the builtin // (it's the one that runs and the one you can edit), so the picker shows one clear, editable entry. for (const [key, list] of byKey) { const perVersion = new Map() for (const s of list) { const existing = perVersion.get(s.version) if (!existing || (existing.origin === 'Builtin' && s.origin !== 'Builtin')) perVersion.set(s.version, s) } byKey.set(key, [...perVersion.values()]) } return [...byKey.entries()].sort((a, b) => a[0].localeCompare(b[0])) }, [skills]) // Read-only details: reconstruct the SKILL.md and render it. Works for builtins too — inspect a // skill (frontmatter, prompt body, actions, golden tests) without forking or versioning it. const openView = async (key: string, version: string) => { try { const details = await api.get(`/api/skills/${key}?organizationId=${organizationId}`) const d = details.find((x) => x.skill.version === version) ?? details[0] if (!d) return setPreview({ title: `${d.skill.name} · ${d.skill.version}`, content: skillToMarkdown(d) }) } catch (err) { toast.error((err as Error).message) } } const openForm = async (key: string, version: string, mode: Mode) => { try { const details = await api.get(`/api/skills/${key}?organizationId=${organizationId}`) const d = details.find((x) => x.skill.version === version) ?? details[0] if (!d) return setForm({ mode, skillKey: d.skill.skillKey, name: d.skill.name, version: mode === 'version' ? bumpPatch(d.skill.version) : d.skill.version, summary: d.skill.summary ?? '', roles: d.skill.roles.join(', '), inputs: d.inputs ?? '', outputs: d.outputs ?? '', tools: d.tools.join(', '), context: d.context.join(', '), visibility: d.skill.visibility === 'PrivateToOrg' ? 'private' : 'public', minTier: d.skill.minTier.toLowerCase(), body: d.body, actions: d.skill.actions.map((a) => ({ name: a.name, risk: a.risk.toLowerCase(), description: a.description ?? '' })), goldenTests: d.goldenTests.map((g) => ({ ...g })), }) } catch (err) { toast.error((err as Error).message) } } const fork = async (key: string, version: string) => { setBusy(true) try { await api.post(`/api/skills/${key}/fork`, { organizationId, version }) toast.success(`Forked ${key} into your org — edit it to customize.`) await load() } catch (err) { toast.error((err as Error).message) } finally { setBusy(false) } } const setListed = async (key: string, version: string, listed: boolean) => { setBusy(true) try { await api.post(`/api/skills/${key}/${listed ? 'publish' : 'unpublish'}`, { organizationId, version }) toast.success(listed ? `Published ${key}@${version} to the marketplace.` : `Unlisted ${key}@${version}.`) await load() } catch (err) { toast.error((err as Error).message) } finally { setBusy(false) } } const install = async (sourceSkillId: string, name: string) => { setBusy(true) try { await api.post('/api/skills/install', { organizationId, sourceSkillId }) toast.success(`Installed ${name} into your library.`) await load() } catch (err) { toast.error((err as Error).message) } finally { setBusy(false) } } const save = async () => { if (!form) return setBusy(true) try { await api.post('/api/skills/authored', { organizationId, skillKey: form.skillKey.trim(), name: form.name.trim(), version: form.version.trim(), summary: form.summary.trim() || null, roles: csv(form.roles), inputs: form.inputs.trim() || null, outputs: form.outputs.trim() || null, tools: csv(form.tools), context: csv(form.context), visibility: form.visibility, minTier: form.minTier, body: form.body, actions: form.actions .filter((a) => a.name.trim()) .map((a) => ({ name: a.name.trim(), risk: a.risk, description: a.description?.trim() || null })), goldenTests: form.goldenTests.filter((g) => g.input.trim() && g.expected.trim()), }) toast.success(`Saved ${form.skillKey}@${form.version}.`) setForm(null) await load() } catch (err) { toast.error((err as Error).message) } finally { setBusy(false) } } const willPublish = !!form && csv(form.roles).length > 0 && form.goldenTests.some((g) => g.input.trim() && g.expected.trim()) return (

Skills

Your company's skill library. Builtin starter skills are shared; author and version your own.

setTab('library')} icon={BookMarked}>Library setTab('marketplace')} icon={Store}>Marketplace
{tab === 'library' ? (
{groups.map(([key, versions]) => ( openView(key, v)} onNewVersion={(v) => openForm(key, v, 'version')} onEdit={(v) => openForm(key, v, 'edit')} onFork={(v) => fork(key, v)} onPublish={(v) => setListed(key, v, true)} onUnpublish={(v) => setListed(key, v, false)} /> ))} {groups.length === 0 && (

No skills yet. Run a Git sync for builtins, or author one.

)}
) : (

Published skills shared by other organizations. Install a copy into your library — it lands private, so you can edit or version it freely.

{marketplace.map(({ skill: s, alreadyInLibrary }) => ( {s.name} {s.version} {s.skillKey} {s.summary} {s.roles.map((r) => {r})} {s.goldenTestCount} golden test{s.goldenTestCount === 1 ? '' : 's'} {alreadyInLibrary ? ( In your library ) : ( )} ))} {marketplace.length === 0 && (

Nothing published yet. Publish one of your own skills to share it here.

)}
)}
{form && ( !o && setForm(null)}> {form.mode === 'new' ? 'New skill' : form.mode === 'version' ? `New version of ${form.skillKey}` : `Edit ${form.skillKey}`} {willPublish ? 'Has roles + a golden test — saves as Published.' : 'Add ≥1 role and ≥1 golden test to publish; otherwise saved as Draft.'}
setForm({ ...form, skillKey: e.target.value })} placeholder="api-endpoint-design" /> setForm({ ...form, version: e.target.value })} />
setForm({ ...form, name: e.target.value })} placeholder="API Endpoint Design" /> setForm({ ...form, summary: e.target.value })} /> setForm({ ...form, roles: e.target.value })} placeholder="engineer" />
setForm({ ...form, inputs: e.target.value })} /> setForm({ ...form, outputs: e.target.value })} />
setForm({ ...form, visibility: v })} /> setForm({ ...form, minTier: v })} />
setForm({ ...form, tools: e.target.value })} /> setForm({ ...form, context: e.target.value })} />
setForm({ ...form, actions: [...form.actions, { name: '', risk: 'draft', description: '' }] })} > {form.actions.map((a, i) => (
setForm({ ...form, actions: form.actions.map((x, j) => (j === i ? { ...x, name: e.target.value } : x)) })} /> setForm({ ...form, actions: form.actions.map((x, j) => (j === i ? { ...x, risk: v } : x)) })} />
))}
setForm({ ...form, goldenTests: [...form.goldenTests, { input: '', expected: '' }] })} > {form.goldenTests.map((g, i) => (
setForm({ ...form, goldenTests: form.goldenTests.map((x, j) => (j === i ? { ...x, input: e.target.value } : x)) })} />