Fully Headless UI

Build a Vue surface on CopilotKit’s reactive agent and runtime primitives.

Open your coding agent in your project's folder, or in an empty folder for a new app.This runs in a coding agent on your computer.

What is this?#

A headless UI gives you full control over the experience. You bring your own components, layout, and styling while CopilotKit handles agent communication, reactive message state, tool registration, and streaming. No CopilotChat shell is required: your Vue components compose the low-level primitives.

When should I use this?#

Use headless UI when:

  • The slot system is not enough for the layout you need.
  • You are embedding chat into an existing interface with its own patterns.
  • You are building a non-chat surface such as a dashboard, canvas, or inspector that still talks to an agent.
  • You need direct control over which message roles and registered renderers appear.

Going headless also transfers responsibility for the composer, accessibility, scroll behavior, attachments, reasoning, interrupts, and every message role to your application unless you deliberately compose the public Vue chat leaves.

The core composables#

These composables provide the same agent and runtime state used by the prebuilt chat:

  • useAgent({ agentId }) exposes the reactive conversation (messages, isRunning, state, and agent methods).
  • useCopilotKit() exposes the runtime ref used to call copilotkit.value.runAgent({ agent }).
  • useRenderActivityMessage() resolves registered MCP Apps and other custom activity renderers for the current agent scope.
  • useRenderCustomMessages() resolves registered before/after message renderers for the active CopilotChatConfigurationProvider.

Composables must execute in a component below CopilotKitProvider; calling one above the provider has no injected runtime context.

Vue has no useRenderToolCall composable

The current Vue public API can register renderers with useRenderTool and useDefaultRenderTool, but it does not expose React's resolver-style useRenderToolCall. Use CopilotChatMessageView when a custom outer layout still needs the registered tool-rendering pipeline, or own tool-call dispatch explicitly through Vue slots.

Minimal example#

Start with a text-only message list and composer built from useAgent and useCopilotKit:

HeadlessChat.vue
<script setup lang="ts">
import { computed, ref } from "vue";
import { useAgent, useCopilotKit } from "@copilotkit/vue/v2";

const { agent } = useAgent({ agentId: "my-agent" });
const { copilotkit } = useCopilotKit();
const input = ref("");
const messages = computed(() => agent.value?.messages ?? []);
const isRunning = computed(() => agent.value?.isRunning ?? false);

async function submit() {
  const current = agent.value;
  const content = input.value.trim();
  if (!current || !content || isRunning.value) return;

  current.addMessage({
    id: crypto.randomUUID(),
    role: "user",
    content,
  });
  input.value = "";
  await copilotkit.value.runAgent({ agent: current });
}
</script>

<template>
  <section aria-live="polite">
    <article v-for="message in messages" :key="message.id" :data-role="message.role">
      {{ message.content }}
    </article>
    <p v-if="isRunning">Thinking…</p>
    <form @submit.prevent="submit">
      <input v-model="input" aria-label="Message" />
      <button type="submit" :disabled="isRunning">Send</button>
    </form>
  </section>
</template>

Render it below the runtime provider and a chat-configuration provider so agent-scoped custom renderers can resolve consistently:

<CopilotKitProvider runtime-url="/api/copilotkit">
  <CopilotChatConfigurationProvider agent-id="my-agent">
    <HeadlessChat />
  </CopilotChatConfigurationProvider>
</CopilotKitProvider>

The trade-off is deliberate: this minimal example renders only text. Tool calls, reasoning messages, activity messages, interrupts, attachments, and custom before/after content do not appear until you wire them in.

Complete example#

For a custom outer layout that should retain CopilotKit's complete message composition, use the public Vue leaves rather than rebuilding their state machines. CopilotChatMessageView already dispatches assistant, user, reasoning, tool, activity, interrupt, and custom before/after content:

RenderedMessages.vue
<script setup lang="ts">
import { computed } from "vue";
import {
  CopilotChatAssistantMessage,
  CopilotChatMessageView,
  CopilotChatUserMessage,
  useAgent,
} from "@copilotkit/vue/v2";

const { agent } = useAgent({ agentId: "my-agent" });
const messages = computed(() => agent.value?.messages ?? []);
const isRunning = computed(() => agent.value?.isRunning ?? false);
</script>

<template>
  <CopilotChatMessageView :messages="messages" :is-running="isRunning">
    <template #assistant-message="assistantProps">
      <article class="assistant-bubble">
        <CopilotChatAssistantMessage v-bind="assistantProps" />
      </article>
    </template>
    <template #user-message="userProps">
      <article class="user-bubble">
        <CopilotChatUserMessage v-bind="userProps" />
      </article>
    </template>
  </CopilotChatMessageView>
</template>

The Vue composition equivalent#

React's complete example manually builds useRenderedMessages() from useRenderToolCall, useRenderActivityMessage, and useRenderCustomMessages. Vue exposes the latter two resolvers but not useRenderToolCall, so there is no honest one-for-one manual hook recreation. CopilotChatMessageView is the supported public composition boundary when you need all registered tool renderers.

Per-role dispatch#

If you choose a completely manual renderer, switch on every AG-UI message role:

  • Assistant messages contain streamed text and optional tool calls.
  • User messages can contain text or multimodal content parts.
  • Reasoning messages can use CopilotChatReasoningMessage.
  • Activity messages can use useRenderActivityMessage().
  • Before/after content can use useRenderCustomMessages() while inside CopilotChatConfigurationProvider.
  • Interrupts require their own pending-state UI and resolve/cancel action.

Missing a role means that content silently disappears from your custom surface.

Tool-call composition#

The prebuilt message view resolves tool calls in this order: a named tool-call-<name> slot, the tool-call fallback slot, then renderers registered through useRenderTool / useDefaultRenderTool. Keep the public message view to retain that behavior. A fully manual Vue surface must own its tool-call rendering because no public resolver composable currently exposes the registered renderer as a callable function.

Bubble chrome#

Keep bubble components as pure chrome around already-rendered public leaves. That lets your design system own spacing, colors, and layout without copying the message parsing, toolbar, Markdown, tool-call, or reasoning logic.

Next steps#