UNPKG

@copilotkit/react-core

Version:

<img src="https://github.com/user-attachments/assets/0a6b64d9-e193-4940-a3f6-60334ac34084" alt="banner" style="border-radius: 12px; border: 2px solid #d6d4fa;" />

290 lines (224 loc) 7.84 kB
# CopilotKit Threads (React) This skill builds on `copilotkit/agent-access`. Durable threads only exist in Intelligence mode — a runtime pointed at `api.cloud.copilotkit.ai` or a self-managed Intelligence instance. In plain SSE mode the hook errors. ## Setup ```tsx "use client"; import { useThreads } from "@copilotkit/react-core/v2"; export function ThreadSidebar({ agentId }: { agentId: string }) { const { threads, isLoading, error, hasMoreThreads, fetchMoreThreads, renameThread, archiveThread, deleteThread, } = useThreads({ agentId }); if (error) return <div className="text-red-500">{error.message}</div>; if (isLoading) return <div>Loading threads…</div>; return ( <ul className="space-y-1"> {threads.map((t) => ( <li key={t.id} className="flex gap-2"> <span>{t.name ?? "Untitled"}</span> <button onClick={() => renameThread(t.id, "Renamed")}>Rename</button> <button onClick={() => archiveThread(t.id)}>Archive</button> </li> ))} {hasMoreThreads && <button onClick={fetchMoreThreads}>Load more</button>} </ul> ); } ``` ## Core Patterns ### Paginated list ```tsx const { threads, hasMoreThreads, fetchMoreThreads, isFetchingMoreThreads } = useThreads({ agentId: "default", limit: 25 }); ``` ### Include archived threads ```tsx const { threads: archived } = useThreads({ agentId: "default", includeArchived: true, }); ``` ### Optimistic archive with error rollback ```tsx const { threads, archiveThread } = useThreads({ agentId: "default" }); async function onArchive(id: string) { try { await archiveThread(id); toast.success("Archived"); } catch (err) { toast.error(`Failed to archive: ${String(err)}`); } } ``` ### Thread-switcher + `<CopilotChat>` ```tsx import { CopilotChat, useThreads } from "@copilotkit/react-core/v2"; import { useState } from "react"; export function ThreadSwitcher() { const { threads } = useThreads({ agentId: "default" }); const [activeId, setActiveId] = useState<string | null>(null); return ( <div className="grid grid-cols-[200px_1fr]"> <ul> {threads.map((t) => ( <li key={t.id}> <button onClick={() => setActiveId(t.id)}> {t.name ?? "Untitled"} </button> </li> ))} </ul> {/* `key` here remounts ONLY <CopilotChat>. Keep it that way: a `key` on an ancestor would remount the app tree below it too. See "Keying a subtree on the active thread id" below. */} {activeId && ( <CopilotChat key={activeId} agentId="default" threadId={activeId} /> )} </div> ); } ``` `activeId` starts as `null` and becomes a real thread id only after the `useThreads` fetch resolves — so this is an **asynchronous, post-mount** change, not something settled during the first render. ## Common Mistakes ### HIGH — Keying a subtree on the active thread id above app state Wrong: ```tsx // app/layout.tsx const { threadId } = useThreadSelection(); return ( <CopilotKitProvider runtimeUrl="/api/copilotkit"> {/* Remounts EVERYTHING below on every thread change. */} <MyAppProvider key={threadId}>{children}</MyAppProvider> </CopilotKitProvider> ); ``` Correct: ```tsx // app/layout.tsx — app state stays mounted across thread changes. return ( <CopilotKitProvider runtimeUrl="/api/copilotkit"> <MyAppProvider>{children}</MyAppProvider> </CopilotKitProvider> ); ``` ```tsx // Reset only what is genuinely per-thread, as deep as possible. <ThreadScopedTranscript key={threadId} /> ``` `key={threadId}` is a legitimate way to reset per-thread state, but it discards **all** state below it — refs, correlation maps, in-flight request bookkeeping, scroll positions. Placed on a layout-level provider it wipes the whole page, with no error and no warning; the symptom surfaces somewhere unrelated, as "our response routing is flaky". Two properties make this hard to catch: - The reset is asynchronous. Durable threads only exist in Intelligence mode, so with a plain SSE runtime `useThreads` returns nothing, the selected thread never changes, and the remount never fires. It appears the moment Intelligence is wired. - It is timing-dependent. Whether state survives depends on whether the user acted before the thread list resolved. Put the `key` on the smallest subtree that genuinely owns per-thread state, and never above state the application expects to keep. If a component both dispatches requests and correlates the responses, it must sit **outside** the keyed subtree. Source: `packages/react-core/src/v2/hooks/use-threads.tsx:282-289` (thread endpoints exist only in Intelligence mode), `364-368` (the list fetch is deferred until `/info` resolves) ### HIGHUsing `useThreads` with an SSE-only runtime Wrong: ```tsx // Runtime has no Intelligence configured new CopilotRuntime({ agents }); // Client side: const { threads, error } = useThreads({ agentId: "default" }); // error: "Runtime URL is not configured" or empty list forever ``` Correct: ```ts // Server — upgrade to Intelligence mode: import { CopilotIntelligenceRuntime, CopilotKitIntelligence, } from "@copilotkit/runtime/v2"; const intelligence = new CopilotKitIntelligence({ // apiUrl / wsUrl default to cloud-hosted CopilotKit Intelligence — leave unset. apiKey: process.env.INTELLIGENCE_API_KEY!, }); const runtime = new CopilotIntelligenceRuntime({ agents, intelligence, identifyUser: async (req) => ({ userId: await getUserId(req) }), }); ``` `CopilotKitIntelligence` and `CopilotIntelligenceRuntime` are only exposed on the `@copilotkit/runtime/v2` subpath — the package root exports SSE primitives only. Thread routes only exist in Intelligence mode. In plain SSE the list fetch fails and mutations reject. Source: `packages/react-core/src/v2/hooks/use-threads.tsx:207-213,229` ### HIGHExpecting `deleteThread` to be recoverable Wrong: ```tsx await deleteThread(id); // user expected a trash bin ``` Correct: ```tsx // For soft-delete UX, use archive: await archiveThread(id); // Then expose archived threads in a separate view: const { threads: archived } = useThreads({ agentId: "default", includeArchived: true, }); ``` `deleteThread` is irreversible in CopilotKit Intelligence. Use `archiveThread` for user-facing delete UX and only call `deleteThread` for genuine "permanently erase" flows. Source: `packages/react-core/src/v2/hooks/use-threads.tsx:101-105` ### MEDIUMAssuming archived threads appear by default Wrong: ```tsx const { threads } = useThreads({ agentId: "default" }); // User archived a thread. User opens the "Archived" tab. It's empty. ``` Correct: ```tsx const { threads: activeThreads } = useThreads({ agentId: "default" }); const { threads: archivedThreads } = useThreads({ agentId: "default", includeArchived: true, }); ``` `includeArchived` defaults to `false`. Archived threads are filtered out of the default list; opt in explicitly for an archived-view tab. Source: `packages/react-core/src/v2/hooks/use-threads.tsx:60-62` ### MEDIUMNot handling `error` Wrong: ```tsx const { threads } = useThreads({ agentId: "default" }); return <ul>{threads.map(...)}</ul>; // Silent failures — handshake errors, network errors all vanish. ``` Correct: ```tsx const { threads, isLoading, error } = useThreads({ agentId: "default" }); if (error) return <ErrorBanner message={error.message} />; if (isLoading) return <Spinner />; return <ul>{threads.map(...)}</ul>; ``` `error` holds the most recent fetch/mutation error until the next successful fetch clears it. Surface it or you'll miss Intelligence-mode mis-configuration. Source: `packages/react-core/src/v2/hooks/use-threads.tsx:70-74`