CopilotKit
Render OpenUI Lang in an existing CopilotKit chat.
This guide assumes the application already has a working CopilotKit chat built with CopilotChat, and that its agent responds with OpenUI Lang. If it doesn't yet, follow Set up UI generation first.
The change is in one place: CopilotKit's assistantMessage.markdownRenderer slot. The chat, agent, and runtime stay the same.
Install
Skip this step if the chat UI and the backend share a package and you already installed these in Set up UI generation.
pnpm add @openuidev/react-lang @openuidev/react-ui@openuidev/react-lang renders OpenUI Lang. @openuidev/react-ui provides the built-in component library and its styles; if the agent uses only your own components, skip it and the styles import below.
When using the built-in library, import its styles once, from the application's global stylesheet:
@import "@openuidev/react-ui/styles/index.css";The examples below import library from src/lib/openui.tsx, the file created in Set up UI generation. If the chat UI lives in a separate package, define the same library there so it renders every component the agent can write.
Render OpenUI Lang
Pass the OpenUI renderer as the assistant message's Markdown renderer:
"use client";
import { CopilotChat } from "@copilotkit/react-core/v2";
import { Renderer } from "@openuidev/react-lang";
import { library } from "@/lib/openui";
function OpenUIRenderer({ content }: { content: string }) {
return <Renderer response={content} library={library} />;
}
export function Chat() {
return (
<CopilotChat
messageView={{
assistantMessage: {
markdownRenderer: OpenUIRenderer,
},
}}
/>
);
}content is the accumulated response supplied by CopilotKit. The renderer re-parses it as it grows, so the interface builds up while the response streams.
See The Renderer for all of its props, including error handling.
Handle actions
Buttons, follow-ups, and forms in a generated interface report clicks through the renderer's onAction callback. Without it, they do nothing. Handle the two built-in action types:
continue_conversation: send a new user turn. Include the form values so the model knows what the user entered, as described in Conversation history.open_url: openevent.params.urlin a new tab.
Add the user turn to the agent and run it, as described in CopilotKit's programmatic control guide:
"use client";
import { CopilotChat, useAgent, useCopilotKit } from "@copilotkit/react-core/v2";
import { BuiltinActionType, Renderer, type ActionEvent } from "@openuidev/react-lang";
import { library } from "@/lib/openui";
function toModelMessage(event: ActionEvent) {
const lines = [`User clicked: ${event.humanFriendlyMessage}`];
if (event.formState) lines.push(`Form values: ${JSON.stringify(event.formState)}`);
return lines.join("\n");
}
function OpenUIRenderer({ content }: { content: string }) {
const { agent } = useAgent();
const { copilotkit } = useCopilotKit();
const onAction = (event: ActionEvent) => {
if (event.type === BuiltinActionType.ContinueConversation) {
agent.addMessage({ id: crypto.randomUUID(), role: "user", content: toModelMessage(event) });
void copilotkit.runAgent({ agent });
} else if (event.type === BuiltinActionType.OpenUrl) {
window.open(String(event.params?.url), "_blank", "noopener,noreferrer");
}
};
return <Renderer response={content} library={library} onAction={onAction} />;
}
export function Chat() {
return (
<CopilotChat
messageView={{
assistantMessage: {
markdownRenderer: OpenUIRenderer,
},
}}
/>
);
}copilotkit.runAgent is the same entry point CopilotChat uses when the user sends a message, so the new turn streams into the chat as usual.