Merge pull request #946 from RooVetGit/cte/store-initial-checkpoint

Store initial checkpoint
This commit is contained in:
Chris Estreich 2025-02-11 11:17:19 -08:00 committed by GitHub
commit be2e0b775e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 63 additions and 12 deletions

View file

@ -370,7 +370,13 @@ export class Cline {
this.askResponseImages = images
}
async say(type: ClineSay, text?: string, images?: string[], partial?: boolean): Promise<undefined> {
async say(
type: ClineSay,
text?: string,
images?: string[],
partial?: boolean,
checkpoint?: Record<string, unknown>,
): Promise<undefined> {
if (this.abort) {
throw new Error("Roo Code instance aborted")
}
@ -423,7 +429,7 @@ export class Cline {
// this is a new non-partial message, so add it like normal
const sayTs = Date.now()
this.lastMessageTs = sayTs
await this.addToClineMessages({ ts: sayTs, type: "say", say: type, text, images })
await this.addToClineMessages({ ts: sayTs, type: "say", say: type, text, images, checkpoint })
await this.providerRef.deref()?.postStateToWebview()
}
}
@ -2747,6 +2753,13 @@ export class Cline {
// get previous api req's index to check token usage and determine if we need to truncate conversation history
const previousApiReqIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
// Save checkpoint if this is the first API request.
const isFirstRequest = this.clineMessages.filter((m) => m.say === "api_req_started").length === 0
if (isFirstRequest) {
await this.checkpointSave()
}
// getting verbose details is an expensive operation, it uses globby to top-down build file structure of project which for large projects can take a few seconds
// for the best UX we show a placeholder api_req_started message with a loading spinner as this happens
await this.say(
@ -3299,6 +3312,7 @@ export class Cline {
}
try {
const isFirst = !this.checkpointService
const service = await this.getCheckpointService()
const commit = await service.saveCheckpoint(`Task: ${this.taskId}, Time: ${Date.now()}`)
@ -3307,7 +3321,14 @@ export class Cline {
.deref()
?.postMessageToWebview({ type: "currentCheckpointUpdated", text: service.currentCheckpoint })
await this.say("checkpoint_saved", commit.commit)
// Checkpoint metadata required by the UI.
const checkpoint = {
isFirst,
from: service.baseCommitHash,
to: commit.commit,
}
await this.say("checkpoint_saved", commit.commit, undefined, undefined, checkpoint)
}
} catch (err) {
this.providerRef.deref()?.log("[checkpointSave] disabling checkpoints for this task")

View file

@ -139,6 +139,7 @@ export interface ClineMessage {
partial?: boolean
reasoning?: string
conversationHistoryIndex?: number
checkpoint?: Record<string, unknown>
}
export type ClineAsk =

View file

@ -761,6 +761,7 @@ export const ChatRowContent = ({
<CheckpointSaved
ts={message.ts!}
commitHash={message.text!}
checkpoint={message.checkpoint}
currentCheckpointHash={currentCheckpoint}
/>
)

View file

@ -1,17 +1,19 @@
import { useState, useEffect, useCallback } from "react"
import { CheckIcon, Cross2Icon } from "@radix-ui/react-icons"
import { vscode } from "../../../utils/vscode"
import { Button, Popover, PopoverContent, PopoverTrigger } from "@/components/ui"
import { vscode } from "../../../utils/vscode"
import { Checkpoint } from "./schema"
type CheckpointMenuProps = {
ts: number
commitHash: string
checkpoint?: Checkpoint
currentCheckpointHash?: string
}
export const CheckpointMenu = ({ ts, commitHash, currentCheckpointHash }: CheckpointMenuProps) => {
export const CheckpointMenu = ({ ts, commitHash, checkpoint, currentCheckpointHash }: CheckpointMenuProps) => {
const [portalContainer, setPortalContainer] = useState<HTMLElement>()
const [isOpen, setIsOpen] = useState(false)
const [isConfirming, setIsConfirming] = useState(false)
@ -43,9 +45,11 @@ export const CheckpointMenu = ({ ts, commitHash, currentCheckpointHash }: Checkp
return (
<div className="flex flex-row gap-1">
<Button variant="ghost" size="icon" onClick={onCheckpointDiff} title="View Diff">
<span className="codicon codicon-diff-single" />
</Button>
{!checkpoint?.isFirst && (
<Button variant="ghost" size="icon" onClick={onCheckpointDiff} title="View Diff">
<span className="codicon codicon-diff-single" />
</Button>
)}
<Popover
open={isOpen}
onOpenChange={(open) => {

View file

@ -1,22 +1,37 @@
import { useMemo } from "react"
import { CheckpointMenu } from "./CheckpointMenu"
import { checkpointSchema } from "./schema"
type CheckpointSavedProps = {
ts: number
commitHash: string
checkpoint?: Record<string, unknown>
currentCheckpointHash?: string
}
export const CheckpointSaved = (props: CheckpointSavedProps) => {
export const CheckpointSaved = ({ checkpoint, ...props }: CheckpointSavedProps) => {
const isCurrent = props.currentCheckpointHash === props.commitHash
const metadata = useMemo(() => {
if (!checkpoint) {
return undefined
}
const result = checkpointSchema.safeParse(checkpoint)
return result.success ? result.data : undefined
}, [checkpoint])
const isFirst = !!metadata?.isFirst
return (
<div className="flex items-center justify-between">
<div className="flex gap-2">
<span className="codicon codicon-git-commit text-blue-400" />
<span className="font-bold">Checkpoint</span>
<span className="font-bold">{isFirst ? "Initial Checkpoint" : "Checkpoint"}</span>
{isCurrent && <span className="text-muted text-sm">Current</span>}
</div>
<CheckpointMenu {...props} />
<CheckpointMenu {...props} checkpoint={metadata} />
</div>
)
}

View file

@ -0,0 +1,9 @@
import { z } from "zod"
export const checkpointSchema = z.object({
isFirst: z.boolean(),
from: z.string(),
to: z.string(),
})
export type Checkpoint = z.infer<typeof checkpointSchema>