Strata generative UI — how Elle builds with Strata

Elle composes runtime UI (“generated sheets”) from a closed, Zod-validated catalog that maps 1:1 onto real Strata components. The catalog is the single source of truth on both sides of the wire; there is no second list to drift. Full interaction doctrine: apps/web-v2/docs/AGENT_APP_UI_CONVENTIONS.md § “Generated sheets compose from the closed catalog”.

The pipeline

packages/ui-schema (@layers/ui-schema)          ← the vocabulary
  src/catalog.ts    ELLE_UI_CATALOG — per-component Zod prop schemas,
                    whenToUse guidance, uiComponent binding, acceptsChildren
  src/tree.ts       ElleUITreeSchema + normalize/validate + node/depth caps
  src/action.ts     ElleUIActionSchema (navigate | message Elle)
  src/prompt.ts     renderCatalogIndex() — the prompt-embedded index

apps/elle                                        ← the producer
  src/tools/render-sheet.ts   the renderSheet tool; inputSchema IS
                              ElleUITreeSchema (validation at the boundary)
  src/prompts/system-prompt.ts embeds renderCatalogIndex() — prompt and
                              schema cannot drift

apps/web-v2                                      ← the renderer
  client/components/elle-ui/registry.tsx  catalog name → @layers/ui adapter
  client/components/elle-ui/renderer.tsx  re-validates EVERY node, fails
                                          closed per node
  client/components/elle-ui/sheet-host.tsx mounts on the canvas behind a
                                          view-link

Doctrine

  • One vocabulary. Every catalog entry maps onto a real Strata component. Never an Elle-only component; never bespoke generated DOM.
  • Fail closed per node. Unknown component or invalid props renders an inline placeholder in that slot; one bad node never takes down the sheet.
  • Grid is the only container (side-by-side intent). The shell owns vertical rhythm — trees never author it.
  • Data binds by dataPath JSON Pointers (/toolResults/<toolName>/…) into the turn’s tool results; prefer pointers over inlining large arrays. An unresolvable pointer renders an honest “data unavailable” placeholder.
  • Actions are the only commitment. A component may carry an ElleUIAction (navigate — validated against the route catalog — or message Elle). Everything else is display.
  • Generated views render on the canvas behind a view-link, never inline in the chat stream.

Adding a component to Elle’s vocabulary

  1. The component exists in packages/ui (or add it there first — see components.md). It must be composable from JSON-serializable props; function/ReactNode props get a data projection (icon names, declarative columns).
  2. Add the entry to ELLE_UI_CATALOG in packages/ui-schema/src/catalog.ts: uiComponent, a sharp whenToUse (this is prompt text — it steers Elle), the Zod props projection, acceptsChildren.
  3. Rebuild the package (pnpm build in packages/ui-schema — it ships dist/). Elle’s system prompt picks the entry up automatically via renderCatalogIndex().
  4. Add the adapter in apps/web-v2/client/components/elle-ui/registry.tsx mapping the catalog props onto the real component (resolve dataPath, map icon names, wire actions).
  5. Verify in the workbench’s catalog browser, then live: Elle runs locally via apps/elle (pnpm dev, Studio at :4111) — a local restart picks up prompt/catalog changes.

Current binding exception (to fix, not to copy): MediaCard/MediaGrid bind to web-v2-local live-post-card — new entries must bind to @layers/ui components so the catalog stays app-agnostic.

Raw markdown for agents: /docs/generative-ui.md