Generative UI primitive (legacy)

Render agent-described React UI from a JSON spec with a consumer-provided component allowlist, using the MessagePrimitive.GenerativeUI primitive.

This is the original generative UI primitive. For new code, use the present tool: it ships a component vocabulary, generates the model-facing schema for you, and renders inside the stock Thread without extra wiring. This page stays for integrations that already emit generative-ui message parts.

MessagePrimitive.GenerativeUI renders UI described by the agent at runtime as a JSON spec. Instead of hard-coding a component per tool, the agent emits a generative-ui message part containing a tree of components by name. assistant-ui resolves each name against a consumer-provided allowlist and renders the result.

The allowlist controls which components the agent may render: any name not in it throws a typed GenerativeUIRenderError (no implicit fallback). It does not constrain the props passed to those components; see Security.

The default shadcn Thread does not render generative-ui parts. You must wire the primitive explicitly, see Opt-in wiring.

When not to use the primitive

Quick start

1. Define your component allowlist

components/gui.tsx
const Card = ({ title, children }) => (
  <div className="rounded-xl border bg-card p-4 shadow-sm">
    <div className="text-base font-semibold">{title}</div>
    <div className="mt-2">{children}</div>
  </div>
);

const Button = ({ label }) => (
  <button className="rounded-md bg-primary px-3 py-1.5 text-primary-foreground">
    {label}
  </button>
);

export const componentsAllowlist = { Card, Button };

2. Wire the primitive into your message renderer

See Opt-in wiring for all three integration patterns.

3. Have the agent emit UI

ExternalStore / manual messages attach a native part:

{
  type: "generative-ui",
  spec: {
    root: {
      component: "Card",
      props: { title: "Welcome" },
      children: [
        { component: "Button", props: { label: "Get started" } },
      ],
    },
  },
}

AI SDK (useChatRuntime) maps tool results to tool-call parts, not generative-ui parts. Use the AI SDK interim bridge.

Live routes in examples/with-generative-ui: static primitive (/primitive), GUI chat bridge (/gui-chat).

Opt-in wiring

The stock @assistant-ui/ui Thread switch returns null for unknown part types, including generative-ui. Add one of these patterns in your assistant message renderer.

Pattern 1 — MessagePrimitive.Parts

<MessagePrimitive.Parts
  components={{
    generativeUI: {
      components: componentsAllowlist,
      Fallback: UnknownComponentFallback,
    },
  }}
/>

Pattern 2 — GroupedParts case (shadcn Thread fork)

case "generative-ui":
  return (
    <MessagePrimitive.GenerativeUI
      components={componentsAllowlist}
      Fallback={UnknownComponentFallback}
    />
  );

Also exclude render_gui from tool-group chrome in groupBy if you use the AI SDK bridge (return null for that tool name).

Pattern 3 — AI SDK interim bridge

When using useChatRuntime, map a dedicated tool result to the renderer:

case "tool-call":
  if (part.toolName === "render_gui") {
    const spec = parseRenderGuiResult(part.result);
    if (spec) {
      return (
        <MessagePrimitive.GenerativeUI
          spec={spec}
          components={componentsAllowlist}
          Fallback={UnknownComponentFallback}
        />
      );
    }
  }
  return part.toolUI ?? <ToolFallback {...part} />;

The message store still holds a tool-call on this path, not a generative-ui part. See examples/with-generative-ui/app/gui-chat for a working reference.

Bare strings act as inline text leaves.

Spec shape

type GenerativeUINode =
  | string
  | {
      component: string;             // resolved against the allowlist
      props?: Record<string, unknown>;
      children?: GenerativeUINode[];
      key?: string;                   // optional stable React key
    };

type GenerativeUISpec = {
  root: GenerativeUINode | GenerativeUINode[];
};

The spec is plain JSON, easy for any agent to emit and easy to validate on the server before delivery.

This is a different shape from the flat $type tree the present tool uses. The two are not interchangeable, and the Slack, Teams, and A2UI converters only accept the $type form.

Streaming

When a message contains native generative-ui parts whose spec updates incrementally (for example via ExternalStore), the primitive renders progressively as nodes and props arrive.

The AI SDK render_gui tool path returns the full spec at tool completion, not incrementally during the tool execute step. For args streaming during generation, use Tool UI instead.

Security

The allowlist is the boundary on which components render: a spec can only instantiate components you put in the registry, with no eval and no dynamic import (names are looked up in the registry and nothing else). An unknown name throws GenerativeUIRenderError or invokes your Fallback.

It does not constrain the props the agent supplies. Spec props are spread directly onto your allowlisted components, so treat every allowlisted component as receiving untrusted input: never forward agent-supplied props into dangerouslySetInnerHTML, validate or reject href and src values (for example block javascript: URLs), and avoid passing spec props anywhere they become executable. The safest allowlisted components accept only primitive, display-oriented props.

Error handling

Unknown component names throw GenerativeUIRenderError with a typed componentName field. Catch it with a React error boundary, or pass a Fallback component to opt into a soft-fail UX:

<MessagePrimitive.GenerativeUI
  components={componentsAllowlist}
  Fallback={({ component }) => (
    <span className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">
      unknown component: {component}
    </span>
  )}
/>

Composing with other primitives

generative-ui is a regular MessagePart type, so it composes cleanly with MessagePrimitive.Parts, MessagePrimitive.PartByIndex, and MessagePrimitive.GroupedParts. Render it alongside text, tool calls, and reasoning in the same message.