@@ -3,7 +3,13 @@ import fs from "fs/promises"
33import pWaitFor from "p-wait-for"
44import * as vscode from "vscode"
55
6- import { type Language , type ProviderSettings , type GlobalState , TelemetryEventName } from "@roo-code/types"
6+ import {
7+ type Language ,
8+ type ProviderSettings ,
9+ type GlobalState ,
10+ TelemetryEventName ,
11+ type ClineMessage ,
12+ } from "@roo-code/types"
713import { CloudService } from "@roo-code/cloud"
814import { TelemetryService } from "@roo-code/telemetry"
915
@@ -28,6 +34,7 @@ import { playTts, setTtsEnabled, setTtsSpeed, stopTts } from "../../utils/tts"
2834import { singleCompletionHandler } from "../../utils/single-completion-handler"
2935import { searchCommits } from "../../utils/git"
3036import { exportSettings , importSettings } from "../config/importExport"
37+ import { checkpointRestore } from "../checkpoints"
3138import { getOpenAiModels } from "../../api/providers/openai"
3239import { getOllamaModels } from "../../api/providers/ollama"
3340import { getVsCodeLmModels } from "../../api/providers/vscode-lm"
@@ -959,6 +966,186 @@ export const webviewMessageHandler = async (
959966 }
960967 break
961968 }
969+ case "editMessage" : {
970+ if (
971+ provider . getCurrentCline ( ) &&
972+ typeof message . value === "number" &&
973+ message . value &&
974+ message . text !== undefined
975+ ) {
976+ const timeCutoff = message . value - 1000 // 1 second buffer before the message to edit
977+
978+ const messageIndex = provider
979+ . getCurrentCline ( ) !
980+ . clineMessages . findIndex ( ( msg ) => msg . ts && msg . ts >= timeCutoff )
981+
982+ const apiConversationHistoryIndex =
983+ provider
984+ . getCurrentCline ( )
985+ ?. apiConversationHistory . findIndex ( ( msg ) => msg . ts && msg . ts >= timeCutoff ) ?? - 1
986+
987+ if ( messageIndex !== - 1 ) {
988+ // Check if there are subsequent messages that will be deleted
989+ const totalMessages = provider . getCurrentCline ( ) ! . clineMessages . length
990+ const hasSubsequentMessages = messageIndex < totalMessages - 1
991+
992+ // Check for checkpoints if enabled
993+ const checkpointsEnabled = ( await provider . getState ( ) ) . enableCheckpoints
994+ let affectedCheckpointsCount = 0
995+ let closestPreviousCheckpoint : ClineMessage | undefined
996+
997+ if ( checkpointsEnabled ) {
998+ const editMessageTimestamp = message . value
999+ const checkpointMessages = provider
1000+ . getCurrentCline ( ) !
1001+ . clineMessages . filter ( ( msg ) => msg . say === "checkpoint_saved" )
1002+ . sort ( ( a , b ) => a . ts - b . ts )
1003+
1004+ // Find checkpoints that will be affected (those after the edited message)
1005+ affectedCheckpointsCount = checkpointMessages . filter (
1006+ ( cp ) => cp . ts > editMessageTimestamp ,
1007+ ) . length
1008+
1009+ // Find the closest checkpoint before the edited message
1010+ closestPreviousCheckpoint = checkpointMessages
1011+ . reverse ( )
1012+ . find ( ( cp ) => cp . ts < editMessageTimestamp )
1013+ }
1014+
1015+ // Build confirmation message
1016+ let confirmationMessage = "Edit and delete subsequent messages?"
1017+
1018+ if ( checkpointsEnabled && affectedCheckpointsCount > 0 ) {
1019+ confirmationMessage += `\n\n• ${ affectedCheckpointsCount } checkpoint(s) will be removed`
1020+
1021+ if ( closestPreviousCheckpoint ) {
1022+ confirmationMessage += "\n• Files will restore to previous checkpoint"
1023+ }
1024+ }
1025+
1026+ // Show confirmation dialog if there are subsequent messages or affected checkpoints
1027+ if ( hasSubsequentMessages || affectedCheckpointsCount > 0 ) {
1028+ const confirmation = await vscode . window . showWarningMessage (
1029+ confirmationMessage ,
1030+ { modal : true } ,
1031+ "Edit Message" ,
1032+ )
1033+
1034+ if ( confirmation !== "Edit Message" ) {
1035+ // User cancelled, update the webview to show the original state
1036+ await provider . postStateToWebview ( )
1037+ break
1038+ }
1039+ }
1040+
1041+ const { historyItem } = await provider . getTaskWithId ( provider . getCurrentCline ( ) ! . taskId )
1042+
1043+ // Get messages up to and including the edited message
1044+ const updatedClineMessages = [
1045+ ...provider . getCurrentCline ( ) ! . clineMessages . slice ( 0 , messageIndex + 1 ) ,
1046+ ]
1047+ const messageToEdit = updatedClineMessages [ messageIndex ]
1048+
1049+ if ( messageToEdit && messageToEdit . type === "say" && messageToEdit . say === "user_feedback" ) {
1050+ // Update the text content
1051+ messageToEdit . text = message . text
1052+
1053+ // Update images if provided
1054+ if ( message . images ) {
1055+ messageToEdit . images = message . images
1056+ }
1057+
1058+ // Overwrite with only messages up to and including the edited one
1059+ await provider . getCurrentCline ( ) ! . overwriteClineMessages ( updatedClineMessages )
1060+
1061+ // Handle checkpoint restoration if checkpoints are enabled
1062+ if ( checkpointsEnabled && closestPreviousCheckpoint ) {
1063+ // Restore to the closest checkpoint before the edited message
1064+ const commitHash = closestPreviousCheckpoint . text // The commit hash is stored in the text field
1065+ if ( commitHash ) {
1066+ // Use "preview" mode to only restore files without affecting messages
1067+ // (we've already handled message cleanup above)
1068+ await checkpointRestore ( provider . getCurrentCline ( ) ! , {
1069+ ts : closestPreviousCheckpoint . ts ,
1070+ commitHash : commitHash ,
1071+ mode : "preview" ,
1072+ } )
1073+ }
1074+ }
1075+
1076+ // Update API conversation history if needed
1077+ if ( apiConversationHistoryIndex !== - 1 ) {
1078+ const updatedApiHistory = [
1079+ ...provider
1080+ . getCurrentCline ( ) !
1081+ . apiConversationHistory . slice ( 0 , apiConversationHistoryIndex + 1 ) ,
1082+ ]
1083+ const apiMessage = updatedApiHistory [ apiConversationHistoryIndex ]
1084+
1085+ if ( apiMessage && apiMessage . role === "user" ) {
1086+ // Update the content in API history
1087+ if ( typeof apiMessage . content === "string" ) {
1088+ apiMessage . content = message . text
1089+ } else if ( Array . isArray ( apiMessage . content ) ) {
1090+ // Find and update text content blocks
1091+ apiMessage . content = apiMessage . content . map ( ( block : any ) => {
1092+ if ( block . type === "text" ) {
1093+ return { ...block , text : message . text }
1094+ }
1095+ return block
1096+ } )
1097+
1098+ // Handle image updates if provided
1099+ if ( message . images ) {
1100+ // Remove existing image blocks
1101+ apiMessage . content = apiMessage . content . filter (
1102+ ( block : any ) => block . type !== "image" ,
1103+ )
1104+
1105+ // Add new image blocks
1106+ const imageBlocks = message . images . map ( ( image ) => ( {
1107+ type : "image" as const ,
1108+ source : {
1109+ type : "base64" as const ,
1110+ media_type : ( image . startsWith ( "data:image/png" )
1111+ ? "image/png"
1112+ : "image/jpeg" ) as
1113+ | "image/png"
1114+ | "image/jpeg"
1115+ | "image/gif"
1116+ | "image/webp" ,
1117+ data : image . split ( "," ) [ 1 ] || image ,
1118+ } ,
1119+ } ) )
1120+
1121+ // Add image blocks after text
1122+ apiMessage . content . push ( ...imageBlocks )
1123+ }
1124+ }
1125+
1126+ // Overwrite with only API messages up to and including the edited one
1127+ await provider . getCurrentCline ( ) ! . overwriteApiConversationHistory ( updatedApiHistory )
1128+ }
1129+ }
1130+
1131+ await provider . initClineWithHistoryItem ( historyItem )
1132+ // Force a state update to ensure the webview reflects the changes
1133+ await provider . postStateToWebview ( )
1134+
1135+ // Auto-resume the task after editing
1136+ // Use setTimeout to ensure the task is fully initialized and the ask dialog is ready
1137+ setTimeout ( async ( ) => {
1138+ const currentCline = provider . getCurrentCline ( )
1139+ if ( currentCline && currentCline . isInitialized ) {
1140+ // Simulate clicking "Resume Task" by sending the response directly
1141+ currentCline . handleWebviewAskResponse ( "messageResponse" , message . text , message . images )
1142+ }
1143+ } , 100 ) // Small delay to ensure proper initialization
1144+ }
1145+ }
1146+ }
1147+ break
1148+ }
9621149 case "screenshotQuality" :
9631150 await updateGlobalState ( "screenshotQuality" , message . value )
9641151 await provider . postStateToWebview ( )
0 commit comments