Skip to content

Commit 1d65824

Browse files
authored
Merge pull request #866 from RooVetGit/cte/checkpoints-ui-tweaks
Checkpoint UI tweaks
2 parents 121f3ab + d65e9ef commit 1d65824

File tree

9 files changed

+130
-64
lines changed

9 files changed

+130
-64
lines changed

src/core/Cline.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3313,6 +3313,10 @@ export class Cline {
33133313
const commit = await service.saveCheckpoint(`Task: ${this.taskId}, Time: ${Date.now()}`)
33143314

33153315
if (commit?.commit) {
3316+
await this.providerRef
3317+
.deref()
3318+
?.postMessageToWebview({ type: "currentCheckpointUpdated", text: service.currentCheckpoint })
3319+
33163320
await this.say("checkpoint_saved", commit.commit)
33173321
}
33183322
} catch (err) {
@@ -3349,6 +3353,10 @@ export class Cline {
33493353
const service = await this.getCheckpointService()
33503354
await service.restoreCheckpoint(commitHash)
33513355

3356+
await this.providerRef
3357+
.deref()
3358+
?.postMessageToWebview({ type: "currentCheckpointUpdated", text: service.currentCheckpoint })
3359+
33523360
if (mode === "restore") {
33533361
await this.overwriteApiConversationHistory(
33543362
this.apiConversationHistory.filter((m) => !m.ts || m.ts < ts),

src/services/checkpoints/CheckpointService.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,6 @@ if (process.env.NODE_ENV !== "test") {
99
debug.enable("simple-git")
1010
}
1111

12-
export interface Checkpoint {
13-
hash: string
14-
message: string
15-
timestamp?: Date
16-
}
17-
1812
export type CheckpointServiceOptions = {
1913
taskId: string
2014
git?: SimpleGit
@@ -60,6 +54,16 @@ export type CheckpointServiceOptions = {
6054
*/
6155

6256
export class CheckpointService {
57+
private _currentCheckpoint?: string
58+
59+
public get currentCheckpoint() {
60+
return this._currentCheckpoint
61+
}
62+
63+
private set currentCheckpoint(value: string | undefined) {
64+
this._currentCheckpoint = value
65+
}
66+
6367
constructor(
6468
public readonly taskId: string,
6569
private readonly git: SimpleGit,
@@ -217,6 +221,8 @@ export class CheckpointService {
217221
await this.popStash()
218222
}
219223

224+
this.currentCheckpoint = commit.commit
225+
220226
return commit
221227
} catch (err) {
222228
this.log(`[saveCheckpoint] Failed to save checkpoint: ${err instanceof Error ? err.message : String(err)}`)
@@ -237,6 +243,7 @@ export class CheckpointService {
237243
await this.ensureBranch(this.mainBranch)
238244
await this.git.clean([CleanOptions.FORCE, CleanOptions.RECURSIVE])
239245
await this.git.raw(["restore", "--source", commitHash, "--worktree", "--", "."])
246+
this.currentCheckpoint = commitHash
240247
}
241248

242249
public static async create({ taskId, git, baseDir, log = console.log }: CheckpointServiceOptions) {
@@ -291,7 +298,7 @@ export class CheckpointService {
291298
// the checkpoint (i.e. the `git restore` command doesn't work
292299
// for empty commits).
293300
await fs.writeFile(path.join(baseDir, ".gitkeep"), "")
294-
await git.add(".")
301+
await git.add(".gitkeep")
295302
const commit = await git.commit("Initial commit")
296303

297304
if (!commit.commit) {

src/services/checkpoints/__tests__/CheckpointService.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ describe("CheckpointService", () => {
291291
const baseDir = path.join(os.tmpdir(), `checkpoint-service-test2-${Date.now()}`)
292292
await fs.mkdir(baseDir)
293293
const newTestFile = path.join(baseDir, "test.txt")
294+
await fs.writeFile(newTestFile, "Hello, world!")
294295

295296
const newGit = simpleGit(baseDir)
296297
const initSpy = jest.spyOn(newGit, "init")
@@ -300,7 +301,6 @@ describe("CheckpointService", () => {
300301
expect(initSpy).toHaveBeenCalled()
301302

302303
// Save a checkpoint: Hello, world!
303-
await fs.writeFile(newTestFile, "Hello, world!")
304304
const commit1 = await newService.saveCheckpoint("Hello, world!")
305305
expect(commit1?.commit).toBeTruthy()
306306
expect(await fs.readFile(newTestFile, "utf-8")).toBe("Hello, world!")

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export interface ExtensionMessage {
4242
| "autoApprovalEnabled"
4343
| "updateCustomMode"
4444
| "deleteCustomMode"
45+
| "currentCheckpointUpdated"
4546
text?: string
4647
action?:
4748
| "chatButtonClicked"

webview-ui/src/components/chat/ChatRow.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export const ChatRowContent = ({
8181
isLast,
8282
isStreaming,
8383
}: ChatRowContentProps) => {
84-
const { mcpServers, alwaysAllowMcp } = useExtensionState()
84+
const { mcpServers, alwaysAllowMcp, currentCheckpoint } = useExtensionState()
8585
const [reasoningCollapsed, setReasoningCollapsed] = useState(false)
8686

8787
// Auto-collapse reasoning when new messages arrive
@@ -757,7 +757,13 @@ export const ChatRowContent = ({
757757
</>
758758
)
759759
case "checkpoint_saved":
760-
return <CheckpointSaved ts={message.ts!} commitHash={message.text!} />
760+
return (
761+
<CheckpointSaved
762+
ts={message.ts!}
763+
commitHash={message.text!}
764+
currentCheckpointHash={currentCheckpoint}
765+
/>
766+
)
761767
default:
762768
return (
763769
<>

webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx

Lines changed: 72 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,35 @@
11
import { useState, useEffect, useCallback } from "react"
2-
import { DotsHorizontalIcon } from "@radix-ui/react-icons"
3-
import { DropdownMenuItemProps } from "@radix-ui/react-dropdown-menu"
2+
import { CheckIcon, Cross2Icon } from "@radix-ui/react-icons"
43

54
import { vscode } from "../../../utils/vscode"
65

7-
import {
8-
Button,
9-
DropdownMenu,
10-
DropdownMenuTrigger,
11-
DropdownMenuContent,
12-
DropdownMenuItem,
13-
DropdownMenuShortcut,
14-
} from "@/components/ui"
6+
import { Button, Popover, PopoverContent, PopoverTrigger } from "@/components/ui"
157

168
type CheckpointMenuProps = {
179
ts: number
1810
commitHash: string
11+
currentCheckpointHash?: string
1912
}
2013

21-
export const CheckpointMenu = ({ ts, commitHash }: CheckpointMenuProps) => {
14+
export const CheckpointMenu = ({ ts, commitHash, currentCheckpointHash }: CheckpointMenuProps) => {
2215
const [portalContainer, setPortalContainer] = useState<HTMLElement>()
16+
const [isOpen, setIsOpen] = useState(false)
17+
const [isConfirming, setIsConfirming] = useState(false)
2318

24-
const onTaskDiff = useCallback(() => {
25-
vscode.postMessage({ type: "checkpointDiff", payload: { ts, commitHash, mode: "full" } })
26-
}, [ts, commitHash])
19+
const isCurrent = currentCheckpointHash === commitHash
2720

2821
const onCheckpointDiff = useCallback(() => {
2922
vscode.postMessage({ type: "checkpointDiff", payload: { ts, commitHash, mode: "checkpoint" } })
3023
}, [ts, commitHash])
3124

3225
const onPreview = useCallback(() => {
3326
vscode.postMessage({ type: "checkpointRestore", payload: { ts, commitHash, mode: "preview" } })
27+
setIsOpen(false)
3428
}, [ts, commitHash])
3529

3630
const onRestore = useCallback(() => {
3731
vscode.postMessage({ type: "checkpointRestore", payload: { ts, commitHash, mode: "restore" } })
32+
setIsOpen(false)
3833
}, [ts, commitHash])
3934

4035
useEffect(() => {
@@ -47,34 +42,68 @@ export const CheckpointMenu = ({ ts, commitHash }: CheckpointMenuProps) => {
4742
}, [])
4843

4944
return (
50-
<DropdownMenu>
51-
<DropdownMenuTrigger asChild>
52-
<Button variant="ghost" size="icon">
53-
<DotsHorizontalIcon />
54-
</Button>
55-
</DropdownMenuTrigger>
56-
<DropdownMenuContent container={portalContainer} align="end">
57-
<CheckpointMenuItem label="Checkpoint Diff" icon="diff-single" onClick={onCheckpointDiff} />
58-
<CheckpointMenuItem label="Task Diff" icon="diff-multiple" onClick={onTaskDiff} />
59-
<CheckpointMenuItem label="Preview" icon="open-preview" onClick={onPreview} />
60-
<CheckpointMenuItem label="Restore" icon="history" onClick={onRestore} />
61-
</DropdownMenuContent>
62-
</DropdownMenu>
45+
<div className="flex flex-row gap-1">
46+
<Button variant="ghost" size="icon" onClick={onCheckpointDiff}>
47+
<span className="codicon codicon-diff-single" />
48+
</Button>
49+
<Popover
50+
open={isOpen}
51+
onOpenChange={(open) => {
52+
setIsOpen(open)
53+
setIsConfirming(false)
54+
}}>
55+
<PopoverTrigger asChild>
56+
<Button variant="ghost" size="icon">
57+
<span className="codicon codicon-history" />
58+
</Button>
59+
</PopoverTrigger>
60+
<PopoverContent align="end" container={portalContainer}>
61+
<div className="flex flex-col gap-2">
62+
{!isCurrent && (
63+
<div className="flex flex-col gap-1 group hover:text-foreground">
64+
<Button variant="secondary" onClick={onPreview}>
65+
Restore Files
66+
</Button>
67+
<div className="text-muted transition-colors group-hover:text-foreground">
68+
Restores your project's files back to a snapshot taken at this point.
69+
</div>
70+
</div>
71+
)}
72+
<div className="flex flex-col gap-1 group hover:text-foreground">
73+
<div className="flex flex-col gap-1 group hover:text-foreground">
74+
{!isConfirming ? (
75+
<Button variant="secondary" onClick={() => setIsConfirming(true)}>
76+
Restore Files & Task
77+
</Button>
78+
) : (
79+
<>
80+
<Button variant="default" onClick={onRestore} className="grow">
81+
<div className="flex flex-row gap-1">
82+
<CheckIcon />
83+
<div>Confirm</div>
84+
</div>
85+
</Button>
86+
<Button variant="secondary" onClick={() => setIsConfirming(false)}>
87+
<div className="flex flex-row gap-1">
88+
<Cross2Icon />
89+
<div>Cancel</div>
90+
</div>
91+
</Button>
92+
</>
93+
)}
94+
{isConfirming ? (
95+
<div className="text-destructive font-bold">This action cannot be undone.</div>
96+
) : (
97+
<div className="text-muted transition-colors group-hover:text-foreground">
98+
Restores your project's files back to a snapshot taken at this point and deletes
99+
all messages after this point.
100+
</div>
101+
)}
102+
</div>
103+
</div>
104+
</div>
105+
</PopoverContent>
106+
</Popover>
107+
</div>
63108
)
64109
}
65-
66-
type CheckpointMenuItemProps = DropdownMenuItemProps & {
67-
label: React.ReactNode
68-
icon: "diff-single" | "diff-multiple" | "open-preview" | "history"
69-
}
70-
71-
const CheckpointMenuItem = ({ label, icon, ...props }: CheckpointMenuItemProps) => (
72-
<DropdownMenuItem {...props}>
73-
<div className="flex flex-row-reverse gap-1">
74-
<div>{label}</div>
75-
<DropdownMenuShortcut>
76-
<span className={`codicon codicon-${icon}`} />
77-
</DropdownMenuShortcut>
78-
</div>
79-
</DropdownMenuItem>
80-
)

webview-ui/src/components/chat/checkpoints/CheckpointSaved.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,20 @@ import { CheckpointMenu } from "./CheckpointMenu"
33
type CheckpointSavedProps = {
44
ts: number
55
commitHash: string
6+
currentCheckpointHash?: string
67
}
78

8-
export const CheckpointSaved = (props: CheckpointSavedProps) => (
9-
<div className="flex items-center justify-between">
10-
<div className="flex items-center gap-2">
11-
<span className="codicon codicon-git-commit" />
12-
<span className="font-bold">Checkpoint</span>
9+
export const CheckpointSaved = (props: CheckpointSavedProps) => {
10+
const isCurrent = props.currentCheckpointHash === props.commitHash
11+
12+
return (
13+
<div className="flex items-center justify-between">
14+
<div className="flex gap-2">
15+
<span className="codicon codicon-git-commit text-blue-400" />
16+
<span className="font-bold">Checkpoint</span>
17+
{isCurrent && <span className="text-muted text-sm">Current</span>}
18+
</div>
19+
<CheckpointMenu {...props} />
1320
</div>
14-
<CheckpointMenu {...props} />
15-
</div>
16-
)
21+
)
22+
}

webview-ui/src/components/ui/popover.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ const PopoverAnchor = PopoverPrimitive.Anchor
1111

1212
const PopoverContent = React.forwardRef<
1313
React.ElementRef<typeof PopoverPrimitive.Content>,
14-
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
15-
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16-
<PopoverPrimitive.Portal>
14+
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
15+
container?: HTMLElement
16+
}
17+
>(({ className, align = "center", sideOffset = 4, container, ...props }, ref) => (
18+
<PopoverPrimitive.Portal container={container}>
1719
<PopoverPrimitive.Content
1820
ref={ref}
1921
align={align}

webview-ui/src/context/ExtensionStateContext.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface ExtensionStateContextType extends ExtensionState {
2626
openRouterModels: Record<string, ModelInfo>
2727
openAiModels: string[]
2828
mcpServers: McpServer[]
29+
currentCheckpoint?: string
2930
filePaths: string[]
3031
openedTabs: Array<{ label: string; isActive: boolean; path?: string }>
3132
setApiConfiguration: (config: ApiConfiguration) => void
@@ -126,6 +127,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
126127

127128
const [openAiModels, setOpenAiModels] = useState<string[]>([])
128129
const [mcpServers, setMcpServers] = useState<McpServer[]>([])
130+
const [currentCheckpoint, setCurrentCheckpoint] = useState<string>()
129131

130132
const setListApiConfigMeta = useCallback(
131133
(value: ApiConfigMeta[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
@@ -241,6 +243,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
241243
setMcpServers(message.mcpServers ?? [])
242244
break
243245
}
246+
case "currentCheckpointUpdated": {
247+
setCurrentCheckpoint(message.text)
248+
break
249+
}
244250
case "listApiConfig": {
245251
setListApiConfigMeta(message.listApiConfig ?? [])
246252
break
@@ -265,6 +271,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
265271
openRouterModels,
266272
openAiModels,
267273
mcpServers,
274+
currentCheckpoint,
268275
filePaths,
269276
openedTabs,
270277
soundVolume: state.soundVolume,

0 commit comments

Comments
 (0)