Skip to main content
Register host component implementations by the same names used in the catalog descriptors:
Names are PascalCase and unique. Descriptions and prop schemas are generation context. Vendo prefers host components when the catalog covers the requested UI.

Register a catalog

Pass the catalog to createVendo so its entries flow into the generation prompt and the engine validates emitted props against the schema before render. Keep each name identical to the key you use in the client components map so source:"host" nodes resolve without an alias.
  • description is selection guidance for the model. State when to pick the component and when to skip it — hedged copy leads to worse selection.
  • propsSchema validates props at render time. Bindings for $path, $state, and zero-arg actions are exempt from that validation.
  • propsJsonSchema is the prompt-safe form the model sees. Keep it serializable so an init or sync flow can reuse the same contract later.
  • examples are JSON strings the model can copy from. One or two concrete shapes usually beat a long prose description.

Auto-extract with sync

vendo sync scans exported JSX components referenced by your <VendoRoot> components map and writes a strict .vendo/catalog.json with each entry’s export path, JSON Schema props, description, and example props. Rescans are deterministic and byte-stable, so the file is safe to commit. Both vendo init and vendo sync print one status line:
createVendo loads .vendo/catalog.json at boot alongside .vendo/theme.json and uses each entry’s JSON Schema for prompt guidance. Explicit createVendo({ catalog }) registrations win by name, so keep code as the source of truth for anything you want strictly validated at render time — disk entries use a pass-through validator because JSON Schema on disk is prompt guidance, not an executable validator. If sync cannot infer a prop type it falls back to a permissive schema and attaches a note; correct the entry by registering the component in code rather than hand-editing the file. A malformed .vendo/catalog.json fails sync loudly, and createVendo logs one actionable error naming the file and telling you to rerun vendo sync.

Review AI-authored copy

Sync can optionally draft descriptions and examples for scanned entries. Any proposed copy is written to .vendo/catalog.proposals.json as a before/after diff — nothing enters .vendo/catalog.json until you accept it through the explicit acceptance API. The runtime never reads the proposals file. Accepted copy is pinned to the deterministic basis (export path, prop schema, note) so a later rescan that changes the underlying component invalidates the accepted copy instead of applying it silently. The prewired primitives are Stack, Row, Grid, Text, Skeleton, Surface, Divider, Card, Button, Input, Select, Table, Badge, Stat, and Tabs. Catalog entries and generated components cannot shadow them. All prewired primitives read the same theme tokens so they pick up your brand’s colors, radii, density, and motion without extra wiring. Card, Button, Badge, Stat, Table, and Tabs are display and action surfaces — interactive controls invoke bound zero-arg action callbacks. Input and Select accept typed input locally, but under the current zero-arg binding contract they cannot round-trip a typed value back through a bound action. Treat them as display plus local input, not as form fields wired to server state. Set remixable only on components whose real frontend source sync may capture. Sync writes a baseline to .vendo/remixable/<slot>.json. Shipping a pin always requires host review of the net diff. A drifted or erroring pin falls back to the original component.

Drift and rebase

When you update a host component and run vendo sync, the new baseline overwrites .vendo/remixable/<slot>.json. Any existing user forks of that pin are now drifted: their recorded pins[].base hash no longer matches the captured baseline. Sync names each drifted slot in its report and points to rebase. Vendo surfaces drift everywhere the fork is opened or edited:
  • The tree renderer shows an in-surface notice above the fork (“The host updated <slot> … Ask the agent to rebase”).
  • open() attaches a server-authoritative pinDrift array to the payload.
  • Edit results include driftedPins so an agent editing a stale fork learns about it at edit time.
  • Ship-diff review fail-closes new in-client approvals for drifted pins.
Drifted forks keep rendering their previous content, sandboxed and untouched. Nothing auto-rebases and no agent turn is auto-triggered — a rebase rewrites the fork through the model, so it stays behind an explicit user or host ask. Rebase re-forks the pin from the new baseline and replays the recorded edit trail through the model, producing one new app version:
Rebase is all-or-nothing. Any replay failure persists nothing and the pre-rebase version stays live; the result reports replayed, remaining, and which intent failed with its issues. On success, pins[].base moves to the new baseline hash and the new version invalidates any existing in-client approval — a reviewer must re-approve. Rebase requires a tree (rung 1) app with a recorded edit trail. A pin that was forked without any subsequent edits, or an app that has graduated to a served HTTP surface, fails closed with conflict — there is no reproducible trail to replay.

When static capture cannot see the source

vendo sync never skips a remixable slot silently. When it cannot follow the registration to real source (an inline component, a dynamic import, a path it cannot resolve, or source outside the project root), it lists the slot with a machine-readable reason and exits non-zero. To capture such a slot at runtime instead, wrap the registration with the remixable helper and pass the module URL:
In development the helper reports the module to your Vendo endpoint, which writes the same .vendo/remixable/<slot>.json baseline sync produces — an existing static baseline is never overwritten, and the capture route is not mounted in production. A slot that is intentionally never capturable can be acknowledged in .vendo/overrides.json:
A remixable component’s source file must expose the component as a default export. Sync accepts named-export registrations, but the jail refuses to render them with must have a React default export. Re-export the component as the default from its own file if the surrounding code needs the named form.

What sync captures for a remixable component

Sync captures enough of the surrounding host to render a fork in the sandboxed jail with its real look and feel:
  • Source imports, two hops deep. Sync follows local JavaScript and TypeScript imports from the captured component for two hops and captures each resolved file. Imports it cannot resolve, refuses (out-of-root or through a symlink escape), or that go beyond two hops are dropped with a named warning. Package imports never enter the jail.
  • Root-level stylesheets. Sync snapshots direct .css imports from your canonical app root (app/layout.*, app/root.*, pages/_app.*, or their src/ variants) and applies them inside the jailed document only. Sync does not follow component-local stylesheet imports and warns when it sees one.
  • Sample props. Sync captures a static, JSON-compatible sampleProps object on the remixable registration verbatim. The runtime uses it as stubbed rehearsal data when a fork renders without live tree props; live props always win when they are present. Non-static sampleProps are skipped with a warning.
Sync rewrites the baseline whenever any captured payload changes, not only the primary component source.

Remixing a captured slot

createVendo loads every valid .vendo/remixable/*.json at startup and hands the baselines to the apps runtime. A missing directory means no baselines. Files that fail schema validation are skipped with a warning and do not break composition. Once a slot is captured, an edit request against the app (edit(appId, instruction), exposed on the wire as POST /api/vendo/apps/:id/edit) can fork the pin instead of only rewriting generated content. The runtime copies the captured source into a deterministic generated component and renders it through the same sandboxed jail as any other generated component. Captured sub-imports resolve through a per-module import table; only the blessed React kit resolves outside that table. It records the pin on the app version as { slot, base: <baseline hash> }. A follow-up edit that touches the pinned subtree lands as a new version on the same app. Baselines carry an exportable flag. When a pinned slot’s baseline is exportable: false, exporting that app fails with the baseline-forbids-export error code. Set exportable: true on baselines you are willing to include in .vendoapp bundles that leave the host. Host components render natively. Generated components remain in the sandboxed iframe jail.