Build a screen
Render provider-owned messages with plain props and children. No intermediary message schema, no runtime coupling.
Compose your runtime
Render provider-owned messages directly with Control UI components. Keep native provider types — no intermediary message schema, no store to adopt.
import type { MastraDBMessage } from "@mastra/core/agent/message-list";import { MessageFactory, type MessageRoleRendererProps, type MessageRoleRenderers } from "@mastra/react";import { ChatMessage, ChatMessageBody, ChatMessageContent, ChatMessageRow } from "@/components/control-ui/chat-message";function MessageFrame({ from, children }: MessageRoleRendererProps & { from: "user" | "assistant" | "system" }) { return ( <ChatMessage from={from}> <ChatMessageRow> <ChatMessageBody> <ChatMessageContent>{children}</ChatMessageContent> </ChatMessageBody> </ChatMessageRow> </ChatMessage> );}const roles = { User: (props: MessageRoleRendererProps) => <MessageFrame {...props} from="user" />, Assistant: (props: MessageRoleRendererProps) => <MessageFrame {...props} from="assistant" />, System: (props: MessageRoleRendererProps) => <MessageFrame {...props} from="system" />, Signal: () => null,} satisfies MessageRoleRenderers;export function Message({ message }: { message: MastraDBMessage }) { return ( <MessageFactory message={message} roles={roles} Text={({ text }) => <span>{text}</span>} fallback={(part) => <span>Unsupported message part: {part.type}</span>} /> );}Your provider's own message objects reaching the screen unmapped: the runtime keeps the transport and the lifecycle, the components keep the paint.
One prop convention
Every component reads the same four props, so a call site written for one reads like a call site written for another.
variant picks the recipe, tone picks the semantic color, size picks the control height, and iconOnly collapses the label.render where other libraries use asChild.Browse what is installed: components for the agent surfaces, blocks for complete recipes, primitives for the classic app surface.
The contract
Installed source is owned but never edited. Every look change belongs in the skin: the source is what the update overwrites, and the three skin-owned files are what it never touches.
Components accept plain props and children. Anatomy is stable without runtime metadata, so a skin can re-paint every screen that reads these token names without either side knowing about the other.