Threads Drawer

Drop-in threads drawer that lists, switches, and manages conversations next to your chat.

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?#

<CopilotThreadsDrawer> gives your app a ready-made conversation sidebar. Users can list, switch, start, archive, and delete their threads, and you wire up no active-thread state to make it work. Drop it next to your chat and it connects automatically through a shared chat configuration.

If your project was created from a CopilotKit CLI starter, the Drawer and its shared configuration may already be included. Otherwise, use the setup below to add it to an existing Vue application.

A support desk application with a Threads Drawer listing customer conversations beside CopilotChat

Under the hood, the Vue component is an SSR-safe wrapper around the self-contained copilotkit-threads-drawer web component. It is fed by useThreads and the current chat configuration.

Prefer to build your own thread UI? The same data and actions are available through useThreads; see Headless Threads. Thread rename is one of those headless actions: the prebuilt drawer covers archive, unarchive, and delete, so use your own rows when rename is required.

When should I use this?#

Use the drawer when you want:

  • A persistent thread switcher beside your chat
  • Thread switching, new conversations, archive, and delete with zero wiring
  • A self-contained, themeable UI that follows your app's light/dark theme
  • A mobile-ready off-canvas drawer with its own launcher

It requires CopilotKit Intelligence because threads are stored and synchronized server-side. Get a free developer account to set that up.

For multi-user applications, configure the runtime to scope Rich Threads to the signed-in user.

Threads run in CopilotKit Intelligence
Get persistent threads and realtime sync on the free Developer tier.
Get CopilotKit Intelligence free

Set up the Threads Drawer#

Place <CopilotThreadsDrawer> next to <CopilotChat> and wrap both in a shared <CopilotChatConfigurationProvider>. That shared configuration lets the Drawer drive the chat: selecting a row connects the chat to that thread and replays its history; the New Conversation row resets the chat to a fresh welcome screen.

src/App.vue
<script setup lang="ts">
import {
  CopilotChat,
  CopilotChatConfigurationProvider,
  CopilotKitProvider,
  CopilotThreadsDrawer,
} from "@copilotkit/vue/v2";
import "@copilotkit/vue/styles.css";
</script>

<template>
  <CopilotKitProvider
    runtime-url="/api/copilotkit"
    public-license-key="ck_pub_..."
  >
    <CopilotChatConfigurationProvider agent-id="support">
      <div style="display: flex; height: 100dvh">
        <CopilotThreadsDrawer />
        <CopilotChat />
      </div>
    </CopilotChatConfigurationProvider>
  </CopilotKitProvider>
</template>

That's the whole integration. A hand-built sidebar would track the active thread id and pass selection/new-thread handlers between the list and chat; the shared configuration owns that state for the Drawer.

Add your own "New conversation" button#

The Drawer has its own "New Conversation" row. Your app can also keep its own button, for example in a toolbar or a page header. Call startNewThread() from useCopilotChatConfiguration, the same configuration method the Drawer calls for its own row.

src/components/NewConversationButton.vue
<script setup lang="ts">
import { useCopilotChatConfiguration } from "@copilotkit/vue/v2";

const configuration = useCopilotChatConfiguration();

function startNewConversation() {
  configuration.value?.startNewThread();
}
</script>

<template>
  <button type="button" @click="startNewConversation">New conversation</button>
</template>

Put the button inside the same <CopilotChatConfigurationProvider> as the Drawer and chat:

src/App.vue with a toolbar button
<script setup lang="ts">
import {
  CopilotChat,
  CopilotChatConfigurationProvider,
  CopilotKitProvider,
  CopilotThreadsDrawer,
} from "@copilotkit/vue/v2";
import NewConversationButton from "./components/NewConversationButton.vue";
import "@copilotkit/vue/styles.css";
</script>

<template>
  <CopilotKitProvider
    runtime-url="/api/copilotkit"
    public-license-key="ck_pub_..."
  >
    <CopilotChatConfigurationProvider agent-id="support">
      <div style="display: flex; height: 100dvh">
        <CopilotThreadsDrawer />
        <main style="flex: 1; display: flex; flex-direction: column">
          <header><NewConversationButton /></header>
          <CopilotChat />
        </main>
      </div>
    </CopilotChatConfigurationProvider>
  </CopilotKitProvider>
</template>

How it works:

  • Outside a configuration provider, useCopilotChatConfiguration() returns a computed ref containing null, so the button does nothing.

  • startNewThread() gives the chat a new thread id and shows its welcome screen. The Drawer reads the active thread from the same configuration, so no row stays selected.

  • The new thread appears in the Drawer list after its first run. An empty thread does not get a row.

  • If you set threadId on <CopilotChatConfigurationProvider>, your app controls the thread. Then the configuration's startNewThread() and setActiveThreadId() do nothing and log a warning. Keep the id in your own state and pass the Drawer onThreadSelect and onNewThread callbacks. Your button updates the same state:

    src/components/ControlledChat.vue
    <script setup lang="ts">
    import { ref } from "vue";
    import {
      CopilotChat,
      CopilotChatConfigurationProvider,
      CopilotThreadsDrawer,
    } from "@copilotkit/vue/v2";
    
    const threadId = ref<string>(crypto.randomUUID());
    const selectThread = (id: string) => {
      threadId.value = id;
    };
    const startNewConversation = () => {
      threadId.value = crypto.randomUUID();
    };
    </script>
    
    <template>
      <CopilotChatConfigurationProvider :thread-id="threadId">
        <CopilotThreadsDrawer
          :on-thread-select="selectThread"
          :on-new-thread="startNewConversation"
        />
        <button type="button" @click="startNewConversation">
          New conversation
        </button>
        <CopilotChat />
      </CopilotChatConfigurationProvider>
    </template>

    Render ControlledChat inside your <CopilotKitProvider>.

To show only your button, hide the Drawer's own new-conversation controls with their CSS parts. new-thread-button is the row in the open Drawer. launcher-new-thread is the (+) button in the closed or collapsed launcher.

copilotkit-threads-drawer::part(new-thread-button),
copilotkit-threads-drawer::part(launcher-new-thread) {
  display: none;
}

Configuring the drawer#

<CopilotThreadsDrawer> works with zero props. Its optional props are:

PropDescription
agentIdAgent whose threads to list; defaults to the chat configuration's agent.
labelAccessible name for the drawer region and listbox; defaults to "Threads".
recentLabelHeading above the list; defaults to "Recent Conversations".
onThreadSelectEscape hatch to take over thread selection.
onNewThreadEscape hatch to handle the New Conversation row.
onLicensedCalled when the user clicks Upgrade in the locked drawer. This is not a license-check completion notification.
licenseUrlOverride for the licensing destination.
collapsibleWhether the expanded desktop drawer can collapse; defaults to true.
limitPage size. The drawer shows Load more while another page exists.

See the Vue Threads Drawer reference for the full prop and event types.

Customization#

The drawer renders in a shadow root with self-contained, theme-inheriting styles. Vue exposes bounded escape hatches:

  • Named web-component slots: project children with a slot attribute such as header, empty, footer, memories, or launcher-icon.

    <CopilotThreadsDrawer>
      <span slot="header">My conversations</span>
    </CopilotThreadsDrawer>
  • Per-row content: use the Vue #row="{ thread }" scoped slot. Defining it replaces every row's inner content, so reproduce the default markup inside the slot when you need a per-row fallback.

    <CopilotThreadsDrawer>
      <template #row="{ thread }">
        <strong>{{ thread.name ?? "New conversation" }}</strong>
      </template>
    </CopilotThreadsDrawer>
  • CSS parts and variables: style the web component's structural ::part() targets or override its --cpk-drawer-* custom properties. The reference lists the supported parts and tokens.

License#

Threads require CopilotKit Intelligence. Without a valid Threads license, the drawer shows a locked view in place of the list.