Skip to content

Commit e559ac6

Browse files
authored
Edit/Delete User Message (#7447)
1 parent 2b53399 commit e559ac6

35 files changed

+2033
-144
lines changed

src/core/checkpoints/__tests__/checkpoint.test.ts

Lines changed: 432 additions & 0 deletions
Large diffs are not rendered by default.

src/core/checkpoints/index.ts

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,13 @@ export type CheckpointRestoreOptions = {
184184
ts: number
185185
commitHash: string
186186
mode: "preview" | "restore"
187+
operation?: "delete" | "edit" // Optional to maintain backward compatibility
187188
}
188189

189-
export async function checkpointRestore(task: Task, { ts, commitHash, mode }: CheckpointRestoreOptions) {
190+
export async function checkpointRestore(
191+
task: Task,
192+
{ ts, commitHash, mode, operation = "delete" }: CheckpointRestoreOptions,
193+
) {
190194
const service = await getCheckpointService(task)
191195

192196
if (!service) {
@@ -215,7 +219,10 @@ export async function checkpointRestore(task: Task, { ts, commitHash, mode }: Ch
215219
task.combineMessages(deletedMessages),
216220
)
217221

218-
await task.overwriteClineMessages(task.clineMessages.slice(0, index + 1))
222+
// For delete operations, exclude the checkpoint message itself
223+
// For edit operations, include the checkpoint message (to be edited)
224+
const endIndex = operation === "edit" ? index + 1 : index
225+
await task.overwriteClineMessages(task.clineMessages.slice(0, endIndex))
219226

220227
// TODO: Verify that this is working as expected.
221228
await task.say(
@@ -264,15 +271,16 @@ export async function checkpointDiff(task: Task, { ts, previousCommitHash, commi
264271
TelemetryService.instance.captureCheckpointDiffed(task.taskId)
265272

266273
let prevHash = commitHash
267-
let nextHash: string | undefined
268-
269-
const checkpoints = typeof service.getCheckpoints === "function" ? service.getCheckpoints() : []
270-
const idx = checkpoints.indexOf(commitHash)
271-
272-
if (idx !== -1 && idx < checkpoints.length - 1) {
273-
nextHash = checkpoints[idx + 1]
274-
} else {
275-
nextHash = undefined
274+
let nextHash: string | undefined = undefined
275+
276+
if (mode !== "full") {
277+
const checkpoints = task.clineMessages.filter(({ say }) => say === "checkpoint_saved").map(({ text }) => text!)
278+
const idx = checkpoints.indexOf(commitHash)
279+
if (idx !== -1 && idx < checkpoints.length - 1) {
280+
nextHash = checkpoints[idx + 1]
281+
} else {
282+
nextHash = undefined
283+
}
276284
}
277285

278286
try {
@@ -285,7 +293,7 @@ export async function checkpointDiff(task: Task, { ts, previousCommitHash, commi
285293

286294
await vscode.commands.executeCommand(
287295
"vscode.changes",
288-
mode === "full" ? "Changes since task started" : "Changes since previous checkpoint",
296+
mode === "full" ? "Changes since task started" : "Changes compare with next checkpoint",
289297
changes.map((change) => [
290298
vscode.Uri.file(change.paths.absolute),
291299
vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${change.paths.relative}`).with({

src/core/webview/ClineProvider.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,20 @@ import { getUri } from "./getUri"
9797
* https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/customSidebarViewProvider.ts
9898
*/
9999

100+
export type ClineProviderEvents = {
101+
clineCreated: [cline: Task]
102+
}
103+
104+
interface PendingEditOperation {
105+
messageTs: number
106+
editedContent: string
107+
images?: string[]
108+
messageIndex: number
109+
apiConversationHistoryIndex: number
110+
timeoutId: NodeJS.Timeout
111+
createdAt: number
112+
}
113+
100114
export class ClineProvider
101115
extends EventEmitter<TaskProviderEvents>
102116
implements vscode.WebviewViewProvider, TelemetryPropertiesProvider, TaskProviderLike
@@ -121,6 +135,8 @@ export class ClineProvider
121135
private taskEventListeners: WeakMap<Task, Array<() => void>> = new WeakMap()
122136

123137
private recentTasksCache?: string[]
138+
private pendingOperations: Map<string, PendingEditOperation> = new Map()
139+
private static readonly PENDING_OPERATION_TIMEOUT_MS = 30000 // 30 seconds
124140

125141
public isViewLaunched = false
126142
public settingsImportedAt?: number
@@ -440,6 +456,71 @@ export class ClineProvider
440456
// the 'parent' calling task).
441457
await this.getCurrentTask()?.completeSubtask(lastMessage)
442458
}
459+
// Pending Edit Operations Management
460+
461+
/**
462+
* Sets a pending edit operation with automatic timeout cleanup
463+
*/
464+
public setPendingEditOperation(
465+
operationId: string,
466+
editData: {
467+
messageTs: number
468+
editedContent: string
469+
images?: string[]
470+
messageIndex: number
471+
apiConversationHistoryIndex: number
472+
},
473+
): void {
474+
// Clear any existing operation with the same ID
475+
this.clearPendingEditOperation(operationId)
476+
477+
// Create timeout for automatic cleanup
478+
const timeoutId = setTimeout(() => {
479+
this.clearPendingEditOperation(operationId)
480+
this.log(`[setPendingEditOperation] Automatically cleared stale pending operation: ${operationId}`)
481+
}, ClineProvider.PENDING_OPERATION_TIMEOUT_MS)
482+
483+
// Store the operation
484+
this.pendingOperations.set(operationId, {
485+
...editData,
486+
timeoutId,
487+
createdAt: Date.now(),
488+
})
489+
490+
this.log(`[setPendingEditOperation] Set pending operation: ${operationId}`)
491+
}
492+
493+
/**
494+
* Gets a pending edit operation by ID
495+
*/
496+
private getPendingEditOperation(operationId: string): PendingEditOperation | undefined {
497+
return this.pendingOperations.get(operationId)
498+
}
499+
500+
/**
501+
* Clears a specific pending edit operation
502+
*/
503+
private clearPendingEditOperation(operationId: string): boolean {
504+
const operation = this.pendingOperations.get(operationId)
505+
if (operation) {
506+
clearTimeout(operation.timeoutId)
507+
this.pendingOperations.delete(operationId)
508+
this.log(`[clearPendingEditOperation] Cleared pending operation: ${operationId}`)
509+
return true
510+
}
511+
return false
512+
}
513+
514+
/**
515+
* Clears all pending edit operations
516+
*/
517+
private clearAllPendingEditOperations(): void {
518+
for (const [operationId, operation] of this.pendingOperations) {
519+
clearTimeout(operation.timeoutId)
520+
}
521+
this.pendingOperations.clear()
522+
this.log(`[clearAllPendingEditOperations] Cleared all pending operations`)
523+
}
443524

444525
/*
445526
VSCode extensions use the disposable pattern to clean up resources when the sidebar/editor tab is closed by the user or system. This applies to event listening, commands, interacting with the UI, etc.
@@ -465,6 +546,10 @@ export class ClineProvider
465546

466547
this.log("Cleared all tasks")
467548

549+
// Clear all pending edit operations to prevent memory leaks
550+
this.clearAllPendingEditOperations()
551+
this.log("Cleared pending operations")
552+
468553
if (this.view && "dispose" in this.view) {
469554
this.view.dispose()
470555
this.log("Disposed webview")
@@ -805,6 +890,49 @@ export class ClineProvider
805890
`[createTaskWithHistoryItem] ${task.parentTask ? "child" : "parent"} task ${task.taskId}.${task.instanceId} instantiated`,
806891
)
807892

893+
// Check if there's a pending edit after checkpoint restoration
894+
const operationId = `task-${task.taskId}`
895+
const pendingEdit = this.getPendingEditOperation(operationId)
896+
if (pendingEdit) {
897+
this.clearPendingEditOperation(operationId) // Clear the pending edit
898+
899+
this.log(`[createTaskWithHistoryItem] Processing pending edit after checkpoint restoration`)
900+
901+
// Process the pending edit after a short delay to ensure the task is fully initialized
902+
setTimeout(async () => {
903+
try {
904+
// Find the message index in the restored state
905+
const { messageIndex, apiConversationHistoryIndex } = (() => {
906+
const messageIndex = task.clineMessages.findIndex((msg) => msg.ts === pendingEdit.messageTs)
907+
const apiConversationHistoryIndex = task.apiConversationHistory.findIndex(
908+
(msg) => msg.ts === pendingEdit.messageTs,
909+
)
910+
return { messageIndex, apiConversationHistoryIndex }
911+
})()
912+
913+
if (messageIndex !== -1) {
914+
// Remove the target message and all subsequent messages
915+
await task.overwriteClineMessages(task.clineMessages.slice(0, messageIndex))
916+
917+
if (apiConversationHistoryIndex !== -1) {
918+
await task.overwriteApiConversationHistory(
919+
task.apiConversationHistory.slice(0, apiConversationHistoryIndex),
920+
)
921+
}
922+
923+
// Process the edited message
924+
await task.handleWebviewAskResponse(
925+
"messageResponse",
926+
pendingEdit.editedContent,
927+
pendingEdit.images,
928+
)
929+
}
930+
} catch (error) {
931+
this.log(`[createTaskWithHistoryItem] Error processing pending edit: ${error}`)
932+
}
933+
}, 100) // Small delay to ensure task is fully ready
934+
}
935+
808936
return task
809937
}
810938

0 commit comments

Comments
 (0)