diff --git a/client/src/pages/SkillsPage.tsx b/client/src/pages/SkillsPage.tsx index 945e39e..77c0191 100644 --- a/client/src/pages/SkillsPage.tsx +++ b/client/src/pages/SkillsPage.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from 'react' -import { BookMarked, GitFork, Pencil, Plus, Store, Trash2 } from 'lucide-react' +import { BookMarked, Download, GitFork, Pencil, Plus, Store, Trash2, Upload } from 'lucide-react' import { toast } from 'sonner' import { AppShell } from '@/components/AppShell' import { Badge } from '@/components/ui/badge' @@ -32,6 +32,7 @@ interface GoldenTest { } interface SkillSummary { + id: string skillKey: string name: string version: string @@ -56,6 +57,11 @@ interface SkillDetail { body: string } +interface MarketplaceEntry { + skill: SkillSummary + alreadyInLibrary: boolean +} + type Mode = 'new' | 'version' | 'edit' interface FormState { @@ -92,7 +98,7 @@ const emptyForm = (): FormState => ({ outputs: '', tools: '', context: '', - visibility: 'public', + visibility: 'private', minTier: 'free', body: '', actions: [], @@ -120,7 +126,7 @@ 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 [marketplace, setMarketplace] = useState([]) const [form, setForm] = useState(null) const [busy, setBusy] = useState(false) @@ -129,7 +135,7 @@ export function SkillsPage() { try { const [lib, market] = await Promise.all([ api.get(`/api/skills?organizationId=${organizationId}`), - api.get(`/api/skills/marketplace`), + api.get(`/api/skills/marketplace?organizationId=${organizationId}`), ]) setSkills(lib) setMarketplace(market) @@ -193,6 +199,32 @@ export function SkillsPage() { } } + 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) @@ -260,6 +292,8 @@ export function SkillsPage() { 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 && ( @@ -269,23 +303,38 @@ export function SkillsPage() { ) : (

- Public skills shared by other organizations. One-click install lands in the next step. + 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((s) => ( - + {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.

} + {marketplace.length === 0 && ( +

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

+ )}
)} @@ -420,16 +469,22 @@ function SkillGroupCard({ onNewVersion, onEdit, onFork, + onPublish, + onUnpublish, }: { versions: SkillSummary[] busy: boolean onNewVersion: (version: string) => void onEdit: (version: string) => void onFork: (version: string) => void + onPublish: (version: string) => void + onUnpublish: (version: string) => void }) { const [selected, setSelected] = useState(versions[0].version) const current = versions.find((v) => v.version === selected) ?? versions[0] const isBuiltin = current.origin === 'Builtin' + const isListed = current.visibility === 'Public' + const canPublish = !isBuiltin && current.status === 'Published' return ( @@ -456,6 +511,7 @@ function SkillGroupCard({ )} {current.roles.map((r) => {r})} {current.goldenTestCount} golden test{current.goldenTestCount === 1 ? '' : 's'} + {isListed && Listed}
{isBuiltin ? ( @@ -463,9 +519,20 @@ function SkillGroupCard({ Fork to my org ) : ( - + <> + + {isListed ? ( + + ) : canPublish ? ( + + ) : null} + )}