useThread
Build branching AI conversations with a useChat-compatible interface
useThread is the React interface to the Thread controller behind branching
conversations in ChatJS. You keep the familiar AI SDK useChat interface for
the selected conversation, while every message and response branch lives in a
complete tree.
Use it when you need to edit earlier messages, compare alternative replies, or move between branches while responses continue streaming.
Install
bun add @chatjs/thread ai @ai-sdk/react react
Replace the useChat import without changing your existing message list or
composer:
- import { useChat } from "@ai-sdk/react";
+ import { useThread } from "@chatjs/thread/react";
- const chat = useChat({ transport });
+ const chat = useThread({ transport });
The top-level helpers remain compatible with UseChatHelpers, including
messages, sendMessage, regenerate, stop, status, tools, and approvals.
Mental model
useThread separates four concepts:
- Tree: every message and its parent-child relationship
- Cursor: the message currently selected by the application
- Active path: the root-to-cursor messages exposed as
chat.messages - Run: one assistant response with its own status, error, and stop control
Moving the cursor changes the active path. It does not delete descendants or stop runs on other branches.
Create a branch
Select any message and send normally. The new user message becomes a child of the selected node.
chat.tree.setCursor(messageId);
await chat.sendMessage({ text: "Explore another approach" });
Editing is an application-level operation. Select the original user message’s parent, then send the replacement as a new message:
chat.tree.setCursorToParentOf(originalMessageId);
await chat.sendMessage({
id: crypto.randomUUID(),
role: "user",
parts: [{ type: "text", text: editedText }],
});
See Branching for the ChatJS user experience.
Run parallel responses
Each assistant response is an independent run. Start the first response with a new user message, then start alternatives from that same user node:
const userMessageId = crypto.randomUUID();
const primary = await chat.tree.startRun({
message: {
id: userMessageId,
role: "user",
parts: [{ type: "text", text: "Give me three options" }],
},
follow: true,
});
const alternatives = await Promise.all([
chat.tree.startRun({ from: userMessageId, follow: false }),
chat.tree.startRun({ from: userMessageId, follow: false }),
]);
await Promise.all([
primary.finished,
...alternatives.map((run) => run.finished),
]);
follow: false leaves the cursor unchanged. The sibling appears when the first
stream write arrives. ChatJS uses this model for multiple replies from the same
model and for comparisons across different models.
See Parallel Responses for the product flow.
Status and controls
Top-level state describes the selected path:
chat.status;
chat.error;
await chat.stop();
Tree state describes all runs:
chat.tree.status;
chat.tree.activeRuns;
chat.tree.runs;
await chat.tree.stopRun(runId);
await chat.tree.stopAll();
Statuses use the AI SDK values submitted, streaming, ready, and error.
Persist the tree
Save the complete tree instead of only the selected chat.messages path:
const snapshot = chat.tree.getSnapshot();
await saveThread(snapshot);
Restore it with initialTree:
import { DefaultChatTransport } from "ai";
const chat = useThread({
initialTree: savedSnapshot,
transport: new DefaultChatTransport({ api: "/api/chat" }),
});
The snapshot is { version: 1, cursorId, nodes }. Runtime indexes, active
requests, and run adapters are not persisted.
The package README documents the complete interface, transport metadata, and
Thread exports.