Velloo documentation
Markdown for LLMs ↗Extensions and live islands
Hand-registering a host-app component Velloo can't simply render, with placeholders by default and opt-in live mounts for charts.
Sometimes a screen needs a component Velloo can’t simply render for you: a chart wired to your data layer, a widget that only works inside a runtime the canvas has no way to stand up. Extensions let the agent tell Velloo the component exists, what props it takes, and where it imports from, so the node can sit in a screen tree, render as something on the canvas, and emit as a real import in code.
Declaring one
Extensions live in the extensions map of .design/config.json, usually written by the agent via the add_extension tool:
"extensions": {
"DataTable": {
"importPath": "@/components/data-table",
"category": "ui",
"description": "Sortable, paginated table",
"props": [
{ "name": "data", "type": "any[]", "optional": false, "control": "string" },
{ "name": "sortable", "type": "boolean | undefined", "optional": true, "control": "boolean" }
],
"origin": "agent"
}
}
importPathis emitted by codegen exactly as written: an alias path (@/components/data-table), a relative path, or an npm package. Use the alias your app actually resolves.propsis a hand-authored schema: each entry has aname, a displaytypestring,optional, and acontrol(boolean,number,string,color,enumwithenumValues, oricon). It mirrors a library component’s manifest entry, so the canvas inspector renders the same controls for either.category(uiortypography, defaultui) anddescriptiondrive how the component is grouped and described in the canvas Library.app(monorepos only) names theconfig.hostAppsentry the component lives in, so a live island bundles against that app’s root, aliases, and React copy. Omit it in a single-app folder.originrecords who registered it:"agent"(via the tool) or"manual"(you edited config.json). Informational today.
Once declared, the component is referenced from screens like any other node: in compose JSX it’s just a tag (<DataTable data={[]} sortable={true} />), stored as { "$ref": "DataTable", "props": { … } }. list_components with kind: "extension" lists what’s registered.
Folder-global, and shadowing
Extensions are folder-global: every screen, in every library the folder uses, sees them. An extension also shadows a library component with the same id: register a Button extension and your Button wins over shadcn’s on every screen. add_extension surfaces a shadowedLibraryComponent field in its response when this happens, so an accidental collision is visible immediately; rename unless you meant it.
Lifecycle is agent-driven: update_extension is a sparse patch of props, importPath, description, category, render, fit, and app, and unlisted fields keep their values; remove_extension refuses while any screen or snippet tree still references the id, and returns the offending nodes. There’s no rename: remove and re-add, which naturally surfaces every existing reference first.
What the canvas shows: Tier 1
By default (render: "static", or simply omitted) an extension node renders as a labelled placeholder card: dashed border, the component id, a compact summary of the resolved prop values, and the importPath. Not the real component, but exactly enough to design layout around: it takes up space, participates in flex and grid, and theme-flips with the rest of the canvas because it’s styled with semantic tokens.
Codegen is unaffected by the placeholder: emit_code writes a real import from the importPath and mounts the component with the node’s props.
Live islands: render: "live"
For components whose visual fidelity matters on the canvas (charts, above all), an extension can opt into a live preview:
{ "importPath": "@/components/charts/PriceChart", "render": "live", "props": [ … ] }
Velloo bundles the actual component from your app (resolved from the importPath against the host app and its node_modules, so it’s your exact recharts) and client-mounts it into the server-rendered marker. The SSR skeleton is the same placeholder card, so nothing breaks when the bundle isn’t ready; any bundle or render failure falls back to the placeholder rather than erroring the canvas.
Constraints, by design:
- Visual-only. The mounted island keeps
pointer-events: none. Clicking it selects the node like any other, so the design-mode interaction model survives. No in-canvas tooltips or chart interactions. - Browser-renderable. The component must resolve from the host app and carry no server-only imports.
- Sizing is controlled by
fit:"aspect-video"(default) locks a 16:9 box, right for charts that fill a responsive container, while"content"lets a fixed-height chart or an absolute-inset overlay drive its own height.
This is the one sanctioned crack in “designs are static”: real client React runs inside the island, but selection still wins and the rest of the tree stays inert. Note the built-in Chart component already ships a canvas preview and needs no extension. Reach for render: "live" when the app’s own chart components have to pixel-match.
Extensions vs snippets
| Snippet | Extension | |
|---|---|---|
| What it is | A composition of library components | A component that exists only in your app |
| Lives in | snippets/<id>.json | extensions map in config |
| Canvas render | Fully real | Placeholder, or live island |
| Codegen | Parameterized component or inlined subtree | A real import from importPath |
If you could build it from list_components output, make a snippet. If Velloo would have to reimplement your code to render it, register an extension. Variations of an existing component need neither: that’s props and class overrides.
Next
- The agent-side workflow: Working with your agent.
- Tool signatures: MCP mutations reference.