Skip to content
ChatJS
Esc
navigateopen⌘Jpreview
On this page

Threads

Add branching conversations and parallel responses to your AI SDK app with @chat-js/thread

@chat-js/thread is a standalone package for branching AI conversations. Use it in your existing app without installing the ChatJS starter.

The React hook, useThread, keeps the familiar AI SDK useChat interface for the selected conversation and stores every branch in a complete message tree. The Thread controller provides the same tree and run controls outside React.

Use it when you need to edit earlier messages, compare alternative replies, or move between branches while responses continue streaming.

Try the interactive demo.

Install

bun add @chat-js/thread

The package manages its AI SDK dependencies and uses your app’s existing React instance (React 18 or newer).

Use with React

You need a React app and an AI SDK chat endpoint at /api/chat. useThread() uses that endpoint by default, just like useChat(). Pass a transport option when you need a custom endpoint or request configuration.

Replace the useChat import without changing your existing message list or composer:

- import { useChat } from "@ai-sdk/react";
+ import { useThread } from "@chat-js/thread/react";

- const chat = useChat();
+ const chat = useThread();

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.

Use without React

Import the controller from the core entry point:

import { Thread } from "@chat-js/thread";
import { DefaultChatTransport } from "ai";

const thread = new Thread({
  transport: new DefaultChatTransport({ api: "/api/chat" }),
});

await thread.sendMessage({ text: "Plan a weekend in Lisbon" });
const snapshot = thread.getSnapshot();

Use subscribe() to observe state changes and stopAll() to cancel active requests when your application no longer needs the controller.

Package reference

The package README covers the full interface, transport metadata, and custom state adapters.

Last updated on Sep 10, 2026

Was this page helpful?