mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: add lightweight conversation forking functionality
- Add fork button to user messages in ChatRow - Reuse existing delete message logic for truncating history - Create new task with truncated history using existing createTaskWithHistoryItem - No tool exposed to the model - purely user-driven functionality - Minimal implementation as requested in issue feedback Addresses #9790
This commit is contained in:
parent
1879200964
commit
f827c3267e
6 changed files with 119 additions and 1 deletions
|
|
@ -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<void> {
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ export interface WebviewMessage {
|
|||
| "deleteMessage"
|
||||
| "deleteMessageConfirm"
|
||||
| "submitEditedMessage"
|
||||
| "forkConversation"
|
||||
| "editMessageConfirm"
|
||||
| "enableMcpServerCreation"
|
||||
| "remoteControlEnabled"
|
||||
|
|
|
|||
|
|
@ -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 = ({
|
|||
}}>
|
||||
<Edit className="w-4 shrink-0" aria-label="Edit message icon" />
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
style={{ visibility: isStreaming ? "hidden" : "visible" }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
vscode.postMessage({
|
||||
type: "forkConversation",
|
||||
messageTs: message.ts,
|
||||
})
|
||||
}}
|
||||
title={t("chat:fork.tooltip")}>
|
||||
<GitBranch
|
||||
className="w-4 shrink-0"
|
||||
aria-label="Fork conversation icon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
style={{ visibility: isStreaming ? "hidden" : "visible" }}
|
||||
|
|
|
|||
|
|
@ -241,6 +241,9 @@
|
|||
"feedback": {
|
||||
"youSaid": "You said"
|
||||
},
|
||||
"fork": {
|
||||
"tooltip": "Fork conversation from this point"
|
||||
},
|
||||
"mcp": {
|
||||
"wantsToUseTool": "Roo wants to use a tool on the {{serverName}} MCP server",
|
||||
"wantsToAccessResource": "Roo wants to access a resource on the {{serverName}} MCP server"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue