Markdown Edit/Preview tabs + read-only .md viewer for skills & profiles
Adds MarkdownEditor (react-markdown + remark-gfm, no raw HTML — authored/retrieved content is data, not markup) with Edit | Preview tabs, wired into the AGENTS.md and SKILL.md editors, the agent persona, and the review artifact. Adds a read-only "View" on every skill and agent-profile card — including builtins, which previously had no way to be inspected at all — rendering the full SKILL.md / AGENTS.md (frontmatter + body + actions/golden tests). Collapses a same-version builtin that an org has forked so its own copy shadows it, keeping the version picker unambiguous and the item clearly editable/versionable. Also lands the agent-face wiring on the seat configurator (a live xl preview with a state cycler) and the review inbox header. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { BookMarked, Download, GitFork, Pencil, Plus, Store, Trash2, Upload } from 'lucide-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'
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} 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'
|
||||
|
||||
@@ -121,6 +122,31 @@ function bumpPatch(version: string): string {
|
||||
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)
|
||||
@@ -128,6 +154,7 @@ export function SkillsPage() {
|
||||
const [skills, setSkills] = useState<SkillSummary[]>([])
|
||||
const [marketplace, setMarketplace] = useState<MarketplaceEntry[]>([])
|
||||
const [form, setForm] = useState<FormState | null>(null)
|
||||
const [preview, setPreview] = useState<{ title: string; content: string } | null>(null)
|
||||
const [busy, setBusy] = useState(false)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
@@ -156,9 +183,32 @@ export function SkillsPage() {
|
||||
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<string, SkillSummary>()
|
||||
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<SkillDetail[]>(`/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<SkillDetail[]>(`/api/skills/${key}?organizationId=${organizationId}`)
|
||||
@@ -289,6 +339,7 @@ export function SkillsPage() {
|
||||
key={key}
|
||||
versions={versions}
|
||||
busy={busy}
|
||||
onView={(v) => openView(key, v)}
|
||||
onNewVersion={(v) => openForm(key, v, 'version')}
|
||||
onEdit={(v) => openForm(key, v, 'edit')}
|
||||
onFork={(v) => fork(key, v)}
|
||||
@@ -446,7 +497,12 @@ export function SkillsPage() {
|
||||
</Repeater>
|
||||
|
||||
<Field label="Body (the prompt the agent runs)">
|
||||
<Textarea rows={8} value={form.body} onChange={(e) => setForm({ ...form, body: e.target.value })} placeholder="You are the engineer. Turn the input into…" />
|
||||
<MarkdownEditor
|
||||
rows={8}
|
||||
value={form.body}
|
||||
onChange={(body) => setForm({ ...form, body })}
|
||||
placeholder="You are the engineer. Turn the input into…"
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
@@ -459,6 +515,20 @@ export function SkillsPage() {
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)}
|
||||
|
||||
{preview && (
|
||||
<Sheet open onOpenChange={(o) => !o && setPreview(null)}>
|
||||
<SheetContent className="flex w-full flex-col gap-0 overflow-y-auto sm:max-w-xl">
|
||||
<SheetHeader>
|
||||
<SheetTitle>{preview.title}</SheetTitle>
|
||||
<SheetDescription>The full SKILL.md — read-only. Fork or make a new version to edit.</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex flex-col gap-4 px-4 pb-6">
|
||||
<MarkdownEditor rows={20} mono frontmatter value={preview.content} />
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)}
|
||||
</AppShell>
|
||||
)
|
||||
}
|
||||
@@ -466,6 +536,7 @@ export function SkillsPage() {
|
||||
function SkillGroupCard({
|
||||
versions,
|
||||
busy,
|
||||
onView,
|
||||
onNewVersion,
|
||||
onEdit,
|
||||
onFork,
|
||||
@@ -474,6 +545,7 @@ function SkillGroupCard({
|
||||
}: {
|
||||
versions: SkillSummary[]
|
||||
busy: boolean
|
||||
onView: (version: string) => void
|
||||
onNewVersion: (version: string) => void
|
||||
onEdit: (version: string) => void
|
||||
onFork: (version: string) => void
|
||||
@@ -514,6 +586,9 @@ function SkillGroupCard({
|
||||
{isListed && <Badge variant="default" className="gap-1"><Store className="size-3" /> Listed</Badge>}
|
||||
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<Button size="sm" variant="ghost" onClick={() => onView(current.version)}>
|
||||
<Eye data-icon="inline-start" /> View
|
||||
</Button>
|
||||
{isBuiltin ? (
|
||||
<Button size="sm" variant="outline" disabled={busy} onClick={() => onFork(current.version)}>
|
||||
<GitFork data-icon="inline-start" /> Fork to my org
|
||||
|
||||
Reference in New Issue
Block a user