Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
432 changes: 432 additions & 0 deletions src/core/checkpoints/__tests__/checkpoint.test.ts

Large diffs are not rendered by default.

32 changes: 20 additions & 12 deletions src/core/checkpoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,13 @@ export type CheckpointRestoreOptions = {
ts: number
commitHash: string
mode: "preview" | "restore"
operation?: "delete" | "edit" // Optional to maintain backward compatibility
}

export async function checkpointRestore(task: Task, { ts, commitHash, mode }: CheckpointRestoreOptions) {
export async function checkpointRestore(
task: Task,
{ ts, commitHash, mode, operation = "delete" }: CheckpointRestoreOptions,
) {
const service = await getCheckpointService(task)

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

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

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

let prevHash = commitHash
let nextHash: string | undefined

const checkpoints = typeof service.getCheckpoints === "function" ? service.getCheckpoints() : []
const idx = checkpoints.indexOf(commitHash)

if (idx !== -1 && idx < checkpoints.length - 1) {
nextHash = checkpoints[idx + 1]
} else {
nextHash = undefined
let nextHash: string | undefined = undefined

if (mode !== "full") {
const checkpoints = task.clineMessages.filter(({ say }) => say === "checkpoint_saved").map(({ text }) => text!)
const idx = checkpoints.indexOf(commitHash)
if (idx !== -1 && idx < checkpoints.length - 1) {
nextHash = checkpoints[idx + 1]
} else {
nextHash = undefined
}
}

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

await vscode.commands.executeCommand(
"vscode.changes",
mode === "full" ? "Changes since task started" : "Changes since previous checkpoint",
mode === "full" ? "Changes since task started" : "Changes compare with next checkpoint",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
mode === "full" ? "Changes since task started" : "Changes compare with next checkpoint",
mode === "full" ? "Changes since task started" : "Changes compared to next checkpoint",

changes.map((change) => [
vscode.Uri.file(change.paths.absolute),
vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${change.paths.relative}`).with({
Expand Down
128 changes: 128 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,20 @@ import { getUri } from "./getUri"
* https://github.com/KumarVariable/vscode-extension-sidebar-html/blob/master/src/customSidebarViewProvider.ts
*/

export type ClineProviderEvents = {
clineCreated: [cline: Task]
}

interface PendingEditOperation {
messageTs: number
editedContent: string
images?: string[]
messageIndex: number
apiConversationHistoryIndex: number
timeoutId: NodeJS.Timeout
createdAt: number
}

export class ClineProvider
extends EventEmitter<TaskProviderEvents>
implements vscode.WebviewViewProvider, TelemetryPropertiesProvider, TaskProviderLike
Expand All @@ -121,6 +135,8 @@ export class ClineProvider
private taskEventListeners: WeakMap<Task, Array<() => void>> = new WeakMap()

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

public isViewLaunched = false
public settingsImportedAt?: number
Expand Down Expand Up @@ -440,6 +456,71 @@ export class ClineProvider
// the 'parent' calling task).
await this.getCurrentTask()?.completeSubtask(lastMessage)
}
// Pending Edit Operations Management

/**
* Sets a pending edit operation with automatic timeout cleanup
*/
public setPendingEditOperation(
operationId: string,
editData: {
messageTs: number
editedContent: string
images?: string[]
messageIndex: number
apiConversationHistoryIndex: number
},
): void {
// Clear any existing operation with the same ID
this.clearPendingEditOperation(operationId)

// Create timeout for automatic cleanup
const timeoutId = setTimeout(() => {
this.clearPendingEditOperation(operationId)
this.log(`[setPendingEditOperation] Automatically cleared stale pending operation: ${operationId}`)
}, ClineProvider.PENDING_OPERATION_TIMEOUT_MS)

// Store the operation
this.pendingOperations.set(operationId, {
...editData,
timeoutId,
createdAt: Date.now(),
})

this.log(`[setPendingEditOperation] Set pending operation: ${operationId}`)
}

/**
* Gets a pending edit operation by ID
*/
private getPendingEditOperation(operationId: string): PendingEditOperation | undefined {
return this.pendingOperations.get(operationId)
}

/**
* Clears a specific pending edit operation
*/
private clearPendingEditOperation(operationId: string): boolean {
const operation = this.pendingOperations.get(operationId)
if (operation) {
clearTimeout(operation.timeoutId)
this.pendingOperations.delete(operationId)
this.log(`[clearPendingEditOperation] Cleared pending operation: ${operationId}`)
return true
}
return false
}

/**
* Clears all pending edit operations
*/
private clearAllPendingEditOperations(): void {
for (const [operationId, operation] of this.pendingOperations) {
clearTimeout(operation.timeoutId)
}
this.pendingOperations.clear()
this.log(`[clearAllPendingEditOperations] Cleared all pending operations`)
}

/*
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.
Expand All @@ -465,6 +546,10 @@ export class ClineProvider

this.log("Cleared all tasks")

// Clear all pending edit operations to prevent memory leaks
this.clearAllPendingEditOperations()
this.log("Cleared pending operations")

if (this.view && "dispose" in this.view) {
this.view.dispose()
this.log("Disposed webview")
Expand Down Expand Up @@ -805,6 +890,49 @@ export class ClineProvider
`[createTaskWithHistoryItem] ${task.parentTask ? "child" : "parent"} task ${task.taskId}.${task.instanceId} instantiated`,
)

// Check if there's a pending edit after checkpoint restoration
const operationId = `task-${task.taskId}`
const pendingEdit = this.getPendingEditOperation(operationId)
if (pendingEdit) {
this.clearPendingEditOperation(operationId) // Clear the pending edit

this.log(`[createTaskWithHistoryItem] Processing pending edit after checkpoint restoration`)

// Process the pending edit after a short delay to ensure the task is fully initialized
setTimeout(async () => {
try {
// Find the message index in the restored state
const { messageIndex, apiConversationHistoryIndex } = (() => {
const messageIndex = task.clineMessages.findIndex((msg) => msg.ts === pendingEdit.messageTs)
const apiConversationHistoryIndex = task.apiConversationHistory.findIndex(
(msg) => msg.ts === pendingEdit.messageTs,
)
return { messageIndex, apiConversationHistoryIndex }
})()

if (messageIndex !== -1) {
// Remove the target message and all subsequent messages
await task.overwriteClineMessages(task.clineMessages.slice(0, messageIndex))

if (apiConversationHistoryIndex !== -1) {
await task.overwriteApiConversationHistory(
task.apiConversationHistory.slice(0, apiConversationHistoryIndex),
)
}

// Process the edited message
await task.handleWebviewAskResponse(
"messageResponse",
pendingEdit.editedContent,
pendingEdit.images,
)
}
} catch (error) {
this.log(`[createTaskWithHistoryItem] Error processing pending edit: ${error}`)
}
}, 100) // Small delay to ensure task is fully ready
}

return task
}

Expand Down
Loading
Loading