How to Use Fork and Rewind to Manage Conversation History in Claudian
Claudian lets you rewind to any previous assistant response or fork a conversation into a new tab, enabling non-destructive exploration of alternate dialogue paths without losing your original chat flow.
The YishenTu/claudian repository implements these features through a coordinated system of UI rendering, context detection, and SDK operations. By leveraging the findRewindContext utility and the ConversationController layer, you can programmatically navigate conversation history or create parallel discussion branches. This guide explains the complete architecture and provides runnable code examples for implementing both patterns.
Architecture of Fork and Rewind
Claudian’s conversation history management relies on four distinct layers working in concert. The UI rendering layer in src/features/chat/rendering/MessageRenderer.ts injects rewind and fork icons into eligible user messages via MessageRenderer.addRewindButton and MessageRenderer.addForkButton. These buttons call back into the conversation controller, which orchestrates the operation.
The rewind context detection layer in src/features/chat/rewind.ts provides the findRewindContext function that validates whether a message can be rewound. The service layer in src/core/agent/ClaudianService.ts exposes the actual SDK operations rewind and applyForkState, while the tab management layer in src/features/chat/tabs/Tab.ts handles fork routing through handleForkRequest and handleForkAll.
Detecting Rewind Eligibility
Before displaying controls, Claudian verifies that a user message supports rewinding. The MessageRenderer.isRewindEligible method calls findRewindContext from src/features/chat/rewind.ts to locate the previous assistant UUID and confirm a subsequent response exists.
const ctx = findRewindContext(allMessages, index);
return !!ctx.prevAssistantUuid && ctx.hasResponse;
The findRewindContext function walks backward from the target message to find the most recent assistant message containing a valid sdkAssistantUuid. It then scans forward to ensure the user message has an assistant response after it, setting the hasResponse flag. Only when both conditions are met does the UI render the rewind and fork buttons.
Rendering Rewind and Fork Buttons
The MessageRenderer class constructs the interface elements programmatically. The addRewindButton method creates a span with the class claudian-message-rewind-btn, attaches the SVG icon defined in MessageRenderer.REWIND_ICON, and wires a click listener to the rewindCallback.
private addRewindButton(msgEl: HTMLElement, messageId: string) {
const toolbar = this.getOrCreateActionsToolbar(msgEl);
const btn = toolbar.createSpan({ cls: 'claudian-message-rewind-btn' });
btn.innerHTML = MessageRenderer.REWIND_ICON;
btn.setAttribute('aria-label', t('chat.rewind.ariaLabel'));
btn.addEventListener('click', async e => {
e.stopPropagation();
await this.rewindCallback?.(messageId);
});
}
The addForkButton method follows an identical pattern, utilizing MessageRenderer.FORK_ICON and the forkRequestCallback. These callbacks are injected during tab initialization in Tab.initializeTabControllers, binding the UI to ConversationController.rewind and handleForkRequest respectively.
Executing a Rewind Operation
When a user clicks rewind, ConversationController.rewind in src/core/controllers/ConversationController.ts validates the context, prompts for confirmation, and invokes the service layer.
const result = await this.service.rewind(userMsg.sdkUserUuid, prevAssistantUuid);
The ClaudianService.rewind method in src/core/agent/ClaudianService.ts forwards the sdkUserUuid and prevAssistantUuid to the Anthropic SDK. Upon receiving a RewindFilesResult, the controller writes the restored files to the vault and displays a success notice via t('chat.rewind.notice'). Errors surface through Obsidian notices using t('chat.rewind.failed').
Forking Conversations to New Tabs
Forking diverges conversation history by creating a new session branch. The process begins in src/features/chat/tabs/Tab.ts where handleForkRequest validates the streaming state, resolves the source session via resolveForkSource, and constructs a ForkContext object.
The ForkContext includes the trimmed message history up to the user message, the sourceSessionId, the resumeAt assistant UUID, and metadata like the current note title. If the user initiated the fork through the UI, ForkTargetModal in src/shared/modals/ForkTargetModal.ts prompts whether to open the fork in a new tab or the current tab.
await forkRequestCallback({
messages: deepCloneMessages(msgs.slice(0, userIdx)),
sourceSessionId: source.sourceSessionId,
resumeAt: rewindCtx.prevAssistantUuid,
sourceTitle: source.sourceTitle,
forkAtUserMessage: countUserMessagesForForkTitle(...),
currentNote: source.currentNote,
});
The receiving callback creates a fresh Tab instance and calls ClaudianService.applyForkState to seed the new SDK session with the fork metadata, effectively cloning the conversation state up to the selected point.
Code Examples
Programmatically Rewind a Specific Message
Use this pattern to trigger a rewind from a custom command or automation:
import { findRewindContext } from 'src/features/chat/rewind';
import { ConversationController } from 'src/core/controllers/ConversationController';
async function rewindMessage(controller: ConversationController, userMessageId: string) {
const msg = controller.state.messages.find(m => m.id === userMessageId);
if (!msg?.sdkUserUuid) {
new Notice('Message not rewound – missing SDK UUID');
return;
}
const ctx = findRewindContext(
controller.state.messages,
controller.state.messages.indexOf(msg)
);
if (!ctx.prevAssistantUuid || !ctx.hasResponse) {
new Notice('No rewind target found');
return;
}
if (!await confirm('Rewind to this point?')) return;
const result = await controller.service.rewind(
msg.sdkUserUuid,
ctx.prevAssistantUuid
);
console.log('Rewind completed', result);
}
Fork the Current Conversation into a New Tab
Implement a custom fork workflow that prompts for the target location:
import { Tab, createTab } from 'src/features/chat/tabs/Tab';
import { ForkTargetModal } from 'src/shared/modals/ForkTargetModal';
async function forkCurrentConversation(tab: Tab) {
const target = await new ForkTargetModal(app).open();
if (!target) return;
const forkCtx = await tab.controllers.conversationController!.makeForkContextForAll();
if (target === 'new-tab') {
const newTab = createTab({
plugin,
mcpManager,
containerEl: app.workspace.getLeaf().containerEl,
});
await newTab.service?.applyForkState(forkCtx);
} else {
await tab.service?.applyForkState(forkCtx);
}
}
UI Structure for Action Buttons
The buttons render inside each eligible user message container:
<div class="claudian-user-msg-actions">
<span class="claudian-message-rewind-btn" aria-label="Rewind"></span>
<span class="claudian-message-fork-btn" aria-label="Fork"></span>
</div>
These elements receive their icons from MessageRenderer.REWIND_ICON and MessageRenderer.FORK_ICON constants.
Summary
- Rewind eligibility is determined by
findRewindContextinsrc/features/chat/rewind.ts, which verifies the presence of a previous assistant UUID and a subsequent response. - UI controls are injected by
MessageRendererinsrc/features/chat/rendering/MessageRenderer.tsusingaddRewindButtonandaddForkButton. - Rewind execution flows through
ConversationController.rewindtoClaudianService.rewind, restoring files via the Anthropic SDK. - Forking creates a
ForkContextviahandleForkRequestinsrc/features/chat/tabs/Tab.ts, allowing exploration in new or current tabs throughapplyForkState.
Frequently Asked Questions
How does Claudian determine which messages can be rewound?
Claudian calls findRewindContext from src/features/chat/rewind.ts to scan backward for the nearest assistant message with a valid sdkAssistantUuid and forward to confirm an assistant response exists after the user message. Only messages meeting both criteria display the rewind button.
What happens to file changes when I rewind a conversation?
When ConversationController.rewind executes successfully, it receives a RewindFilesResult from the Anthropic SDK and writes the restored file state to your vault. The controller surfaces success or failure through Obsidian notices defined in the i18n keys chat.rewind.notice and chat.rewind.failed.
Can I fork a conversation without creating a new tab?
Yes. The ForkTargetModal in src/shared/modals/ForkTargetModal.ts prompts you to choose between a new tab or the current tab. Selecting the current tab calls applyForkState on the existing Tab instance, replacing its conversation history with the forked context while preserving the UI container.
What is included in a ForkContext object?
The ForkContext contains the truncated message array up to the fork point, the sourceSessionId (live or persisted), the resumeAt assistant UUID where the new session continues, the original conversation title, and metadata about the current note. This structure is passed to ClaudianService.applyForkState to initialize the branched session.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →