Register a catalog
Pass the catalog tocreateVendo 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.
descriptionis selection guidance for the model. State when to pick the component and when to skip it — hedged copy leads to worse selection.propsSchemavalidates props at render time. Bindings for$path,$state, and zero-arg actions are exempt from that validation.propsJsonSchemais the prompt-safe form the model sees. Keep it serializable so an init or sync flow can reuse the same contract later.examplesare 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 runvendo 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-authoritativepinDriftarray to the payload.- Edit results include
driftedPinsso an agent editing a stale fork learns about it at edit time. - Ship-diff review fail-closes new in-client approvals for drifted pins.
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:
.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:
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
.cssimports from your canonical app root (app/layout.*,app/root.*,pages/_app.*, or theirsrc/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
samplePropsobject 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-staticsamplePropsare skipped with a warning.
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.