diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 77e2dbc2eb..26eff18f2b 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2652,6 +2652,70 @@ export class ClineProvider // task is a subtask of the previous one, and when it finishes it is removed // from the stack and the caller is resumed in this way we can have a chain // of tasks, each one being a sub task of the previous one until the main + /** + * Fork the current conversation at a specific message timestamp. + * Creates a new task with the conversation history up to that point. + */ + public async forkConversationAtMessage(messageTs: number): Promise { + const currentTask = this.getCurrentTask() + if (!currentTask) { + throw new Error("No active task to fork") + } + + // Find the message index to fork from + const messageIndex = currentTask.clineMessages.findIndex((msg: any) => msg.ts === messageTs) + if (messageIndex === -1) { + throw new Error(`Message with timestamp ${messageTs} not found`) + } + + // Find the corresponding API conversation history index + const apiIndex = currentTask.apiConversationHistory.findIndex((msg: any) => msg.ts === messageTs) + + // If exact match not found, find the first API message at or after the timestamp + let apiIndexToUse = apiIndex + if (apiIndexToUse === -1) { + apiIndexToUse = currentTask.apiConversationHistory.findIndex( + (msg: any) => typeof msg?.ts === "number" && msg.ts >= messageTs, + ) + } + + // Get the current mode from provider state + const { mode } = await this.getState() + + // Create a new history item with messages up to the fork point + const forkedHistoryItem: HistoryItem = { + id: currentTask.taskId, // Will be replaced with a new ID in createTaskWithHistoryItem + ts: Date.now(), + number: 0, // Will be set by the history system + mode: mode, + task: `Forked from: ${currentTask.taskId}`, + tokensIn: 0, // Reset token counts for the fork + tokensOut: 0, + cacheWrites: 0, + cacheReads: 0, + totalCost: 0, + // Note: messages are stored separately in the task persistence system + } + + // Store the forked messages in the task's message history + const forkedMessages = currentTask.clineMessages.slice(0, messageIndex + 1) + const forkedApiHistory = + apiIndexToUse !== -1 ? currentTask.apiConversationHistory.slice(0, apiIndexToUse + 1) : [] + + // Create the new task with the forked history + // We'll need to pass the messages through a different mechanism + const newTask = await this.createTaskWithHistoryItem(forkedHistoryItem) + + // Now update the new task with the forked messages + if (this.getCurrentTask()) { + await this.getCurrentTask()!.overwriteClineMessages(forkedMessages) + await this.getCurrentTask()!.overwriteApiConversationHistory(forkedApiHistory) + } + + // The new task is now active, and the UI will be updated + this.log(`Forked conversation from message at ${messageTs}`) + } + // task is finished. public async createTask( text?: string, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index c1c8e6aa20..8e864c9bc9 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1915,6 +1915,35 @@ export const webviewMessageHandler = async ( await handleDeleteMessageConfirm(message.messageTs, message.restoreCheckpoint) break + case "forkConversation": { + const forkMessageTs = message.messageTs + if (!forkMessageTs || typeof forkMessageTs !== "number") { + await vscode.window.showErrorMessage(t("common:errors.message.invalid_timestamp_for_fork")) + break + } + + const currentTask = provider.getCurrentTask() + if (!currentTask) { + await vscode.window.showErrorMessage(t("common:errors.message.no_active_task_to_fork")) + break + } + + try { + // Fork the conversation at the specified message + await provider.forkConversationAtMessage(forkMessageTs) + + // Show success message + await vscode.window.showInformationMessage(t("common:info.conversation_forked")) + } catch (error) { + console.error("Error forking conversation:", error) + await vscode.window.showErrorMessage( + t("common:errors.message.error_forking_conversation", { + error: error instanceof Error ? error.message : String(error), + }), + ) + } + break + } case "editMessageConfirm": if (message.messageTs && message.text) { await handleEditMessageConfirm( diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 784540e06f..371d64c095 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -101,7 +101,10 @@ "cannot_delete_invalid_timestamp": "Cannot delete message: invalid timestamp", "message_not_found": "Message with timestamp {{messageTs}} not found", "error_deleting_message": "Error deleting message: {{error}}", - "error_editing_message": "Error editing message: {{error}}" + "error_editing_message": "Error editing message: {{error}}", + "invalid_timestamp_for_fork": "Invalid timestamp for forking conversation", + "no_active_task_to_fork": "No active task to fork", + "error_forking_conversation": "Error forking conversation: {{error}}" }, "gemini": { "generate_stream": "Gemini generate context stream error: {{error}}", @@ -145,6 +148,7 @@ "share_link_copied": "Share link copied to clipboard", "organization_share_link_copied": "Organization share link copied to clipboard!", "public_share_link_copied": "Public share link copied to clipboard!", + "conversation_forked": "Conversation forked successfully", "image_copied_to_clipboard": "Image data URI copied to clipboard", "image_saved": "Image saved to {{path}}", "mode_exported": "Mode '{{mode}}' exported successfully", diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index b22e7ab3f6..f3e254dcb4 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -92,6 +92,7 @@ export interface WebviewMessage { | "deleteMessage" | "deleteMessageConfirm" | "submitEditedMessage" + | "forkConversation" | "editMessageConfirm" | "enableMcpServerCreation" | "remoteControlEnabled" diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 22886be2da..fde0096001 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -59,6 +59,7 @@ import { FolderTree, TerminalSquare, MessageCircle, + GitBranch, } from "lucide-react" import { cn } from "@/lib/utils" import { PathTooltip } from "../ui/PathTooltip" @@ -1196,6 +1197,22 @@ export const ChatRowContent = ({ }}> +
{ + e.stopPropagation() + vscode.postMessage({ + type: "forkConversation", + messageTs: message.ts, + }) + }} + title={t("chat:fork.tooltip")}> + +