Fully Headless UI
Build any UI — chat or not — on top of the CopilotKit primitives with zero UI opinions.
This feature (headless-complete) hasn't been tagged in any Claude Agent SDK (Python) cell yet. Try LangGraph (Python) instead, or browse the framework-agnostic version.
What is this?
A headless UI gives you full control over the chat experience — you bring your own components, layout, and styling while CopilotKit handles agent communication, message management, tool-call rendering, and streaming. No <CopilotChat>, no slot overrides, just your components composed on top of the low-level hooks.
When should I use this?
Use headless UI when:
- The slot system isn't enough — you need a completely different layout.
- You're embedding chat into an existing UI with its own patterns.
- You're building a non-chat surface that still talks to an agent (a dashboard, a canvas, an inspector) and want
useRenderToolCall/useRenderActivityMessageon their own. - You want to render generative UI primitives outside of a chat entirely.
The core hooks
Three hooks do the heavy lifting — they're the same primitives <CopilotChat> uses internally.
useAgent({ agentId })— exposes the current conversation (messages,isRunning) and the run-state object.useCopilotKit()— returns the runtime handle you callrunAgent({ agent })on.useRenderToolCall()— returns a function that paints any registered tool call inline.
Minimal example
Start small — a hand-rolled message list and composer built from useAgent + useCopilotKit:
claude-sdk-python::headless-simple. Known demos are bundled from manifest demos[i]; check the cell id and framework slug.The message list is a plain .map() over agent.messages — user messages render as right-aligned bubbles, assistant messages render streamed text plus inline tool calls via renderToolCall({ toolCall }):
claude-sdk-python::headless-simple. Known demos are bundled from manifest demos[i]; check the cell id and framework slug.No <CopilotChat />, no slots. The trade-off: you only get text + tool calls. Reasoning messages, activity messages, and custom before/after slots won't show up unless you wire them in yourself — which is exactly what the complete example covers.
Complete example
The headless-complete cell rebuilds the full generative-UI composition — text, tool calls, reasoning cards, A2UI + MCP Apps activity messages, custom before/after message slots — from the low-level hooks directly, without importing <CopilotChatMessageView>.
The useRenderedMessages hook
The cell's central piece is a hand-rolled useRenderedMessages(messages, isRunning) that returns the same flat list of messages, each augmented with a renderedContent: ReactNode field. This hook is a manual recreation of what <CopilotChatMessageView> does:
claude-sdk-python::headless-complete. Known demos are bundled from manifest demos[i]; check the cell id and framework slug.Three low-level hooks feed it:
useRenderToolCall()— returns the renderer for any registered tool call (per-tool viauseRenderTool/useComponent, plus the wildcard fromuseDefaultRenderTool).useRenderActivityMessage()— renders A2UI + MCP Apps activity messages for the current agent scope.useRenderCustomMessages()— invokesrenderCustomMessagehooks registered against the activeCopilotChatConfigurationProvider, emitting"before"and"after"slots around every message.
Per-role dispatch
The role-switch mirrors CopilotChatMessageView's renderMessageBlock exactly — assistant bodies get text + tool calls, user bodies get their text content, reasoning messages go through the <CopilotChatReasoningMessage> leaf, and activity messages route through renderActivityMessage:
claude-sdk-python::headless-complete. Known demos are bundled from manifest demos[i]; check the cell id and framework slug.Tool-call composition
For each toolCall on an assistant message, we look up the sibling tool-role message (keyed by toolCallId) and hand both to renderToolCall:
claude-sdk-python::headless-complete. Known demos are bundled from manifest demos[i]; check the cell id and framework slug.Bubble chrome
The UserBubble and AssistantBubble components are pure chrome — they receive the pre-rendered node from useRenderedMessages and drop it into a styled container. No chat primitives are imported here:
claude-sdk-python::headless-complete. Known demos are bundled from manifest demos[i]; check the cell id and framework slug.Next steps
- Slots — less work than going fully headless, often enough.
- CSS customization — when you just need to re-skin the defaults.