diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 7d7bedcabd1..bacade3f741 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -370,7 +370,13 @@ export class Cline { this.askResponseImages = images } - async say(type: ClineSay, text?: string, images?: string[], partial?: boolean): Promise { + async say( + type: ClineSay, + text?: string, + images?: string[], + partial?: boolean, + checkpoint?: Record, + ): Promise { 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") diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 33a5767f326..29bc0ec3b62 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -139,6 +139,7 @@ export interface ClineMessage { partial?: boolean reasoning?: string conversationHistoryIndex?: number + checkpoint?: Record } export type ClineAsk = diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index c18fa848892..bf359fe2cd9 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -761,6 +761,7 @@ export const ChatRowContent = ({ ) diff --git a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx index 5eba795b736..b3e88013a81 100644 --- a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx +++ b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx @@ -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() const [isOpen, setIsOpen] = useState(false) const [isConfirming, setIsConfirming] = useState(false) @@ -43,9 +45,11 @@ export const CheckpointMenu = ({ ts, commitHash, currentCheckpointHash }: Checkp return (
- + {!checkpoint?.isFirst && ( + + )} { diff --git a/webview-ui/src/components/chat/checkpoints/CheckpointSaved.tsx b/webview-ui/src/components/chat/checkpoints/CheckpointSaved.tsx index d5c0050e533..d48bfaf7f7e 100644 --- a/webview-ui/src/components/chat/checkpoints/CheckpointSaved.tsx +++ b/webview-ui/src/components/chat/checkpoints/CheckpointSaved.tsx @@ -1,22 +1,37 @@ +import { useMemo } from "react" + import { CheckpointMenu } from "./CheckpointMenu" +import { checkpointSchema } from "./schema" type CheckpointSavedProps = { ts: number commitHash: string + checkpoint?: Record 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 (
- Checkpoint + {isFirst ? "Initial Checkpoint" : "Checkpoint"} {isCurrent && Current}
- +
) } diff --git a/webview-ui/src/components/chat/checkpoints/schema.ts b/webview-ui/src/components/chat/checkpoints/schema.ts new file mode 100644 index 00000000000..4acd32a6ab6 --- /dev/null +++ b/webview-ui/src/components/chat/checkpoints/schema.ts @@ -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