Conversations with AI agents are rarely linear. You may want to rephrase a
question, regenerate a response you didn’t like, or explore a completely
different conversational path without losing your previous work. Branching chat
brings version-control semantics to your chat UI. Every edit creates a new
branch, and you can freely navigate between them.
What is branching chat?
Branching chat treats a conversation as a tree rather than a list. Each message
is a node, and editing a message or regenerating a response creates a fork
from that point. The original path is preserved as a sibling branch, so users
can switch back and forth between different conversation trajectories.
Key capabilities:
- Edit any user message: rewrite a previous prompt and re-run the agent
from that point
- Regenerate any AI response: ask the agent to produce a different answer
for the same input
- Navigate branches: switch between different versions of the conversation
using per-message branch controls
Set up useStream with history
To enable branching, pass fetchStateHistory: true so that useStream
retrieves checkpoint metadata needed for branch operations.
Define a TypeScript interface matching your agent’s state schema and pass it as a type parameter to useStream for type-safe access to state values. In the examples below, replace typeof myAgent with your interface name:
import type { BaseMessage } from "@langchain/core/messages";
interface AgentState {
messages: BaseMessage[];
}
import { useStream } from "@langchain/react";
const AGENT_URL = "http://localhost:2024";
export function Chat() {
const stream = useStream<typeof myAgent>({
apiUrl: AGENT_URL,
assistantId: "branching_chat",
fetchStateHistory: true,
});
return (
<div>
{stream.messages.map((msg) => {
const metadata = stream.getMessagesMetadata(msg);
return (
<Message
key={msg.id}
message={msg}
metadata={metadata}
onEdit={(text) => handleEdit(stream, msg, metadata, text)}
onRegenerate={() => handleRegenerate(stream, metadata)}
onBranchSwitch={(id) => stream.setBranch(id)}
/>
);
})}
</div>
);
}
The getMessagesMetadata(msg) function returns branch information for each
message:
interface MessageMetadata {
branch: string;
branchOptions: string[];
firstSeenState: {
parent_checkpoint: Checkpoint | null;
};
}
| Property | Description |
|---|
branch | The branch ID of this specific message version |
branchOptions | Array of all branch IDs available for this message position |
firstSeenState.parent_checkpoint | The checkpoint just before this message—used as the fork point for edits and regenerations |
When a message has only one version, branchOptions contains a single entry.
After an edit or regeneration, new branch IDs are added to branchOptions,
and you can navigate between them.
Edit a message
To edit a user message and create a new branch:
- Get the
parent_checkpoint from the message’s metadata
- Submit the edited message with that checkpoint
- The agent re-runs from that point, creating a new branch
function handleEdit(
stream: ReturnType<typeof useStream>,
originalMsg: HumanMessage,
metadata: MessageMetadata,
newText: string
) {
const checkpoint = metadata.firstSeenState?.parent_checkpoint;
if (!checkpoint) return;
stream.submit(
{
messages: [{ ...originalMsg, content: newText }],
},
{ checkpoint }
);
}
After the edit:
- The message’s
branchOptions gains a new entry
- The view switches to the new branch automatically
- The agent re-runs from the fork point with the updated message
- The original version is preserved and accessible via the branch switcher
Regenerate a response
To regenerate an AI response without changing the input:
- Get the
parent_checkpoint from the AI message’s metadata
- Submit with
undefined input and the parent checkpoint
- The agent produces a fresh response, creating a new branch
function handleRegenerate(
stream: ReturnType<typeof useStream>,
metadata: MessageMetadata
) {
const checkpoint = metadata.firstSeenState?.parent_checkpoint;
if (!checkpoint) return;
stream.submit(undefined, { checkpoint });
}
Each regeneration creates a new branch for the AI message at that position.
Users can then use the branch switcher to compare different responses.
Regeneration is useful for non-deterministic agents. Since LLM outputs vary
with temperature, regenerating the same prompt often produces meaningfully
different responses.
Build a branch switcher
When a message has multiple branches, show a compact inline control with the
current version index and navigation arrows:
function BranchSwitcher({
metadata,
onSwitch,
}: {
metadata: MessageMetadata;
onSwitch: (branchId: string) => void;
}) {
const { branch, branchOptions } = metadata;
if (branchOptions.length <= 1) return null;
const currentIndex = branchOptions.indexOf(branch);
const hasPrev = currentIndex > 0;
const hasNext = currentIndex < branchOptions.length - 1;
return (
<div className="inline-flex items-center gap-1 rounded-full bg-gray-100 px-2 py-0.5 text-xs text-gray-600">
<button
disabled={!hasPrev}
onClick={() => onSwitch(branchOptions[currentIndex - 1])}
className="hover:text-gray-900 disabled:opacity-30"
aria-label="Previous version"
>
◀
</button>
<span className="min-w-[3ch] text-center">
{currentIndex + 1}/{branchOptions.length}
</span>
<button
disabled={!hasNext}
onClick={() => onSwitch(branchOptions[currentIndex + 1])}
className="hover:text-gray-900 disabled:opacity-30"
aria-label="Next version"
>
▶
</button>
</div>
);
}
When the user clicks a branch arrow, call stream.setBranch(branchId) to
switch the conversation view to that branch. This is instant since all branch
data is already loaded via fetchStateHistory: true.
Switching branches affects not only the target message but also all subsequent
messages. If you switch to a different version of message 3, messages 4, 5, 6,
etc. will also update to reflect the conversation that followed that version.
How branching works under the hood
LangGraph persists every state transition as a checkpoint. When you submit
with a checkpoint parameter, the backend forks from that point instead of
appending to the current conversation. The result is a tree structure:
User: "What is React?"
└─ AI: "React is a JavaScript library..." (branch A)
└─ AI: "React is a UI framework..." (branch B, regenerated)
User: "Tell me about hooks" (branch A)
└─ AI: "Hooks are functions..."
User: "Tell me about JSX" (edited from branch A)
└─ AI: "JSX is a syntax extension..."
Each branch is an independent path through the conversation tree. Switching
branches updates the displayed messages but does not delete any data—all
branches persist in the checkpoint store.
Complete message component
Here is a full component that combines message display, editing, regeneration,
and branch switching:
function MessageWithBranching({
message,
metadata,
stream,
}: {
message: BaseMessage;
metadata: MessageMetadata;
stream: ReturnType<typeof useStream>;
}) {
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(message.content as string);
const isHuman = message._getType() === "human";
const isAI = message._getType() === "ai";
const hasBranches = metadata.branchOptions.length > 1;
return (
<div className="group relative py-2">
{isEditing ? (
<EditForm
text={editText}
onChange={setEditText}
onSave={() => {
handleEdit(stream, message as HumanMessage, metadata, editText);
setIsEditing(false);
}}
onCancel={() => {
setEditText(message.content as string);
setIsEditing(false);
}}
/>
) : (
<>
<div className={isHuman ? "text-right" : "text-left"}>
<div
className={
isHuman
? "inline-block rounded-lg bg-blue-600 px-4 py-2 text-white"
: "inline-block rounded-lg bg-gray-100 px-4 py-2"
}
>
{message.content as string}
</div>
</div>
<div className="mt-1 flex items-center gap-2 opacity-0 transition-opacity group-hover:opacity-100">
{isHuman && (
<button
className="text-xs text-gray-400 hover:text-gray-700"
onClick={() => setIsEditing(true)}
>
Edit
</button>
)}
{isAI && (
<button
className="text-xs text-gray-400 hover:text-gray-700"
onClick={() =>
handleRegenerate(stream, metadata)
}
>
Regenerate
</button>
)}
{hasBranches && (
<BranchSwitcher
metadata={metadata}
onSwitch={(id) => stream.setBranch(id)}
/>
)}
</div>
</>
)}
</div>
);
}
function EditForm({
text,
onChange,
onSave,
onCancel,
}: {
text: string;
onChange: (text: string) => void;
onSave: () => void;
onCancel: () => void;
}) {
return (
<div className="space-y-2">
<textarea
className="w-full rounded-lg border p-3 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={text}
onChange={(e) => onChange(e.target.value)}
rows={3}
/>
<div className="flex gap-2">
<button
className="rounded bg-blue-600 px-4 py-1.5 text-sm text-white hover:bg-blue-700"
onClick={onSave}
>
Save & Rerun
</button>
<button
className="rounded border px-4 py-1.5 text-sm hover:bg-gray-50"
onClick={onCancel}
>
Cancel
</button>
</div>
</div>
);
}
Combine with optimistic updates
Combine branching with optimistic updates
for a seamless editing experience. When the user saves an edit, optimistically
show the updated message before the server responds:
function handleEditOptimistic(
stream: ReturnType<typeof useStream>,
originalMsg: HumanMessage,
metadata: MessageMetadata,
newText: string
) {
const checkpoint = metadata.firstSeenState?.parent_checkpoint;
if (!checkpoint) return;
const updatedMsg = { ...originalMsg, content: newText };
stream.submit(
{ messages: [updatedMsg] },
{
checkpoint,
optimisticValues: (prev) => {
if (!prev?.messages) return { messages: [updatedMsg] };
const idx = prev.messages.findIndex((m) => m.id === originalMsg.id);
if (idx === -1) return prev;
return {
...prev,
messages: [...prev.messages.slice(0, idx), updatedMsg],
};
},
}
);
}
Add keyboard navigation
For power users, add keyboard shortcuts to navigate branches:
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
if (!focusedMessageMetadata) return;
const { branch, branchOptions } = focusedMessageMetadata;
const idx = branchOptions.indexOf(branch);
if (e.altKey && e.key === "ArrowLeft" && idx > 0) {
stream.setBranch(branchOptions[idx - 1]);
}
if (e.altKey && e.key === "ArrowRight" && idx < branchOptions.length - 1) {
stream.setBranch(branchOptions[idx + 1]);
}
}
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [focusedMessageMetadata, stream]);
Alt + ← / Alt + → is a natural mapping for branch navigation since it
mirrors browser back/forward navigation.
Best practices
- Always enable
fetchStateHistory—without it, getMessagesMetadata
cannot return branch information.
- Only show the branch switcher when there are multiple branches—a
1/1 indicator adds clutter without value.
- Show branch controls on hover—branch navigation arrows and edit buttons
should appear on hover to keep the UI clean.
- Keep the branch switcher compact—it sits inline with message controls
and should not dominate the UI.
- Preserve scroll position—when switching branches, try to keep the
viewport anchored to the message that changed.
- Indicate the active branch—use subtle visual cues (e.g., a colored dot
or branch label) so users know which branch they’re viewing.
- Disable controls while streaming—don’t allow edits or regeneration
while the agent is actively streaming a response. Check
stream.isLoading
before enabling these actions.
- Preserve edit text on cancel—if the user starts editing, then cancels,
reset the textarea to the original message content.
- Test with deep branch trees—users who edit and regenerate frequently
can create many branches. Ensure the branch switcher and data handling
remain performant.