Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion COPY.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ These exact strings. Do not paraphrase them per page.
| Demo page | What /{name} made Β· One recorded run: the prompt, and what the Agent built with the Skill. | guidance only: the H1 and line of a demo page, `/skills/demos/<owner>/<repo>/<name>`. The meta title is "{name} skill example: {noun}" for the query "<skill> skill example", with `noun` from `DEMO_GROUPS`. The meta description is "{agent} made this {noun} with the /{name} skill from one prompt: β€œ{prompt}”", cut at a word to 160 characters |
| Demo groups | Films and launch videos Β· Landing pages Β· UI components Β· Diagrams and explainers | guidance only: `DEMO_GROUPS` in `shared/demo-groups.ts`: the `/skills/demos` group headings and their lines |
| Demo stage | Open the demo | guidance only: the demo stage link. On the homepage it opens the demo page; on a demo page it opens the Skill page Demo panel |
| Demo panel | Demo Β· Prompt Β· Open live demo Β· Open in a new tab Β· Run it yourself Β· All demos | guidance only: the Skill page Demo panel. "Show screenshots" replaces "Open live demo" while the live output shows. "All demos" opens the demo page and hides while the Skill holds a run check flag |
| Demo panel | Demo Β· Prompt Β· Open live demo Β· Open in a new tab Β· Run it yourself Β· All demos | guidance only: the Skill page Demo panel. "Show screenshots" replaces "Open live demo" while the live output shows. "All demos" opens the demo page. It hides for a Skill page only demo, and while the Skill holds a run check flag |
| Demo provenance | {Agent logo} {model}, such as Opus 5.5 | guidance only: the recording line on the Skill page panel and the homepage stage. Screen readers and the title get "Recorded with {agent}, {model}". No date and no GitHub link: the Skill page is where visitors inspect a Skill |
| Demo outdated | Recorded on an older version of this Skill. | guidance only: under the Demo panel when the Skill moved past the recorded commit |
| Home section: changes | Keep up with skill changes. | guidance only: the homepage section heading for watching and the digest |
Expand Down
2 changes: 2 additions & 0 deletions GLOSSARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ Collisions

**Rule:** a demo shows the Agent's output, never the Skill author's work. It always names how it was recorded.

**Placement:** a demo shows on its Skill page, the homepage, `/skills/demos`, and its own demo page. A Skill page only demo shows on its Skill page alone.

**Groups:** demos group by what the Skill makes, on `/skills/demos`: films and launch videos, landing pages, UI components, and diagrams and explainers. A group is not a track: tracks sort Skills by kind of work.

### preview
Expand Down
2 changes: 1 addition & 1 deletion layers/registry/app/components/SkillDetail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1750,7 +1750,7 @@ useHead(computed(() => ({

<!-- The header width, like Related: the demo answers the header question, not the file viewer one. -->
<div v-if="data.demo" class="mx-auto max-w-5xl px-4 pt-6 sm:px-6 md:pt-8">
<SkillDemo :demo="data.demo" :on-board="!runFlag" />
<SkillDemo :demo="data.demo" :on-board="!runFlag && !data.demo.skillPageOnly" />
</div>

<!-- The two command blocks are breakpoint twins, so neither can hold the anchor. -->
Expand Down
7 changes: 4 additions & 3 deletions layers/registry/app/components/_SkillDemo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import SkillDemoShot from './_SkillDemoShot.vue'
* Skill, and how it was recorded. Screenshots paint first. The live output
* loads only on request, in a frame with no same-origin access, because the
* route serves it under a CSP sandbox too. "All demos" opens the demo's own
* page, the board of every demo with this one on the stage. A Skill with a run
* check flag is off the board, so its panel has no such link.
* page, the board of every demo with this one on the stage. A Skill page only
* demo, or one whose Skill holds a run check flag, is off the board, so its
* panel has no such link.
*/
const { demo, onBoard = true } = defineProps<{
demo: SkillDemoView
/** False while the Skill holds a run check flag: the board leaves it out, and its demo page answers 404. */
/** False for a Skill page only demo, or while the Skill holds a run check flag: the board leaves it out, and its demo page answers 404. */
onBoard?: boolean
}>()

Expand Down
32 changes: 32 additions & 0 deletions layers/registry/server/data/skill-demos.json
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,38 @@
}
]
},
{
"owner": "thebstar",
"repo": "shipreel",
"name": "shipreel",
"makes": "film",
"authorName": "Bikram Sutar",
"sourceUrl": "https://github.com/theBstar/shipreel/blob/main/plugins/shipreel/skills/shipreel/SKILL.md",
"prompt": "Make a shipreel for the idempotent-bookings branch. It has no pull request yet, so read the change from git against main. Don't stop to ask me questions. Save the final video as walkthrough.mp4 in this folder.",
"setup": "The folder held Mossgrove, a fictional garden-plot booking API, with the change on its own branch. Its shipreel.yml set a local Piper voice and a 60-second target.",
"skillPageOnly": true,
"agent": "Claude Code",
"agentVersion": "2.1.292",
"model": "claude-opus-5-5",
"skillCommit": "0d6978b76e0ce94355ea896ff164a359a4fe94c6",
"recordedAt": "2026-10-07",
"video": {
"file": "video-7c69af2173.mp4",
"poster": "poster-b751e14017.jpg",
"width": 1280,
"height": 720,
"durationSeconds": 62.7
},
"shots": [
{
"file": "poster-b751e14017.jpg",
"width": 1280,
"height": 720,
"viewport": "desktop",
"alt": "A frame from the video the Agent rendered for: Make a shipreel for the idempotent-bookings branch. It has no pull request yet, so read the change from git against main"
}
]
},
{
"owner": "tt-a1i",
"repo": "archify",
Expand Down
18 changes: 15 additions & 3 deletions layers/registry/server/utils/skill-demos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import manifest from '../data/skill-demos.json'
* Media lives at `DEMO_MEDIA_ORIGIN`, the bucket's custom domain, under
* content-hashed names, so a URL never changes what it serves.
*
* A demo marked `skillPageOnly` shows on its Skill page and nowhere else:
* not on `/skills/demos`, the homepage, a demo page of its own, or the
* `demos` sitemap.
*
* Cull path: delete the entry, its `server/demos/<owner>/<repo>/<name>/`
* folder, and the bucket's `demos/<owner>/<repo>/<name>/` prefix.
*/
Expand Down Expand Up @@ -72,12 +76,16 @@ const demoSchema = z.object({
recordedAt: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
/** Lower comes first on the homepage. Unpinned demos follow, newest first. */
pin: z.number().int().positive().optional(),
/** Shown on the Skill page alone: left off the board, the homepage, and the `demos` sitemap, with no demo page. */
skillPageOnly: z.literal(true).optional(),
/** File name inside `server/demos/<owner>/<repo>/<name>/`, served sandboxed as the live demo. */
outputFile: z.string().regex(/^[\w-]+\.html$/).optional(),
video: videoSchema.optional(),
shots: z.array(shotSchema).min(1),
}).refine(demo => demo.outputFile !== undefined || demo.video !== undefined, {
message: 'A demo needs an output page or a video.',
}).refine(demo => !(demo.skillPageOnly && demo.pin), {
message: 'A Skill page only demo takes no pin: a pin orders the homepage, which leaves it out.',
})

export type SkillDemoRecord = z.infer<typeof demoSchema>
Expand All @@ -101,12 +109,13 @@ export function listSkillDemos(demos: readonly SkillDemoRecord[] = SKILL_DEMOS):
}

/**
* The demos a visitor may see. A demo whose Skill holds a run check flag stays
* out until a check passes: a visitor who watches it would copy a run command
* The demos the board, the homepage, and the demo pages show. A Skill page
* only demo stays out. So does a demo whose Skill holds a run check flag,
* until a check passes: a visitor who watches it would copy a run command
* that fails.
*/
export function listShownSkillDemos(flagged: ReadonlySet<string>, demos: readonly SkillDemoRecord[] = SKILL_DEMOS): readonly SkillDemoRecord[] {
return listSkillDemos(demos).filter(demo => !flagged.has(runCheckFlagKey(demo.owner, demo.repo, demo.name)))
return listSkillDemos(demos).filter(demo => !demo.skillPageOnly && !flagged.has(runCheckFlagKey(demo.owner, demo.repo, demo.name)))
}

export interface DemoSitemapEntry {
Expand Down Expand Up @@ -171,6 +180,8 @@ export interface SkillDemoView {
recordedAt: string
/** True when the Skill moved past the commit the demo recorded. */
outdated: boolean
/** Shown on the Skill page alone, so the Demo panel links to no demo page. */
skillPageOnly: boolean
/** The sandboxed output page, when the demo kept one. */
liveUrl: string | null
video: SkillDemoVideo | null
Expand All @@ -196,6 +207,7 @@ export function presentSkillDemo(demo: SkillDemoRecord, currentCommit: string |
skillCommit: demo.skillCommit,
recordedAt: demo.recordedAt,
outdated: currentCommit !== null && currentCommit !== demo.skillCommit,
skillPageOnly: demo.skillPageOnly ?? false,
liveUrl: demo.outputFile ? `${path}/live` : null,
video: demo.video
? {
Expand Down
37 changes: 22 additions & 15 deletions scripts/record-skill-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
* pnpm demo:record owner/repo/skill --makes diagram --prompt "..." --output diagram.html --shell
*
* `--makes` is the demo's group on /skills/demos: one of DEMO_MAKES in
* shared/demo-groups.ts.
* shared/demo-groups.ts. `--skill-page-only` keeps the demo on its Skill page
* alone: off /skills/demos, the homepage, and the demos sitemap. A re-record
* keeps an entry Skill page only.
* pnpm demo:record owner/repo/skill --reshoot
* pnpm demo:record owner/repo/skill --makes slides --prompt "..." --output deck.html --resume /tmp/skilld-demo-XXXX --model claude-opus-5-5
*
Expand Down Expand Up @@ -168,6 +170,7 @@ interface DemoEntry {
skillCommit: string
recordedAt: string
pin?: number
skillPageOnly?: true
outputFile?: string
video?: Video
shots: Shot[]
Expand All @@ -183,7 +186,7 @@ type OutputKind = 'page' | 'video'
type Shell = { _tag: 'skilld-run' } | { _tag: 'sandboxed' }

type Parsed
= | { _tag: 'record', owner: string, repo: string, name: string, makes: DemoMakes, prompt: string, output: string, kind: OutputKind, shell: Shell, seed: string | null, setup: string | null, resume: { dir: string, model: string } | null }
= | { _tag: 'record', owner: string, repo: string, name: string, makes: DemoMakes, prompt: string, output: string, kind: OutputKind, shell: Shell, seed: string | null, setup: string | null, skillPageOnly: boolean, resume: { dir: string, model: string } | null }
| { _tag: 'reshoot', owner: string, repo: string, name: string }
| { _tag: 'usage', message: string }

Expand All @@ -192,15 +195,16 @@ function parseInput(argv: string[]): Parsed {
args: argv,
allowPositionals: true,
options: {
prompt: { type: 'string' },
makes: { type: 'string' },
output: { type: 'string', default: 'index.html' },
seed: { type: 'string' },
resume: { type: 'string' },
model: { type: 'string' },
setup: { type: 'string' },
reshoot: { type: 'boolean', default: false },
shell: { type: 'boolean', default: false },
'prompt': { type: 'string' },
'makes': { type: 'string' },
'output': { type: 'string', default: 'index.html' },
'seed': { type: 'string' },
'resume': { type: 'string' },
'model': { type: 'string' },
'setup': { type: 'string' },
'reshoot': { type: 'boolean', default: false },
'shell': { type: 'boolean', default: false },
'skill-page-only': { type: 'boolean', default: false },
},
})
const [ref] = positionals
Expand All @@ -224,7 +228,7 @@ function parseInput(argv: string[]): Parsed {
return { _tag: 'usage', message: 'A --resume folder needs --model: the model the interrupted run used.' }
const resumed = values.resume && values.model ? { dir: resolve(values.resume), model: values.model } : null
const shell: Shell = kind === 'video' || values.shell ? { _tag: 'sandboxed' } : { _tag: 'skilld-run' }
return { _tag: 'record', owner, repo, name, makes, prompt: values.prompt, output: values.output, kind, shell, seed: values.seed ? resolve(values.seed) : null, setup: values.setup ?? null, resume: resumed }
return { _tag: 'record', owner, repo, name, makes, prompt: values.prompt, output: values.output, kind, shell, seed: values.seed ? resolve(values.seed) : null, setup: values.setup ?? null, skillPageOnly: values['skill-page-only'], resume: resumed }
}

interface SkillSource {
Expand Down Expand Up @@ -463,7 +467,9 @@ async function writeEntry(entry: DemoEntry): Promise<void> {
const key = (demo: DemoEntry) => `${demo.owner}/${demo.repo}/${demo.name}`.toLowerCase()
const previous = manifest.demos.find(demo => key(demo) === key(entry))
const demos = manifest.demos.filter(demo => key(demo) !== key(entry))
demos.push(previous?.pin && !entry.pin ? { ...entry, pin: previous.pin } : entry)
// A re-record keeps the earlier placement. A pin orders the homepage, so a Skill page only demo takes none.
const placed: DemoEntry = previous?.skillPageOnly ? { ...entry, skillPageOnly: true } : entry
demos.push(previous?.pin && !placed.pin && !placed.skillPageOnly ? { ...placed, pin: previous.pin } : placed)
demos.sort((a, b) => key(a).localeCompare(key(b)))
await writeFile(MANIFEST, `${JSON.stringify({ demos }, null, 2)}\n`)
}
Expand Down Expand Up @@ -534,15 +540,15 @@ async function reshoot(owner: string, repo: string, name: string): Promise<void>
async function main(): Promise<void> {
const input = parseInput(process.argv.slice(2))
if (input._tag === 'usage') {
console.error(`${input.message}\nUsage: pnpm demo:record owner/repo/skill --makes <group> --prompt "..." [--output index.html|film.mp4] [--shell] [--seed dir --setup "..."]\n pnpm demo:record owner/repo/skill --reshoot`)
console.error(`${input.message}\nUsage: pnpm demo:record owner/repo/skill --makes <group> --prompt "..." [--output index.html|film.mp4] [--shell] [--seed dir --setup "..."] [--skill-page-only]\n pnpm demo:record owner/repo/skill --reshoot`)
process.exitCode = 2
return
}
if (input._tag === 'reshoot') {
await reshoot(input.owner, input.repo, input.name)
return
}
const { owner, repo, name, makes, prompt, output, kind, shell, seed, setup } = input
const { owner, repo, name, makes, prompt, output, kind, shell, seed, setup, skillPageOnly } = input
const skillRef = `${owner}/${repo}/${name}`
const source = await skillSource(owner, repo, name)
const pinned = await pinnedRun(skillRef)
Expand Down Expand Up @@ -580,6 +586,7 @@ async function main(): Promise<void> {
sourceUrl: source.sourceUrl,
prompt,
...(setup ? { setup } : {}),
...(skillPageOnly ? { skillPageOnly: true as const } : {}),
agent: 'Claude Code',
agentVersion: agent.version,
model: agent.model,
Expand Down
14 changes: 14 additions & 0 deletions test/unit/skill-demos.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ describe('findSkillDemo', () => {
expect(findSkillDemo('Anthropics', 'Skills', 'Frontend-Design', [demo()])?.name).toBe('frontend-design')
})

it('finds a Skill page only demo, so its Skill page still shows it', () => {
expect(findSkillDemo('anthropics', 'skills', 'frontend-design', [demo({ skillPageOnly: true })])?.name).toBe('frontend-design')
})

it('returns nothing for a Skill without a demo', () => {
expect(findSkillDemo('anthropics', 'skills', 'pdf', [demo()])).toBeNull()
})
Expand All @@ -91,6 +95,11 @@ describe('listShownSkillDemos', () => {
const shown = listShownSkillDemos(new Set(['anthropics/skills/flagged']), [demo({ name: 'Flagged' }), demo({ name: 'kept' })])
expect(shown.map(entry => entry.name)).toEqual(['kept'])
})

it('leaves out a Skill page only demo', () => {
const shown = listShownSkillDemos(new Set(), [demo({ name: 'own-page', skillPageOnly: true }), demo({ name: 'kept' })])
expect(shown.map(entry => entry.name)).toEqual(['kept'])
})
})

describe('listDemoSitemapEntries', () => {
Expand All @@ -106,4 +115,9 @@ describe('listDemoSitemapEntries', () => {
it('lists nothing once flags drop the shown demos below six, where the pages answer noindex', () => {
expect(listDemoSitemapEntries(new Set(['anthropics/skills/skill-0']), six)).toEqual([])
})

it('never lists a Skill page only demo, nor counts it toward six', () => {
const [first, ...rest] = six
expect(listDemoSitemapEntries(new Set(), [{ ...first!, skillPageOnly: true }, ...rest])).toEqual([])
})
})
Loading