index.ts 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. import pWaitFor from "p-wait-for"
  2. import * as vscode from "vscode"
  3. import { Cline } from "../Cline"
  4. import { getWorkspacePath } from "../../utils/path"
  5. import { ClineApiReqInfo } from "../../shared/ExtensionMessage"
  6. import { getApiMetrics } from "../../shared/getApiMetrics"
  7. import { DIFF_VIEW_URI_SCHEME } from "../../integrations/editor/DiffViewProvider"
  8. import { telemetryService } from "../../services/telemetry/TelemetryService"
  9. import { CheckpointServiceOptions, RepoPerTaskCheckpointService } from "../../services/checkpoints"
  10. export function getCheckpointService(cline: Cline) {
  11. if (!cline.enableCheckpoints) {
  12. return undefined
  13. }
  14. if (cline.checkpointService) {
  15. return cline.checkpointService
  16. }
  17. if (cline.checkpointServiceInitializing) {
  18. console.log("[Cline#getCheckpointService] checkpoint service is still initializing")
  19. return undefined
  20. }
  21. const provider = cline.providerRef.deref()
  22. const log = (message: string) => {
  23. console.log(message)
  24. try {
  25. provider?.log(message)
  26. } catch (err) {
  27. // NO-OP
  28. }
  29. }
  30. console.log("[Cline#getCheckpointService] initializing checkpoints service")
  31. try {
  32. const workspaceDir = getWorkspacePath()
  33. if (!workspaceDir) {
  34. log("[Cline#getCheckpointService] workspace folder not found, disabling checkpoints")
  35. cline.enableCheckpoints = false
  36. return undefined
  37. }
  38. const globalStorageDir = provider?.context.globalStorageUri.fsPath
  39. if (!globalStorageDir) {
  40. log("[Cline#getCheckpointService] globalStorageDir not found, disabling checkpoints")
  41. cline.enableCheckpoints = false
  42. return undefined
  43. }
  44. const options: CheckpointServiceOptions = {
  45. taskId: cline.taskId,
  46. workspaceDir,
  47. shadowDir: globalStorageDir,
  48. log,
  49. }
  50. const service = RepoPerTaskCheckpointService.create(options)
  51. cline.checkpointServiceInitializing = true
  52. service.on("initialize", () => {
  53. log("[Cline#getCheckpointService] service initialized")
  54. try {
  55. const isCheckpointNeeded =
  56. typeof cline.clineMessages.find(({ say }) => say === "checkpoint_saved") === "undefined"
  57. cline.checkpointService = service
  58. cline.checkpointServiceInitializing = false
  59. if (isCheckpointNeeded) {
  60. log("[Cline#getCheckpointService] no checkpoints found, saving initial checkpoint")
  61. checkpointSave(cline)
  62. }
  63. } catch (err) {
  64. log("[Cline#getCheckpointService] caught error in on('initialize'), disabling checkpoints")
  65. cline.enableCheckpoints = false
  66. }
  67. })
  68. service.on("checkpoint", ({ isFirst, fromHash: from, toHash: to }) => {
  69. try {
  70. provider?.postMessageToWebview({ type: "currentCheckpointUpdated", text: to })
  71. cline.say("checkpoint_saved", to, undefined, undefined, { isFirst, from, to }).catch((err) => {
  72. log("[Cline#getCheckpointService] caught unexpected error in say('checkpoint_saved')")
  73. console.error(err)
  74. })
  75. } catch (err) {
  76. log("[Cline#getCheckpointService] caught unexpected error in on('checkpoint'), disabling checkpoints")
  77. console.error(err)
  78. cline.enableCheckpoints = false
  79. }
  80. })
  81. log("[Cline#getCheckpointService] initializing shadow git")
  82. service.initShadowGit().catch((err) => {
  83. log(
  84. `[Cline#getCheckpointService] caught unexpected error in initShadowGit, disabling checkpoints (${err.message})`,
  85. )
  86. console.error(err)
  87. cline.enableCheckpoints = false
  88. })
  89. return service
  90. } catch (err) {
  91. log("[Cline#getCheckpointService] caught unexpected error, disabling checkpoints")
  92. cline.enableCheckpoints = false
  93. return undefined
  94. }
  95. }
  96. async function getInitializedCheckpointService(
  97. cline: Cline,
  98. { interval = 250, timeout = 15_000 }: { interval?: number; timeout?: number } = {},
  99. ) {
  100. const service = getCheckpointService(cline)
  101. if (!service || service.isInitialized) {
  102. return service
  103. }
  104. try {
  105. await pWaitFor(
  106. () => {
  107. console.log("[Cline#getCheckpointService] waiting for service to initialize")
  108. return service.isInitialized
  109. },
  110. { interval, timeout },
  111. )
  112. return service
  113. } catch (err) {
  114. return undefined
  115. }
  116. }
  117. export async function checkpointSave(cline: Cline) {
  118. const service = getCheckpointService(cline)
  119. if (!service) {
  120. return
  121. }
  122. if (!service.isInitialized) {
  123. const provider = cline.providerRef.deref()
  124. provider?.log("[checkpointSave] checkpoints didn't initialize in time, disabling checkpoints for this task")
  125. cline.enableCheckpoints = false
  126. return
  127. }
  128. telemetryService.captureCheckpointCreated(cline.taskId)
  129. // Start the checkpoint process in the background.
  130. return service.saveCheckpoint(`Task: ${cline.taskId}, Time: ${Date.now()}`).catch((err) => {
  131. console.error("[Cline#checkpointSave] caught unexpected error, disabling checkpoints", err)
  132. cline.enableCheckpoints = false
  133. })
  134. }
  135. export type CheckpointRestoreOptions = {
  136. ts: number
  137. commitHash: string
  138. mode: "preview" | "restore"
  139. }
  140. export async function checkpointRestore(cline: Cline, { ts, commitHash, mode }: CheckpointRestoreOptions) {
  141. const service = await getInitializedCheckpointService(cline)
  142. if (!service) {
  143. return
  144. }
  145. const index = cline.clineMessages.findIndex((m) => m.ts === ts)
  146. if (index === -1) {
  147. return
  148. }
  149. const provider = cline.providerRef.deref()
  150. try {
  151. await service.restoreCheckpoint(commitHash)
  152. telemetryService.captureCheckpointRestored(cline.taskId)
  153. await provider?.postMessageToWebview({ type: "currentCheckpointUpdated", text: commitHash })
  154. if (mode === "restore") {
  155. await cline.overwriteApiConversationHistory(cline.apiConversationHistory.filter((m) => !m.ts || m.ts < ts))
  156. const deletedMessages = cline.clineMessages.slice(index + 1)
  157. const { totalTokensIn, totalTokensOut, totalCacheWrites, totalCacheReads, totalCost } = getApiMetrics(
  158. cline.combineMessages(deletedMessages),
  159. )
  160. await cline.overwriteClineMessages(cline.clineMessages.slice(0, index + 1))
  161. // TODO: Verify that this is working as expected.
  162. await cline.say(
  163. "api_req_deleted",
  164. JSON.stringify({
  165. tokensIn: totalTokensIn,
  166. tokensOut: totalTokensOut,
  167. cacheWrites: totalCacheWrites,
  168. cacheReads: totalCacheReads,
  169. cost: totalCost,
  170. } satisfies ClineApiReqInfo),
  171. )
  172. }
  173. // The task is already cancelled by the provider beforehand, but we
  174. // need to re-init to get the updated messages.
  175. //
  176. // This was take from Cline's implementation of the checkpoints
  177. // feature. The cline instance will hang if we don't cancel twice,
  178. // so this is currently necessary, but it seems like a complicated
  179. // and hacky solution to a problem that I don't fully understand.
  180. // I'd like to revisit this in the future and try to improve the
  181. // task flow and the communication between the webview and the
  182. // Cline instance.
  183. provider?.cancelTask()
  184. } catch (err) {
  185. provider?.log("[checkpointRestore] disabling checkpoints for this task")
  186. cline.enableCheckpoints = false
  187. }
  188. }
  189. export type CheckpointDiffOptions = {
  190. ts: number
  191. previousCommitHash?: string
  192. commitHash: string
  193. mode: "full" | "checkpoint"
  194. }
  195. export async function checkpointDiff(
  196. cline: Cline,
  197. { ts, previousCommitHash, commitHash, mode }: CheckpointDiffOptions,
  198. ) {
  199. const service = await getInitializedCheckpointService(cline)
  200. if (!service) {
  201. return
  202. }
  203. telemetryService.captureCheckpointDiffed(cline.taskId)
  204. if (!previousCommitHash && mode === "checkpoint") {
  205. const previousCheckpoint = cline.clineMessages
  206. .filter(({ say }) => say === "checkpoint_saved")
  207. .sort((a, b) => b.ts - a.ts)
  208. .find((message) => message.ts < ts)
  209. previousCommitHash = previousCheckpoint?.text
  210. }
  211. try {
  212. const changes = await service.getDiff({ from: previousCommitHash, to: commitHash })
  213. if (!changes?.length) {
  214. vscode.window.showInformationMessage("No changes found.")
  215. return
  216. }
  217. await vscode.commands.executeCommand(
  218. "vscode.changes",
  219. mode === "full" ? "Changes since task started" : "Changes since previous checkpoint",
  220. changes.map((change) => [
  221. vscode.Uri.file(change.paths.absolute),
  222. vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${change.paths.relative}`).with({
  223. query: Buffer.from(change.content.before ?? "").toString("base64"),
  224. }),
  225. vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${change.paths.relative}`).with({
  226. query: Buffer.from(change.content.after ?? "").toString("base64"),
  227. }),
  228. ]),
  229. )
  230. } catch (err) {
  231. const provider = cline.providerRef.deref()
  232. provider?.log("[checkpointDiff] disabling checkpoints for this task")
  233. cline.enableCheckpoints = false
  234. }
  235. }