diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 6e7b7f72b8..83cdabd466 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -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), diff --git a/src/services/checkpoints/CheckpointService.ts b/src/services/checkpoints/CheckpointService.ts index af1d438e54..6df220e115 100644 --- a/src/services/checkpoints/CheckpointService.ts +++ b/src/services/checkpoints/CheckpointService.ts @@ -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) { diff --git a/src/services/checkpoints/__tests__/CheckpointService.test.ts b/src/services/checkpoints/__tests__/CheckpointService.test.ts index cd33a5dc7c..faa4cd5cef 100644 --- a/src/services/checkpoints/__tests__/CheckpointService.test.ts +++ b/src/services/checkpoints/__tests__/CheckpointService.test.ts @@ -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!") diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index d1ae925287..18cee3ef95 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -42,6 +42,7 @@ export interface ExtensionMessage { | "autoApprovalEnabled" | "updateCustomMode" | "deleteCustomMode" + | "currentCheckpointUpdated" text?: string action?: | "chatButtonClicked" diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index d932dee5c9..c18fa84889 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -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 + return ( + + ) default: return ( <> diff --git a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx index 39141a6fbf..954dc2e8f3 100644 --- a/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx +++ b/webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx @@ -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() 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) => {
-
- -
- Restores your project's files back to a snapshot taken at this point. + {!isCurrent && ( +
+ +
+ Restores your project's files back to a snapshot taken at this point. +
-
+ )}
{!isConfirming ? ( diff --git a/webview-ui/src/components/chat/checkpoints/CheckpointSaved.tsx b/webview-ui/src/components/chat/checkpoints/CheckpointSaved.tsx index 5947b6a1fc..d5c0050e53 100644 --- a/webview-ui/src/components/chat/checkpoints/CheckpointSaved.tsx +++ b/webview-ui/src/components/chat/checkpoints/CheckpointSaved.tsx @@ -3,14 +3,20 @@ import { CheckpointMenu } from "./CheckpointMenu" type CheckpointSavedProps = { ts: number commitHash: string + currentCheckpointHash?: string } -export const CheckpointSaved = (props: CheckpointSavedProps) => ( -
-
- - Checkpoint +export const CheckpointSaved = (props: CheckpointSavedProps) => { + const isCurrent = props.currentCheckpointHash === props.commitHash + + return ( +
+
+ + Checkpoint + {isCurrent && Current} +
+
- -
-) + ) +} diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 7e6a06894d..d518878aae 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -26,6 +26,7 @@ export interface ExtensionStateContextType extends ExtensionState { openRouterModels: Record 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([]) const [mcpServers, setMcpServers] = useState([]) + const [currentCheckpoint, setCurrentCheckpoint] = useState() 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,