> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vendo.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Host components: register branded UI and remix baselines

> Register branded host components with prop schemas and JSON examples so Vendo's generated apps can select and render your own UI natively.

Register host component implementations by the same names used in the catalog
descriptors:

```ts theme={null}
export interface RegisteredComponent {
  name: string;
  description: string;
  propsSchema: StandardSchema;
  propsJsonSchema?: JsonSchema;   // prompt-safe JSON Schema for generation
  examples?: string[];            // JSON prop examples shown to the model
  remixable?: boolean;
}

export type ComponentCatalog = ReadonlyArray<RegisteredComponent>;
```

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.

```ts theme={null}
import { createVendo } from "@vendoai/vendo/server";
import { z } from "zod";

const catalog = [
  {
    name: "MapleSpendingDonut",
    description:
      "Category breakdown of spending. Use when the user asks to see spend by category.",
    propsSchema: z.object({
      categories: z.array(z.object({ label: z.string(), amount: z.number() })),
      currency: z.string().default("USD"),
    }),
    propsJsonSchema: {
      type: "object",
      properties: {
        categories: {
          type: "array",
          items: {
            type: "object",
            properties: { label: { type: "string" }, amount: { type: "number" } },
            required: ["label", "amount"],
          },
        },
        currency: { type: "string" },
      },
      required: ["categories"],
    },
    examples: [
      '{ "categories": [{ "label": "Groceries", "amount": 412.55 }], "currency": "USD" }',
    ],
  },
] as const;

const vendo = createVendo({ model, principal, catalog });
```

* `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:

```
catalog.json: 2 discovered, 2 registered
```

`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](/connect/theming)
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:

```ts theme={null}
await client.apps.rebasePin(appId, slot);
```

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:

```ts theme={null}
import { remixable } from "@vendoai/vendo/react";

const invoiceCard = remixable({
  name: "InvoiceCard",
  component: InvoiceCard,
  exportable: true,
}, import.meta.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`:

```json theme={null}
{
  "format": "vendo/overrides@1",
  "tools": {},
  "remix": { "ignoreSlots": ["ThirdPartyWidget"] }
}
```

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.

```ts theme={null}
export const catalog: ComponentCatalog = [
  {
    name: "NetWorthCard",
    description: "Summary card of net worth over the last 30 days.",
    propsSchema: netWorthSchema,
    remixable: true,
    sampleProps: {
      balance: 12500,
      currency: "USD",
      trend: "up",
    },
  },
];
```

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.
