Merge pull request #868 from RooVetGit/cte/current-checkpoint-indicator

Current checkpoint indicator
This commit is contained in:
Matt Rubens 2025-02-08 10:57:27 -05:00 committed by GitHub
commit d65e9efc89
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 66 additions and 26 deletions

View file

@ -3313,6 +3313,10 @@ export class Cline {
const commit = await service.saveCheckpoint(`Task: ${this.taskId}, Time: ${Date.now()}`)
if (commit?.commit) {
await this.providerRef
.deref()
?.postMessageToWebview({ type: "currentCheckpointUpdated", text: service.currentCheckpoint })
await this.say("checkpoint_saved", commit.commit)
}
} catch (err) {
@ -3349,6 +3353,10 @@ export class Cline {
const service = await this.getCheckpointService()
await service.restoreCheckpoint(commitHash)
await this.providerRef
.deref()
?.postMessageToWebview({ type: "currentCheckpointUpdated", text: service.currentCheckpoint })
if (mode === "restore") {
await this.overwriteApiConversationHistory(
this.apiConversationHistory.filter((m) => !m.ts || m.ts < ts),

View file

@ -9,12 +9,6 @@ if (process.env.NODE_ENV !== "test") {
debug.enable("simple-git")
}
export interface Checkpoint {
hash: string
message: string
timestamp?: Date
}
export type CheckpointServiceOptions = {
taskId: string
git?: SimpleGit
@ -60,6 +54,16 @@ export type CheckpointServiceOptions = {
*/
export class CheckpointService {
private _currentCheckpoint?: string
public get currentCheckpoint() {
return this._currentCheckpoint
}
private set currentCheckpoint(value: string | undefined) {
this._currentCheckpoint = value
}
constructor(
public readonly taskId: string,
private readonly git: SimpleGit,
@ -217,6 +221,8 @@ export class CheckpointService {
await this.popStash()
}
this.currentCheckpoint = commit.commit
return commit
} catch (err) {
this.log(`[saveCheckpoint] Failed to save checkpoint: ${err instanceof Error ? err.message : String(err)}`)
@ -237,6 +243,7 @@ export class CheckpointService {
await this.ensureBranch(this.mainBranch)
await this.git.clean([CleanOptions.FORCE, CleanOptions.RECURSIVE])
await this.git.raw(["restore", "--source", commitHash, "--worktree", "--", "."])
this.currentCheckpoint = commitHash
}
public static async create({ taskId, git, baseDir, log = console.log }: CheckpointServiceOptions) {
@ -291,7 +298,7 @@ export class CheckpointService {
// the checkpoint (i.e. the `git restore` command doesn't work
// for empty commits).
await fs.writeFile(path.join(baseDir, ".gitkeep"), "")
await git.add(".")
await git.add(".gitkeep")
const commit = await git.commit("Initial commit")
if (!commit.commit) {

View file

@ -291,6 +291,7 @@ describe("CheckpointService", () => {
const baseDir = path.join(os.tmpdir(), `checkpoint-service-test2-${Date.now()}`)
await fs.mkdir(baseDir)
const newTestFile = path.join(baseDir, "test.txt")
await fs.writeFile(newTestFile, "Hello, world!")
const newGit = simpleGit(baseDir)
const initSpy = jest.spyOn(newGit, "init")
@ -300,7 +301,6 @@ describe("CheckpointService", () => {
expect(initSpy).toHaveBeenCalled()
// Save a checkpoint: Hello, world!
await fs.writeFile(newTestFile, "Hello, world!")
const commit1 = await newService.saveCheckpoint("Hello, world!")
expect(commit1?.commit).toBeTruthy()
expect(await fs.readFile(newTestFile, "utf-8")).toBe("Hello, world!")

View file

@ -42,6 +42,7 @@ export interface ExtensionMessage {
| "autoApprovalEnabled"
| "updateCustomMode"
| "deleteCustomMode"
| "currentCheckpointUpdated"
text?: string
action?:
| "chatButtonClicked"

View file

@ -81,7 +81,7 @@ export const ChatRowContent = ({
isLast,
isStreaming,
}: ChatRowContentProps) => {
const { mcpServers, alwaysAllowMcp } = useExtensionState()
const { mcpServers, alwaysAllowMcp, currentCheckpoint } = useExtensionState()
const [reasoningCollapsed, setReasoningCollapsed] = useState(false)
// Auto-collapse reasoning when new messages arrive
@ -757,7 +757,13 @@ export const ChatRowContent = ({
</>
)
case "checkpoint_saved":
return <CheckpointSaved ts={message.ts!} commitHash={message.text!} />
return (
<CheckpointSaved
ts={message.ts!}
commitHash={message.text!}
currentCheckpointHash={currentCheckpoint}
/>
)
default:
return (
<>

View file

@ -8,13 +8,16 @@ import { Button, Popover, PopoverContent, PopoverTrigger } from "@/components/ui
type CheckpointMenuProps = {
ts: number
commitHash: string
currentCheckpointHash?: string
}
export const CheckpointMenu = ({ ts, commitHash }: CheckpointMenuProps) => {
export const CheckpointMenu = ({ ts, commitHash, currentCheckpointHash }: CheckpointMenuProps) => {
const [portalContainer, setPortalContainer] = useState<HTMLElement>()
const [isOpen, setIsOpen] = useState(false)
const [isConfirming, setIsConfirming] = useState(false)
const isCurrent = currentCheckpointHash === commitHash
const onCheckpointDiff = useCallback(() => {
vscode.postMessage({ type: "checkpointDiff", payload: { ts, commitHash, mode: "checkpoint" } })
}, [ts, commitHash])
@ -56,14 +59,16 @@ export const CheckpointMenu = ({ ts, commitHash }: CheckpointMenuProps) => {
</PopoverTrigger>
<PopoverContent align="end" container={portalContainer}>
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-1 group hover:text-foreground">
<Button variant="secondary" onClick={onPreview}>
Restore Files
</Button>
<div className="text-muted transition-colors group-hover:text-foreground">
Restores your project's files back to a snapshot taken at this point.
{!isCurrent && (
<div className="flex flex-col gap-1 group hover:text-foreground">
<Button variant="secondary" onClick={onPreview}>
Restore Files
</Button>
<div className="text-muted transition-colors group-hover:text-foreground">
Restores your project's files back to a snapshot taken at this point.
</div>
</div>
</div>
)}
<div className="flex flex-col gap-1 group hover:text-foreground">
<div className="flex flex-col gap-1 group hover:text-foreground">
{!isConfirming ? (

View file

@ -3,14 +3,20 @@ import { CheckpointMenu } from "./CheckpointMenu"
type CheckpointSavedProps = {
ts: number
commitHash: string
currentCheckpointHash?: string
}
export const CheckpointSaved = (props: CheckpointSavedProps) => (
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="codicon codicon-git-commit text-blue-400" />
<span className="font-bold">Checkpoint</span>
export const CheckpointSaved = (props: CheckpointSavedProps) => {
const isCurrent = props.currentCheckpointHash === props.commitHash
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>
{isCurrent && <span className="text-muted text-sm">Current</span>}
</div>
<CheckpointMenu {...props} />
</div>
<CheckpointMenu {...props} />
</div>
)
)
}

View file

@ -26,6 +26,7 @@ export interface ExtensionStateContextType extends ExtensionState {
openRouterModels: Record<string, ModelInfo>
openAiModels: string[]
mcpServers: McpServer[]
currentCheckpoint?: string
filePaths: string[]
openedTabs: Array<{ label: string; isActive: boolean; path?: string }>
setApiConfiguration: (config: ApiConfiguration) => void
@ -126,6 +127,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
const [openAiModels, setOpenAiModels] = useState<string[]>([])
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
const [currentCheckpoint, setCurrentCheckpoint] = useState<string>()
const setListApiConfigMeta = useCallback(
(value: ApiConfigMeta[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
@ -241,6 +243,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setMcpServers(message.mcpServers ?? [])
break
}
case "currentCheckpointUpdated": {
setCurrentCheckpoint(message.text)
break
}
case "listApiConfig": {
setListApiConfigMeta(message.listApiConfig ?? [])
break
@ -265,6 +271,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
openRouterModels,
openAiModels,
mcpServers,
currentCheckpoint,
filePaths,
openedTabs,
soundVolume: state.soundVolume,