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:
soroush.asadi
2026-06-15 15:26:14 +03:30
parent d50cd2790e
commit 4758e4b5de
8 changed files with 1841 additions and 20 deletions
+77 -2
View File
@@ -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