Back to Copilotkit

Headless UI

showcase/shell-docs/src/content/docs/headless.mdx

1.57.05.2 KB
Original Source

Full rendering control via hooks

CopilotKit's headless hooks give you complete control over the chat experience: you compose messages, streaming, and tool-call surfaces yourself with zero UI opinions. Bring your own design system and render everything your way.

There are two live cells on this page. Start with Minimal for the smallest possible custom chat on useAgent + useCopilotKit, then jump to Complete to see the full generative-UI composition (tool calls, reasoning, activity messages, custom before/after slots) rebuilt by hand from the low-level hooks.

When should I use this?

Use headless UI when you want to:

  • Build a completely custom chat interface with your own design system
  • Integrate agent chat into existing UI patterns
  • Have full control over message rendering and interaction
  • Drop generative UI primitives (useRenderToolCall, useRenderActivityMessage, useRenderCustomMessages) into a layout that isn't a chat at all

Minimal (headless-simple)

The bare minimum: three hooks do the heavy lifting.

  • useAgent({ agentId }) exposes the current conversation (messages, isRunning) and the run-state object.
  • useCopilotKit() returns the runtime handle you call runAgent({ agent }) on (the same entry point <CopilotChat /> uses internally).
  • useComponent(...) (sugar over useFrontendTool) lets you register a React component the agent can render by invoking a named tool call. useRenderToolCall() then returns a function that paints any tool call inline.
<Snippet region="use-agent-simple" cell="headless-simple" title="frontend/src/app/page.tsx — useAgent + useCopilotKit + useComponent" />

The message list is a plain .map() over agent.messages: user messages render as right-aligned bubbles, assistant messages render any streamed text plus inline tool calls via renderToolCall({ toolCall }):

<Snippet region="message-list-simple" cell="headless-simple" title="frontend/src/app/page.tsx — message list" />

That's it: no <CopilotChat />, no <CopilotChatMessageView>, no slots. The downside: you only get text + tool calls. Reasoning messages, activity messages (A2UI, MCP Apps), and custom before/after slots won't show up unless you wire them in yourself, which is exactly what the next section covers.

Complete (headless-complete)

This is the heart of the page. The headless-complete cell rebuilds the full generative-UI weave (text, tool calls via useRenderTool / useDefaultRenderTool / useComponent / useFrontendTool, reasoning cards, A2UI + MCP Apps activity messages, and custom before/after message slots) from the low-level hooks directly, without importing <CopilotChatMessageView> or <CopilotChatAssistantMessage>.

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; compare it line-for-line against the renderMessageBlock helper inside the canonical primitive: packages/react-core/src/v2/components/chat/CopilotChatMessageView.tsx:542-612.

<Snippet region="use-rendered-messages-hook" title="frontend/src/app/use-rendered-messages.tsx — composition hook" />

Three low-level hooks feed it:

  • useRenderToolCall() — returns the renderer for any registered tool call (per-tool via useRenderTool / useComponent, plus the wildcard from useDefaultRenderTool).
  • useRenderActivityMessage() — renders A2UI + MCP Apps activity messages for the current agent scope.
  • useRenderCustomMessages() — invokes renderCustomMessage hooks registered against the active CopilotChatConfigurationProvider, emitting "before" and "after" slots around every message.

Per-role dispatch

Inside renderMessageContent 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 component, and activity messages route through renderActivityMessage:

<Snippet region="manual-activity-message-rendering" title="frontend/src/app/use-rendered-messages.tsx — per-role dispatch" />

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. This mirrors CopilotChatToolCallsView exactly:

<Snippet region="manual-tool-call-rendering" title="frontend/src/app/use-rendered-messages.tsx — assistant + tool calls" />

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:

<Snippet region="custom-bubbles" title="frontend/src/app/{user,assistant}-bubble.tsx — pure chrome" /> <IntegrationGrid path="headless" />