Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,7 @@ export interface WebviewMessage {
| "openCustomModesSettings"
| "checkpointDiff"
| "checkpointRestore"
| "restoreToTaskStart"
| "deleteMcpServer"
| "codebaseIndexEnabled"
| "telemetrySetting"
Expand Down
58 changes: 57 additions & 1 deletion src/core/checkpoints/__tests__/checkpoint.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { describe, it, expect, vi, beforeEach, afterEach, Mock } from "vitest"
import { Task } from "../../task/Task"
import { ClineProvider } from "../../webview/ClineProvider"
import { checkpointSave, checkpointRestore, checkpointDiff, getCheckpointService } from "../index"
import {
checkpointSave,
checkpointRestore,
checkpointRestoreToBase,
checkpointDiff,
getCheckpointService,
} from "../index"
import { MessageManager } from "../../message-manager"
import * as vscode from "vscode"

Expand Down Expand Up @@ -296,6 +302,56 @@ describe("Checkpoint functionality", () => {
})
})

describe("checkpointRestoreToBase", () => {
beforeEach(() => {
mockCheckpointService.baseHash = "initial-commit-hash"
})

it("should restore to base hash successfully", async () => {
const result = await checkpointRestoreToBase(mockTask)

expect(result).toBe(true)
expect(mockCheckpointService.restoreCheckpoint).toHaveBeenCalledWith("initial-commit-hash")
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "currentCheckpointUpdated",
text: "initial-commit-hash",
})
expect(mockProvider.cancelTask).toHaveBeenCalled()
})

it("should return false if no checkpoint service available", async () => {
mockTask.checkpointService = undefined
mockTask.enableCheckpoints = false

const result = await checkpointRestoreToBase(mockTask)

expect(result).toBe(false)
expect(mockCheckpointService.restoreCheckpoint).not.toHaveBeenCalled()
})

it("should return false if no baseHash available", async () => {
mockCheckpointService.baseHash = undefined

const result = await checkpointRestoreToBase(mockTask)

expect(result).toBe(false)
expect(mockCheckpointService.restoreCheckpoint).not.toHaveBeenCalled()
expect(mockProvider.log).toHaveBeenCalledWith("[checkpointRestoreToBase] no baseHash available")
})

it("should disable checkpoints on error", async () => {
mockCheckpointService.restoreCheckpoint.mockRejectedValue(new Error("Restore failed"))

const result = await checkpointRestoreToBase(mockTask)

expect(result).toBe(false)
expect(mockTask.enableCheckpoints).toBe(false)
expect(mockProvider.log).toHaveBeenCalledWith(
"[checkpointRestoreToBase] disabling checkpoints for this task",
)
})
})

describe("checkpointDiff", () => {
beforeEach(() => {
mockTask.clineMessages = [
Expand Down
40 changes: 40 additions & 0 deletions src/core/checkpoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,46 @@ export async function checkpointRestore(
}
}

/**
* Restore the workspace to its initial state (baseHash) - the state when the shadow git repo was initialized.
* This is a simpler version of checkpointRestore that doesn't need to rewind messages since we're
* restoring to the very beginning of the task.
* @returns true if restoration was successful, false otherwise
*/
export async function checkpointRestoreToBase(task: Task): Promise<boolean> {
const service = await getCheckpointService(task)

if (!service) {
return false
}

const baseHash = service.baseHash

if (!baseHash) {
const provider = task.providerRef.deref()
provider?.log("[checkpointRestoreToBase] no baseHash available")
return false
}

const provider = task.providerRef.deref()

try {
await service.restoreCheckpoint(baseHash)
TelemetryService.instance.captureCheckpointRestored(task.taskId)
await provider?.postMessageToWebview({ type: "currentCheckpointUpdated", text: baseHash })

// Cancel the task to reinitialize with the restored state
// This follows the same pattern as checkpointRestore
provider?.cancelTask()

return true
} catch (err) {
provider?.log("[checkpointRestoreToBase] disabling checkpoints for this task")
task.enableCheckpoints = false
return false
}
}

export type CheckpointDiffOptions = {
ts?: number
previousCommitHash?: string
Expand Down
36 changes: 36 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1212,6 +1212,42 @@ export const webviewMessageHandler = async (

break
}
case "restoreToTaskStart": {
const currentTask = provider.getCurrentTask()

if (!currentTask) {
vscode.window.showErrorMessage(t("common:errors.checkpoint_no_active_task"))
break
}

if (!currentTask.enableCheckpoints) {
vscode.window.showErrorMessage(t("common:errors.checkpoint_not_enabled"))
break
}

// Cancel the current task first
await provider.cancelTask()

try {
await pWaitFor(() => provider.getCurrentTask()?.isInitialized === true, { timeout: 3_000 })
} catch (error) {
vscode.window.showErrorMessage(t("common:errors.checkpoint_timeout"))
break
}

try {
const { checkpointRestoreToBase } = await import("../checkpoints")
const success = await checkpointRestoreToBase(provider.getCurrentTask()!)

if (!success) {
vscode.window.showErrorMessage(t("common:errors.checkpoint_restore_base_failed"))
}
} catch (error) {
vscode.window.showErrorMessage(t("common:errors.checkpoint_failed"))
}

break
}
case "cancelTask":
await provider.cancelTask()
break
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/locales/ca/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/i18n/locales/de/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
"could_not_open_file_generic": "Could not open file!",
"checkpoint_timeout": "Timed out when attempting to restore checkpoint.",
"checkpoint_failed": "Failed to restore checkpoint.",
"checkpoint_no_active_task": "No active task to restore.",
"checkpoint_not_enabled": "Checkpoints are not enabled for this task.",
"checkpoint_restore_base_failed": "Failed to restore workspace to initial state.",
"git_not_installed": "Git is required for the checkpoints feature. Please install Git to enable checkpoints.",
"checkpoint_no_first": "No first checkpoint to compare.",
"checkpoint_no_previous": "No previous checkpoint to compare.",
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/locales/es/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/i18n/locales/fr/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/i18n/locales/hi/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/i18n/locales/id/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/i18n/locales/it/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/i18n/locales/ja/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/i18n/locales/ko/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/i18n/locales/nl/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/i18n/locales/pl/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/i18n/locales/pt-BR/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/i18n/locales/ru/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/i18n/locales/tr/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/i18n/locales/vi/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/i18n/locales/zh-CN/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading