| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295 |
- import pWaitFor from "p-wait-for"
- import * as vscode from "vscode"
- import { Cline } from "../Cline"
- import { getWorkspacePath } from "../../utils/path"
- import { ClineApiReqInfo } from "../../shared/ExtensionMessage"
- import { getApiMetrics } from "../../shared/getApiMetrics"
- import { DIFF_VIEW_URI_SCHEME } from "../../integrations/editor/DiffViewProvider"
- import { telemetryService } from "../../services/telemetry/TelemetryService"
- import { CheckpointServiceOptions, RepoPerTaskCheckpointService } from "../../services/checkpoints"
- export function getCheckpointService(cline: Cline) {
- if (!cline.enableCheckpoints) {
- return undefined
- }
- if (cline.checkpointService) {
- return cline.checkpointService
- }
- if (cline.checkpointServiceInitializing) {
- console.log("[Cline#getCheckpointService] checkpoint service is still initializing")
- return undefined
- }
- const provider = cline.providerRef.deref()
- const log = (message: string) => {
- console.log(message)
- try {
- provider?.log(message)
- } catch (err) {
- // NO-OP
- }
- }
- console.log("[Cline#getCheckpointService] initializing checkpoints service")
- try {
- const workspaceDir = getWorkspacePath()
- if (!workspaceDir) {
- log("[Cline#getCheckpointService] workspace folder not found, disabling checkpoints")
- cline.enableCheckpoints = false
- return undefined
- }
- const globalStorageDir = provider?.context.globalStorageUri.fsPath
- if (!globalStorageDir) {
- log("[Cline#getCheckpointService] globalStorageDir not found, disabling checkpoints")
- cline.enableCheckpoints = false
- return undefined
- }
- const options: CheckpointServiceOptions = {
- taskId: cline.taskId,
- workspaceDir,
- shadowDir: globalStorageDir,
- log,
- }
- const service = RepoPerTaskCheckpointService.create(options)
- cline.checkpointServiceInitializing = true
- service.on("initialize", () => {
- log("[Cline#getCheckpointService] service initialized")
- try {
- const isCheckpointNeeded =
- typeof cline.clineMessages.find(({ say }) => say === "checkpoint_saved") === "undefined"
- cline.checkpointService = service
- cline.checkpointServiceInitializing = false
- if (isCheckpointNeeded) {
- log("[Cline#getCheckpointService] no checkpoints found, saving initial checkpoint")
- checkpointSave(cline)
- }
- } catch (err) {
- log("[Cline#getCheckpointService] caught error in on('initialize'), disabling checkpoints")
- cline.enableCheckpoints = false
- }
- })
- service.on("checkpoint", ({ isFirst, fromHash: from, toHash: to }) => {
- try {
- provider?.postMessageToWebview({ type: "currentCheckpointUpdated", text: to })
- cline.say("checkpoint_saved", to, undefined, undefined, { isFirst, from, to }).catch((err) => {
- log("[Cline#getCheckpointService] caught unexpected error in say('checkpoint_saved')")
- console.error(err)
- })
- } catch (err) {
- log("[Cline#getCheckpointService] caught unexpected error in on('checkpoint'), disabling checkpoints")
- console.error(err)
- cline.enableCheckpoints = false
- }
- })
- log("[Cline#getCheckpointService] initializing shadow git")
- service.initShadowGit().catch((err) => {
- log(
- `[Cline#getCheckpointService] caught unexpected error in initShadowGit, disabling checkpoints (${err.message})`,
- )
- console.error(err)
- cline.enableCheckpoints = false
- })
- return service
- } catch (err) {
- log("[Cline#getCheckpointService] caught unexpected error, disabling checkpoints")
- cline.enableCheckpoints = false
- return undefined
- }
- }
- async function getInitializedCheckpointService(
- cline: Cline,
- { interval = 250, timeout = 15_000 }: { interval?: number; timeout?: number } = {},
- ) {
- const service = getCheckpointService(cline)
- if (!service || service.isInitialized) {
- return service
- }
- try {
- await pWaitFor(
- () => {
- console.log("[Cline#getCheckpointService] waiting for service to initialize")
- return service.isInitialized
- },
- { interval, timeout },
- )
- return service
- } catch (err) {
- return undefined
- }
- }
- export async function checkpointSave(cline: Cline) {
- const service = getCheckpointService(cline)
- if (!service) {
- return
- }
- if (!service.isInitialized) {
- const provider = cline.providerRef.deref()
- provider?.log("[checkpointSave] checkpoints didn't initialize in time, disabling checkpoints for this task")
- cline.enableCheckpoints = false
- return
- }
- telemetryService.captureCheckpointCreated(cline.taskId)
- // Start the checkpoint process in the background.
- return service.saveCheckpoint(`Task: ${cline.taskId}, Time: ${Date.now()}`).catch((err) => {
- console.error("[Cline#checkpointSave] caught unexpected error, disabling checkpoints", err)
- cline.enableCheckpoints = false
- })
- }
- export type CheckpointRestoreOptions = {
- ts: number
- commitHash: string
- mode: "preview" | "restore"
- }
- export async function checkpointRestore(cline: Cline, { ts, commitHash, mode }: CheckpointRestoreOptions) {
- const service = await getInitializedCheckpointService(cline)
- if (!service) {
- return
- }
- const index = cline.clineMessages.findIndex((m) => m.ts === ts)
- if (index === -1) {
- return
- }
- const provider = cline.providerRef.deref()
- try {
- await service.restoreCheckpoint(commitHash)
- telemetryService.captureCheckpointRestored(cline.taskId)
- await provider?.postMessageToWebview({ type: "currentCheckpointUpdated", text: commitHash })
- if (mode === "restore") {
- await cline.overwriteApiConversationHistory(cline.apiConversationHistory.filter((m) => !m.ts || m.ts < ts))
- const deletedMessages = cline.clineMessages.slice(index + 1)
- const { totalTokensIn, totalTokensOut, totalCacheWrites, totalCacheReads, totalCost } = getApiMetrics(
- cline.combineMessages(deletedMessages),
- )
- await cline.overwriteClineMessages(cline.clineMessages.slice(0, index + 1))
- // TODO: Verify that this is working as expected.
- await cline.say(
- "api_req_deleted",
- JSON.stringify({
- tokensIn: totalTokensIn,
- tokensOut: totalTokensOut,
- cacheWrites: totalCacheWrites,
- cacheReads: totalCacheReads,
- cost: totalCost,
- } satisfies ClineApiReqInfo),
- )
- }
- // The task is already cancelled by the provider beforehand, but we
- // need to re-init to get the updated messages.
- //
- // This was take from Cline's implementation of the checkpoints
- // feature. The cline instance will hang if we don't cancel twice,
- // so this is currently necessary, but it seems like a complicated
- // and hacky solution to a problem that I don't fully understand.
- // I'd like to revisit this in the future and try to improve the
- // task flow and the communication between the webview and the
- // Cline instance.
- provider?.cancelTask()
- } catch (err) {
- provider?.log("[checkpointRestore] disabling checkpoints for this task")
- cline.enableCheckpoints = false
- }
- }
- export type CheckpointDiffOptions = {
- ts: number
- previousCommitHash?: string
- commitHash: string
- mode: "full" | "checkpoint"
- }
- export async function checkpointDiff(
- cline: Cline,
- { ts, previousCommitHash, commitHash, mode }: CheckpointDiffOptions,
- ) {
- const service = await getInitializedCheckpointService(cline)
- if (!service) {
- return
- }
- telemetryService.captureCheckpointDiffed(cline.taskId)
- if (!previousCommitHash && mode === "checkpoint") {
- const previousCheckpoint = cline.clineMessages
- .filter(({ say }) => say === "checkpoint_saved")
- .sort((a, b) => b.ts - a.ts)
- .find((message) => message.ts < ts)
- previousCommitHash = previousCheckpoint?.text
- }
- try {
- const changes = await service.getDiff({ from: previousCommitHash, to: commitHash })
- if (!changes?.length) {
- vscode.window.showInformationMessage("No changes found.")
- return
- }
- await vscode.commands.executeCommand(
- "vscode.changes",
- mode === "full" ? "Changes since task started" : "Changes since previous checkpoint",
- changes.map((change) => [
- vscode.Uri.file(change.paths.absolute),
- vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${change.paths.relative}`).with({
- query: Buffer.from(change.content.before ?? "").toString("base64"),
- }),
- vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${change.paths.relative}`).with({
- query: Buffer.from(change.content.after ?? "").toString("base64"),
- }),
- ]),
- )
- } catch (err) {
- const provider = cline.providerRef.deref()
- provider?.log("[checkpointDiff] disabling checkpoints for this task")
- cline.enableCheckpoints = false
- }
- }
|