Advanced

Conversation Branching

Edit messages to create parallel conversation paths, just like ChatGPT and Claude.ai

Edit any user message to create a parallel conversation path, preserving the original. Navigate between variants with ← N/M → — the same UX as ChatGPT, Claude.ai, and Gemini.


Zero-Config Usage

If you use <CopilotChat />, branching is already active. No code changes needed.

  • Edit button (✏) appears on hover over any user message
  • ← 1/2 → navigator appears below user messages when variants exist
  • Regenerate creates a new branch instead of overwriting
// Nothing to add — branching works out of the box
<CopilotChat className="h-[600px]" />

API

useCopilot()

const {
  switchBranch,   // (messageId: string) => void
  getBranchInfo,  // (messageId: string) => BranchInfo | null
  editMessage,    // (messageId: string, newContent: string) => Promise<void>
  hasBranches,    // boolean — true if any fork exists
  getAllMessages,  // () => UIMessage[] — all branches, not just visible path
} = useCopilot();

BranchInfo

interface BranchInfo {
  siblingIndex: number;    // 0-based — which variant this is
  totalSiblings: number;   // how many variants exist at this fork
  siblingIds: string[];    // ordered oldest-first
  hasPrevious: boolean;
  hasNext: boolean;
}

BranchNavigator component

Standalone navigator for custom message renderers:

import { BranchNavigator } from "@yourgpt/copilot-sdk/ui";

<BranchNavigator
  siblingIndex={info.siblingIndex}
  totalSiblings={info.totalSiblings}
  hasPrevious={info.hasPrevious}
  hasNext={info.hasNext}
  onPrevious={() => switchBranch(info.siblingIds[info.siblingIndex - 1])}
  onNext={() => switchBranch(info.siblingIds[info.siblingIndex + 1])}
/>

MessageTree (framework-agnostic)

import { MessageTree } from "@yourgpt/copilot-sdk";

const tree = new MessageTree(messages);
tree.getVisibleMessages(); // active path only (sent to AI)
tree.getAllMessages();     // all branches (for persistence)
tree.getBranchInfo(messageId); // BranchInfo | null
tree.switchBranch(messageId);
tree.hasBranches; // boolean

Custom Message Renderers

Use getBranchInfo + BranchNavigator in your own message components:

function MyMessage({ message }) {
  const { switchBranch, getBranchInfo } = useCopilot();
  const info = message.role === "user" ? getBranchInfo(message.id) : null;

  return (
    <div>
      <p>{message.content}</p>
      {info && (
        <BranchNavigator
          {...info}
          onPrevious={() => switchBranch(info.siblingIds[info.siblingIndex - 1])}
          onNext={() => switchBranch(info.siblingIds[info.siblingIndex + 1])}
        />
      )}
    </div>
  );
}

Programmatic Branching

// Edit a message (creates new branch from same parent)
await editMessage("msg-abc", "Updated question text");

// Navigate between variants
switchBranch("msg-xyz");

// Check if branches exist
if (hasBranches) {
  const info = getBranchInfo("msg-abc");
  console.log(info.totalSiblings, info.siblingIndex);
}

// Persist all branches (not just visible path)
const allMessages = getAllMessages();
await saveToServer(allMessages);

Persistence

Optional DB columns

ALTER TABLE messages
  ADD COLUMN parent_id TEXT REFERENCES messages(id),
  ADD COLUMN children_ids JSONB DEFAULT '[]';

These columns are optional. Existing rows without them are auto-migrated to a linear tree on load — no data loss, no migration required.

When onMessagesChange fires, the payload contains all messages across all branches:

{
  "id": "msg-abc",
  "role": "assistant",
  "content": "...",
  "parent_id": "msg-xyz",
  "children_ids": []
}

Use upsert-by-ID when saving — a simple overwrite will lose inactive branches:

// ✅ Safe for branching
await db.messages.upsert({ id: msg.id, ...msg });

// ⚠️ Loses inactive branches
await db.threads.update({ messages: visibleMessages });

On this page