Files
open-design/docs/schemas/open-design.plugin.v1.json
T
6869b1208b fix(plugins-home): correct deck/scroll preview capture + smoother gallery playback (#4044)
* fix(bake): classify deck-vs-scroll by viewport, real-wheel pan, motion config

Systematic audit of all 126 baked previews surfaced three capture bugs:

- 2 vertical pages misread as decks (the input probe wheel-scrolled them and the
  scroll-driven animation looked like a slide change), so they got walked
  sideways. Classify by viewport height instead: a fixed-viewport page is a deck,
  a vertically-scrollable page is a landing page (pan it) even with a horizontal
  marquee/carousel sub-component.
- 9 scroll-hijack landing pages (custom/transform scroll) that window.scrollTo
  can't move, so the pan was static. Pan those with REAL wheel events
  (page.mouse.wheel), which drive the page's own scroll handler.
- single-screen pages now hold (static) instead of being forced down a deck path.

Plus an opt-in override: authors can declare od.preview.motion ('scroll' | 'deck'
| 'static') and the bake honors it, auto-detecting only when it's absent. Schema
+ plugins-spec document the field. (Also strips a stray NUL byte from the hash
line that made the file read as binary.) BAKE_VERSION -> 4 re-bakes everything.

* perf(plugins-home): only decode visible gallery clips + stream first frame

Two cheap wins for the baked gallery videos:

- Decouple mount from play. The tile mounts the <video> across the wider inView
  margin (so scroll-in/hover never remounts + reloads), but only PLAYS while
  truly visible — off-screen tiles in the mount margin hold their poster frame
  paused instead of all running a simultaneous decode. Adds a 0-margin visible
  observer in PreviewSurface alongside the existing near one.
- preload=metadata instead of auto: paints the first frame off the +faststart
  header instead of eagerly buffering the whole clip up front, so tiles show fast
  and don't saturate the network. The idle hold buffers the pan before hover.

* perf(plugins-home): keep baked clips mounted across a scroll window

Scrolling a tile out of view and back re-showed a load even though the clip
bytes are HTTP-cached (R2 immutable): the <video> unmounted at the tight 120px
margin, so scroll-back remounted a fresh element that re-fetches metadata and
re-decodes the first frame. Add a wide keep-mounted observer (~1500/1800px) so a
clip stays mounted for a few screens — instant scroll-back — while iframes keep
the tight margin and play stays gated to the truly-visible zone (paused, not
unmounted, off screen).

* fix(bake,contracts): probe scroll mechanism before recording; validate motion

Address review:
- Move the window.scrollTo probe before Page.startScreencast so its scrollTo
  160 -> 0 jump isn't baked into the head of the pan as a visible lurch.
- Type od.preview.motion in the Zod PluginManifestSchema (enum scroll|deck|
  static) so an invalid value fails doctor/install instead of silently parsing
  via passthrough and being ignored by the bake; add contract test coverage.

* fix(bake): auto-detect single-screen fixed pages as static, not deck

A fixed-viewport page is only a deck if an input actually advances it; probe the
driver during auto-detect and fall back to 'static' (default viewport + a hold)
when nothing moves it, instead of routing every non-scrollable page through the
deck path where walkSlides(null) just held at the deck-sized capture. Extracted
the arrow/wheel probe into probeDeckDriver(). Verified: a waitlist page now bakes
a 2.5s static hold, guizang still walks, acreage still pans.

---------

Co-authored-by: audit <a@b.c>
2026-06-10 05:43:50 +00:00

249 lines
9.1 KiB
JSON

{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://open-design.ai/schemas/plugin.v1.json",
"title": "Open Design plugin manifest (v1)",
"description": "Schema for `open-design.json` sidecar files. Companion of `SKILL.md`; never duplicates skill body content. See docs/plugins-spec.md §5.",
"type": "object",
"required": ["specVersion", "name", "version"],
"additionalProperties": true,
"properties": {
"$schema": { "type": "string", "format": "uri" },
"specVersion": { "type": "string", "minLength": 1 },
"name": { "type": "string", "minLength": 1, "pattern": "^[a-z0-9][a-z0-9._-]*$" },
"title": { "type": "string", "minLength": 1 },
"title_i18n": { "$ref": "#/$defs/LocalizedText" },
"version": { "type": "string", "minLength": 1 },
"description": { "type": "string" },
"description_i18n": { "$ref": "#/$defs/LocalizedText" },
"author": {
"type": "object",
"properties": {
"name": { "type": "string" },
"url": { "type": "string", "format": "uri" }
},
"additionalProperties": true
},
"license": { "type": "string" },
"homepage": { "type": "string", "format": "uri" },
"icon": { "type": "string" },
"tags": { "type": "array", "items": { "type": "string" } },
"compat": {
"type": "object",
"properties": {
"agentSkills": { "type": "array", "items": { "$ref": "#/$defs/RefPath" } },
"claudePlugins": { "type": "array", "items": { "$ref": "#/$defs/RefPath" } }
},
"additionalProperties": true
},
"od": {
"type": "object",
"properties": {
"kind": { "type": "string", "enum": ["skill", "scenario", "atom", "bundle"] },
"taskKind": { "type": "string", "enum": ["new-generation", "code-migration", "figma-migration", "tune-collab"] },
"mode": { "type": "string" },
"platform": { "type": "string" },
"scenario": { "type": "string" },
"engineRequirements": {
"type": "object",
"properties": { "od": { "type": "string" } },
"additionalProperties": true
},
"preview": {
"type": "object",
"properties": {
"type": { "type": "string" },
"entry": { "type": "string" },
"poster": { "type": "string" },
"video": { "type": "string" },
"gif": { "type": "string" },
"motion": {
"type": "string",
"enum": ["scroll", "deck", "static"],
"description": "How the gallery should capture this HTML preview's hover clip: 'scroll' = vertical-scroll landing page (pan top->bottom), 'deck' = horizontal slideshow (walk slides), 'static' = single fixed screen (hold its in-place animation). Omit to auto-detect from the page's scroll height."
}
},
"additionalProperties": true
},
"useCase": {
"type": "object",
"properties": {
"query": {
"oneOf": [
{ "type": "string" },
{ "$ref": "#/$defs/LocalizedText" }
]
},
"exampleOutputs": {
"type": "array",
"items": {
"type": "object",
"required": ["path"],
"properties": {
"path": { "type": "string" },
"title": { "type": "string" }
},
"additionalProperties": true
}
}
},
"additionalProperties": true
},
"context": {
"type": "object",
"properties": {
"skills": { "type": "array", "items": { "$ref": "#/$defs/Reference" } },
"designSystem": {
"oneOf": [
{ "$ref": "#/$defs/Reference" },
{ "type": "object", "properties": { "ref": { "type": "string" }, "primary": { "type": "boolean" } }, "additionalProperties": true }
]
},
"craft": { "type": "array", "items": { "type": "string" } },
"assets": { "type": "array", "items": { "type": "string" } },
"claudePlugins": { "type": "array", "items": { "$ref": "#/$defs/Reference" } },
"mcp": { "type": "array", "items": { "$ref": "#/$defs/McpServerSpec" } },
"atoms": { "type": "array", "items": { "type": "string" } }
},
"additionalProperties": true
},
"pipeline": {
"type": "object",
"required": ["stages"],
"properties": {
"stages": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "atoms"],
"properties": {
"id": { "type": "string", "minLength": 1 },
"atoms": { "type": "array", "items": { "type": "string" } },
"repeat": { "type": "boolean" },
"until": { "type": "string" },
"onFailure": { "type": "string", "enum": ["abort", "skip", "retry"] }
},
"additionalProperties": true
}
}
},
"additionalProperties": true
},
"genui": {
"type": "object",
"properties": {
"surfaces": {
"type": "array",
"items": { "$ref": "#/$defs/GenUISurface" }
}
},
"additionalProperties": true
},
"connectors": {
"type": "object",
"properties": {
"required": { "type": "array", "items": { "$ref": "#/$defs/ConnectorRef" } },
"optional": { "type": "array", "items": { "$ref": "#/$defs/ConnectorRef" } }
},
"additionalProperties": true
},
"inputs": {
"type": "array",
"items": { "$ref": "#/$defs/InputField" }
},
"capabilities": { "type": "array", "items": { "type": "string" } }
},
"additionalProperties": true
}
},
"$defs": {
"RefPath": {
"type": "object",
"required": ["path"],
"properties": { "path": { "type": "string", "minLength": 1 } },
"additionalProperties": true
},
"LocalizedText": {
"type": "object",
"minProperties": 1,
"additionalProperties": { "type": "string" }
},
"Reference": {
"type": "object",
"properties": {
"ref": { "type": "string" },
"path": { "type": "string" }
},
"additionalProperties": true
},
"McpServerSpec": {
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string" },
"command": { "type": "string" },
"args": { "type": "array", "items": { "type": "string" } },
"env": { "type": "object", "additionalProperties": { "type": "string" } },
"url": { "type": "string" }
},
"additionalProperties": true
},
"GenUISurface": {
"type": "object",
"required": ["id", "kind", "persist"],
"properties": {
"id": { "type": "string", "minLength": 1 },
"kind": { "type": "string", "enum": ["form", "choice", "confirmation", "oauth-prompt"] },
"persist": { "type": "string", "enum": ["run", "conversation", "project"] },
"trigger": {
"type": "object",
"properties": {
"stageId": { "type": "string" },
"atom": { "type": "string" }
},
"additionalProperties": true
},
"schema": { "type": "object" },
"prompt": { "type": "string" },
"capabilitiesRequired": { "type": "array", "items": { "type": "string" } },
"timeout": { "type": "integer", "minimum": 1 },
"onTimeout": { "type": "string", "enum": ["abort", "default", "skip"] },
"default": {},
"oauth": {
"type": "object",
"required": ["route"],
"properties": {
"route": { "type": "string", "enum": ["connector", "mcp", "plugin"] },
"connectorId": { "type": "string" },
"mcpServerId": { "type": "string" }
},
"additionalProperties": true
}
},
"additionalProperties": true
},
"ConnectorRef": {
"type": "object",
"required": ["id"],
"properties": {
"id": { "type": "string" },
"tools": { "type": "array", "items": { "type": "string" } }
},
"additionalProperties": true
},
"InputField": {
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string", "minLength": 1 },
"label": { "type": "string" },
"type": { "type": "string", "enum": ["string", "text", "select", "number", "boolean", "file"] },
"required": { "type": "boolean" },
"options": { "type": "array", "items": { "type": "string" } },
"placeholder": { "type": "string" },
"default": {}
},
"additionalProperties": true
}
}
}