4d4bd19
CopilotKitDocs
  • Docs
  • Integrations
  • Reference
  • Free Developer Access
Get Started
QuickstartCoding Agents
Concepts
ArchitectureGenerative UI OverviewOSS vs Enterprise
Agentic Protocols
OverviewAG-UIAG-UI MiddlewareMCPA2A
Build Chat UIs
Prebuilt Components
CopilotChatCopilotSidebarCopilotPopup
Custom Look and Feel
CSS CustomizationSlots (Subcomponents)Fully Headless UIReasoning Messages
Multimodal AttachmentsVoice
Build Generative UI
Controlled
Tool-based Generative UITool RenderingState RenderingReasoning
Your Components
Display ComponentsInteractive Components
Declarative
A2UIDynamic Schema A2UIFixed Schema A2UI
Open-Ended
MCP Apps
Adding Agent Powers
Frontend ToolsShared State
Human-in-the-Loop
HITL OverviewPausing the Agent for InputHeadless Interrupts
Sub-AgentsAgent ConfigProgrammatic Control
Agents & Backends
Built-in Agent
Backend
Copilot RuntimeFactory ModeAG-UI
Runtime Server AdapterAuthentication
LangGraph (Python)
Your Components
Display-onlyInteractiveInterrupt-based
Shared state
Reading agent stateWriting agent stateInput/Output SchemasState streaming
ReadablesInterruptsConfigurableSubgraphsDeep Agents
Advanced
Disabling state streamingManually emitting messagesExiting the agent loop
Persistence
Loading Agent StateThreadsMessage Persistence
Videos
Video: Research Canvas
Error Debugging & ObservabilityCommon LangGraph issues
Troubleshooting Copilots
Migrate to AG-UI
Observe & Operate
InspectorVS Code Extension
Troubleshooting
Common Copilot IssuesError Debugging & ObservabilityDebug ModeAG-UI Event InspectorHook ExplorerError Observability Connectors
Enterprise
CopilotKit PremiumHow the Enterprise Intelligence Platform WorksHow Threads & Persistence WorkObservabilitySelf-Hosting IntelligenceThreads
Deploy
AWS AgentCore
What's New
Full MCP Apps SupportLangGraph Deep Agents in CopilotKitA2UI Launches with full AG-UI SupportCopilotKit v1.50Generative UI Spec SupportA2A and MCP Handshake
Migrate
Migrate to V2Migrate to 1.8.2
Other
Contributing
Code ContributionsDocumentation Contributions
Anonymous Telemetry
LangGraph (Python)TutorialsTutorial: Build a Multi-Conversation Chat App

Tutorial: Build a Multi-Conversation Chat App

Build a chat application with persistent conversation threads using useThreads and CopilotChat — create, switch, rename, and archive conversations with realtime sync.

Info
Early access: Threads and the Enterprise Intelligence Platform are in early access. APIs may change before general availability.
Need an Enterprise Intelligence Platform project?
The free Developer tier on Copilot Cloud has everything this tutorial uses — threads, realtime sync, and a license key.
Get Intelligence free

What you'll build#

A chat application with a thread sidebar — similar to ChatGPT or Claude's conversation list. Users can create new conversations, switch between them, rename them, and archive old ones. All thread metadata syncs in realtime across tabs.

What you'll learn#

  • How to list and manage threads with useThreads
  • How to wire thread selection into CopilotChat via threadId
  • How to create new threads by clearing the active thread
  • How to add rename and archive actions to each thread
  • How pagination works for users with many conversations

Prerequisites#

  • Node.js 20+
  • A CopilotKit project with the Enterprise Intelligence Platform configured (via Copilot Cloud or self-hosted)
  • @copilotkit/react-core v1.50+

Steps#

Scaffold the layout#

Create a two-panel layout: a sidebar for the thread list on the left, and the chat area on the right. We'll use a simple flexbox layout.

App.tsx
tsx
import { CopilotKit } from "@copilotkit/react-core/v2";

export default function App() {
  return (
    <CopilotKit runtimeUrl="/api/copilotkit">
      <div className="flex h-screen">
        <aside className="w-72 border-r overflow-y-auto">
          <ThreadSidebar />
        </aside>
        <main className="flex-1">
          <ChatPanel />
        </main>
      </div>
    </CopilotKit>
  );
}

Build the thread sidebar#

Use useThreads to fetch the thread list and render it. Each thread shows its name (or "New conversation" if unnamed) and the time it was last updated.

ThreadSidebar.tsx
tsx
import { useThreads } from "@copilotkit/react-core/v2"; // [!code highlight]
import { useState } from "react";

export function ThreadSidebar() {
  const { // [!code highlight:5]
    threads,
    isLoading,
    renameThread,
    archiveThread,
  } = useThreads({ agentId: "my-agent" });

  if (isLoading) {
    return <div className="p-4 text-sm text-gray-500">Loading...</div>;
  }

  return (
    <div className="flex flex-col">
      <button
        className="m-3 p-2 rounded bg-blue-600 text-white text-sm"
        onClick={() => {
          // Clear active thread to start a new conversation
          window.dispatchEvent(new CustomEvent("new-thread"));
        }}
      >
        New conversation
      </button>

      {threads.map((thread) => (
        <ThreadRow
          key={thread.id}
          thread={thread}
          onRename={(name) => renameThread(thread.id, name)}
          onArchive={() => archiveThread(thread.id)}
        />
      ))}
    </div>
  );
}

Add thread row with actions#

Each row needs a click handler to select the thread, plus rename and archive actions. We'll use a simple inline editing pattern for rename.

ThreadRow.tsx
tsx
import { useState } from "react";
import type { Thread } from "@copilotkit/react-core/v2";

interface ThreadRowProps {
  thread: Thread;
  onRename: (name: string) => void;
  onArchive: () => void;
}

export function ThreadRow({ thread, onRename, onArchive }: ThreadRowProps) {
  const [isEditing, setIsEditing] = useState(false);
  const [editName, setEditName] = useState(thread.name ?? "");

const displayName = thread.name || "New conversation";
  const timeAgo = new Date(thread.updatedAt).toLocaleDateString();

  return (
    <div
      className="flex items-center justify-between px-3 py-2 hover:bg-gray-100 cursor-pointer group"
      onClick={() => {
        window.dispatchEvent(
          new CustomEvent("select-thread", { detail: thread.id })
        );
      }}
    >
      <div className="flex-1 min-w-0">
        {isEditing ? (
          <input
            className="w-full text-sm border rounded px-1"
            value={editName}
            onChange={(e) => setEditName(e.target.value)}
            onBlur={() => {
              onRename(editName);
              setIsEditing(false);
            }}
            onKeyDown={(e) => {
              if (e.key === "Enter") {
                onRename(editName);
                setIsEditing(false);
              }
            }}
            autoFocus
            onClick={(e) => e.stopPropagation()}
          />
        ) : (
          <>
            <div className="text-sm truncate">{displayName}</div>
            <div className="text-xs text-gray-400">{timeAgo}</div>
          </>
        )}
      </div>

      <div className="hidden group-hover:flex gap-1 ml-2">
        <button
          className="text-xs text-gray-500 hover:text-gray-700"
          onClick={(e) => {
            e.stopPropagation();
            setIsEditing(true);
          }}
        >
          Rename
        </button>
        <button
          className="text-xs text-gray-500 hover:text-red-600"
          onClick={(e) => {
            e.stopPropagation();
            onArchive();
          }}
        >
          Archive
        </button>
      </div>
    </div>
  );
}

Wire up the chat panel#

The chat panel listens for thread selection events and passes the active threadId to CopilotChat. When no thread is selected, starting a conversation creates a new thread automatically.

ChatPanel.tsx
tsx
import { CopilotChat } from "@copilotkit/react-core/v2"; // [!code highlight]
import { useState, useEffect } from "react";

export function ChatPanel() {
const [activeThreadId, setActiveThreadId] = useState<string | undefined>();

  useEffect(() => {
    const handleSelect = (e: CustomEvent) => setActiveThreadId(e.detail);
    const handleNew = () => setActiveThreadId(undefined);

    window.addEventListener("select-thread", handleSelect as EventListener);
    window.addEventListener("new-thread", handleNew);

    return () => {
      window.removeEventListener("select-thread", handleSelect as EventListener);
      window.removeEventListener("new-thread", handleNew);
    };
  }, []);

  return (
    <CopilotChat
      threadId={activeThreadId} {/* [!code highlight] */}
      className="h-full"
    />
  );
}

When threadId is undefined, the chat starts a fresh conversation. When set to an existing thread ID, it loads that thread's message history and reconnects to any active agent stream.

Add pagination (optional)#

If your users accumulate many conversations, add a "Load more" button at the bottom of the sidebar using the limit parameter.

ThreadSidebar.tsx
tsx
const {
  threads,
  isLoading,
  hasMoreThreads, // [!code highlight]
  isFetchingMoreThreads, // [!code highlight]
  fetchMoreThreads, // [!code highlight]
  renameThread,
  archiveThread,
} = useThreads({
  agentId: "my-agent",
  limit: 25, // [!code highlight]
});

// At the bottom of the thread list:
{hasMoreThreads && (
  <button
    className="m-3 p-2 text-sm text-gray-500 hover:text-gray-700"
    onClick={fetchMoreThreads}
    disabled={isFetchingMoreThreads}
  >
    {isFetchingMoreThreads ? "Loading..." : "Load older conversations"}
  </button>
)}

What's next#

You now have a working multi-conversation chat app with persistent threads. Thread names are auto-generated by the LLM after the first message — you'll see them appear in the sidebar automatically. Here are some ideas for extending further:

  • Search — add a search input that filters threads by name
  • Unread indicators — track which threads have new messages since the user last viewed them
  • Drag to reorder — let users pin important threads to the top
  • Archive view — add a toggle to show archived threads using includeArchived: true

Next steps#

  • Step-by-step guide: Threads — the concise how-to for thread management
  • Understand how it works: How Threads & Persistence Work — architecture, event replay model, and WebSocket sync
  • API reference: useThreads — parameters, return values, types
On this page
What you'll buildWhat you'll learnPrerequisitesStepsWhat's nextNext steps