Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/core/checkpoints/__tests__/checkpoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ describe("Checkpoint functionality", () => {
]
})

it("should show diff for full mode", async () => {
it("should show diff for to-current mode", async () => {
const mockChanges = [
{
paths: { absolute: "/test/file.ts", relative: "file.ts" },
Expand All @@ -295,7 +295,7 @@ describe("Checkpoint functionality", () => {
await checkpointDiff(mockTask, {
ts: 4,
commitHash: "commit2",
mode: "full",
mode: "to-current",
})

expect(mockCheckpointService.getDiff).toHaveBeenCalledWith({
Expand All @@ -304,7 +304,7 @@ describe("Checkpoint functionality", () => {
})
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
"vscode.changes",
"Changes since task started",
"Changes to current workspace",
expect.any(Array),
)
})
Expand Down Expand Up @@ -361,7 +361,7 @@ describe("Checkpoint functionality", () => {
await checkpointDiff(mockTask, {
ts: 4,
commitHash: "commit2",
mode: "full",
mode: "to-current",
})

expect(vscode.window.showInformationMessage).toHaveBeenCalledWith("No changes found.")
Expand All @@ -374,7 +374,7 @@ describe("Checkpoint functionality", () => {
await checkpointDiff(mockTask, {
ts: 4,
commitHash: "commit2",
mode: "full",
mode: "to-current",
})

expect(mockTask.enableCheckpoints).toBe(false)
Expand Down
61 changes: 47 additions & 14 deletions src/core/checkpoints/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,16 @@ export async function checkpointRestore(
}

export type CheckpointDiffOptions = {
ts: number
ts?: number
previousCommitHash?: string
commitHash: string
mode: "full" | "checkpoint"
/**
* from-init: Compare from the first checkpoint to the selected checkpoint.
* checkpoint: Compare the selected checkpoint to the next checkpoint.
* to-current: Compare the selected checkpoint to the current workspace.
* full: Compare from the first checkpoint to the current workspace.
*/
mode: "from-init" | "checkpoint" | "to-current" | "full"
}

export async function checkpointDiff(task: Task, { ts, previousCommitHash, commitHash, mode }: CheckpointDiffOptions) {
Expand All @@ -285,21 +291,48 @@ export async function checkpointDiff(task: Task, { ts, previousCommitHash, commi

TelemetryService.instance.captureCheckpointDiffed(task.taskId)

let prevHash = commitHash
let nextHash: string | undefined = undefined
let fromHash: string | undefined
let toHash: string | undefined
let title: string

if (mode !== "full") {
const checkpoints = task.clineMessages.filter(({ say }) => say === "checkpoint_saved").map(({ text }) => text!)
const idx = checkpoints.indexOf(commitHash)
if (idx !== -1 && idx < checkpoints.length - 1) {
nextHash = checkpoints[idx + 1]
} else {
nextHash = undefined
}
const checkpoints = task.clineMessages.filter(({ say }) => say === "checkpoint_saved").map(({ text }) => text!)

if (["from-init", "full"].includes(mode) && checkpoints.length < 1) {
vscode.window.showInformationMessage("No first checkpoint to compare.")
return
}

const idx = checkpoints.indexOf(commitHash)
switch (mode) {
case "checkpoint":
fromHash = commitHash
toHash = idx !== -1 && idx < checkpoints.length - 1 ? checkpoints[idx + 1] : undefined
title = "Changes compare with next checkpoint"
break
case "from-init":
fromHash = checkpoints[0]
toHash = commitHash
title = "Changes since first checkpoint"
break
case "to-current":
fromHash = commitHash
toHash = undefined
title = "Changes to current workspace"
break
case "full":
fromHash = checkpoints[0]
toHash = undefined
title = "Changes since first checkpoint"
break
}

if (!fromHash) {
vscode.window.showInformationMessage("No previous checkpoint to compare.")
return
}

try {
const changes = await service.getDiff({ from: prevHash, to: nextHash })
const changes = await service.getDiff({ from: fromHash, to: toHash })

if (!changes?.length) {
vscode.window.showInformationMessage("No changes found.")
Expand All @@ -308,7 +341,7 @@ export async function checkpointDiff(task: Task, { ts, previousCommitHash, commi

await vscode.commands.executeCommand(
"vscode.changes",
mode === "full" ? "Changes since task started" : "Changes compare with next checkpoint",
title,
changes.map((change) => [
vscode.Uri.file(change.paths.absolute),
vscode.Uri.parse(`${DIFF_VIEW_URI_SCHEME}:${change.paths.relative}`).with({
Expand Down
4 changes: 2 additions & 2 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,10 +306,10 @@ export interface WebviewMessage {
}

export const checkoutDiffPayloadSchema = z.object({
ts: z.number(),
ts: z.number().optional(),
previousCommitHash: z.string().optional(),
commitHash: z.string(),
mode: z.enum(["full", "checkpoint"]),
mode: z.enum(["full", "checkpoint", "from-init", "to-current"]),
})

export type CheckpointDiffPayload = z.infer<typeof checkoutDiffPayloadSchema>
Expand Down
1 change: 1 addition & 0 deletions webview-ui/src/components/chat/ChatRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ interface ChatRowProps {
onFollowUpUnmount?: () => void
isFollowUpAnswered?: boolean
editable?: boolean
hasCheckpoint?: boolean
}

// eslint-disable-next-line @typescript-eslint/no-empty-object-type
Expand Down
2 changes: 2 additions & 0 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1525,6 +1525,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
/>
)
}
const hasCheckpoint = modifiedMessages.some((message) => message.say === "checkpoint_saved")

// regular message
return (
Expand Down Expand Up @@ -1559,6 +1560,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
return tool.tool === "updateTodoList" && enableButtons && !!primaryButtonText
})()
}
hasCheckpoint={hasCheckpoint}
/>
)
},
Expand Down
107 changes: 88 additions & 19 deletions webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,43 @@ type CheckpointMenuBaseProps = {
checkpoint: Checkpoint
}
type CheckpointMenuControlledProps = {
open: boolean
onOpenChange: (open: boolean) => void
}
type CheckpointMenuUncontrolledProps = {
open?: undefined
onOpenChange?: undefined
}
type CheckpointMenuProps = CheckpointMenuBaseProps & (CheckpointMenuControlledProps | CheckpointMenuUncontrolledProps)

export const CheckpointMenu = ({ ts, commitHash, checkpoint, open, onOpenChange }: CheckpointMenuProps) => {
export const CheckpointMenu = ({ ts, commitHash, checkpoint, onOpenChange }: CheckpointMenuProps) => {
const { t } = useTranslation()
const [internalOpen, setInternalOpen] = useState(false)
const [isConfirming, setIsConfirming] = useState(false)
const [internalRestoreOpen, setInternalRestoreOpen] = useState(false)
const [restoreConfirming, setRestoreIsConfirming] = useState(false)
const [internalMoreOpen, setInternalMoreOpen] = useState(false)
const portalContainer = useRooPortal("roo-portal")

const previousCommitHash = checkpoint?.from

const isOpen = open ?? internalOpen
const setOpen = onOpenChange ?? setInternalOpen
const restoreOpen = internalRestoreOpen
const moreOpen = internalMoreOpen
const setRestoreOpen = useCallback(
(open: boolean) => {
setInternalRestoreOpen(open)
if (onOpenChange) {
onOpenChange(open)
}
},
[onOpenChange],
)

const setMoreOpen = useCallback(
(open: boolean) => {
setInternalMoreOpen(open)
if (onOpenChange) {
onOpenChange(open)
}
},
[onOpenChange],
)

const onCheckpointDiff = useCallback(() => {
vscode.postMessage({
Expand All @@ -41,24 +59,38 @@ export const CheckpointMenu = ({ ts, commitHash, checkpoint, open, onOpenChange
})
}, [ts, previousCommitHash, commitHash])

const onDiffFromInit = useCallback(() => {
vscode.postMessage({
type: "checkpointDiff",
payload: { ts, commitHash, mode: "from-init" },
})
}, [ts, commitHash])

const onDiffWithCurrent = useCallback(() => {
vscode.postMessage({
type: "checkpointDiff",
payload: { ts, commitHash, mode: "to-current" },
})
}, [ts, commitHash])

const onPreview = useCallback(() => {
vscode.postMessage({ type: "checkpointRestore", payload: { ts, commitHash, mode: "preview" } })
setOpen(false)
}, [ts, commitHash, setOpen])
setRestoreOpen(false)
}, [ts, commitHash, setRestoreOpen])

const onRestore = useCallback(() => {
vscode.postMessage({ type: "checkpointRestore", payload: { ts, commitHash, mode: "restore" } })
setOpen(false)
}, [ts, commitHash, setOpen])
setRestoreOpen(false)
}, [ts, commitHash, setRestoreOpen])

const handleOpenChange = useCallback(
(open: boolean) => {
setOpen(open)
setRestoreOpen(open)
if (!open) {
setIsConfirming(false)
setRestoreIsConfirming(false)
}
},
[setOpen],
[setRestoreOpen],
)

return (
Expand All @@ -68,7 +100,13 @@ export const CheckpointMenu = ({ ts, commitHash, checkpoint, open, onOpenChange
<span className="codicon codicon-diff-single" />
</Button>
</StandardTooltip>
<Popover open={isOpen} onOpenChange={handleOpenChange}>
<Popover
open={restoreOpen}
onOpenChange={(open) => {
handleOpenChange(open)
setRestoreIsConfirming(false)
}}
data-testid="restore-popover">
<StandardTooltip content={t("chat:checkpoint.menu.restore")}>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" aria-label={t("chat:checkpoint.menu.restore")}>
Expand All @@ -87,10 +125,10 @@ export const CheckpointMenu = ({ ts, commitHash, checkpoint, open, onOpenChange
</div>
</div>
<div className="flex flex-col gap-1 group hover:text-foreground">
{!isConfirming ? (
{!restoreConfirming ? (
<Button
variant="secondary"
onClick={() => setIsConfirming(true)}
onClick={() => setRestoreIsConfirming(true)}
data-testid="restore-files-and-task-btn">
{t("chat:checkpoint.menu.restoreFilesAndTask")}
</Button>
Expand All @@ -106,15 +144,15 @@ export const CheckpointMenu = ({ ts, commitHash, checkpoint, open, onOpenChange
<div>{t("chat:checkpoint.menu.confirm")}</div>
</div>
</Button>
<Button variant="secondary" onClick={() => setIsConfirming(false)}>
<Button variant="secondary" onClick={() => setRestoreIsConfirming(false)}>
<div className="flex flex-row gap-1">
<Cross2Icon />
<div>{t("chat:checkpoint.menu.cancel")}</div>
</div>
</Button>
</>
)}
{isConfirming ? (
{restoreConfirming ? (
<div data-testid="checkpoint-confirm-warning" className="text-destructive font-bold">
{t("chat:checkpoint.menu.cannotUndo")}
</div>
Expand All @@ -127,6 +165,37 @@ export const CheckpointMenu = ({ ts, commitHash, checkpoint, open, onOpenChange
</div>
</PopoverContent>
</Popover>
<Popover open={moreOpen} onOpenChange={(open) => setMoreOpen(open)} data-testid="more-popover">
<StandardTooltip content={t("chat:task.seeMore")}>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" aria-label={t("chat:checkpoint.menu.more")}>
<span className="codicon codicon-kebab-vertical" />
</Button>
</PopoverTrigger>
</StandardTooltip>
<PopoverContent align="end" container={portalContainer}>
<div className="flex flex-col gap-2">
<Button
variant="secondary"
onClick={() => {
onDiffFromInit()
setMoreOpen(false)
}}>
<span className="codicon codicon-versions mr-2" />
{t("chat:checkpoint.menu.viewDiffFromInit")}
</Button>
<Button
variant="secondary"
onClick={() => {
onDiffWithCurrent()
setMoreOpen(false)
}}>
<span className="codicon codicon-diff mr-2" />
{t("chat:checkpoint.menu.viewDiffWithCurrent")}
</Button>
</div>
</PopoverContent>
</Popover>
</div>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ export const CheckpointSaved = ({ checkpoint, currentHash, ...props }: Checkpoin
data-testid="checkpoint-menu-container"
className={cn("h-4 -mt-2", menuVisible ? "block" : "hidden group-hover:block")}>
<CheckpointMenu
{...props}
ts={props.ts}
commitHash={props.commitHash}
checkpoint={metadata}
open={isPopoverOpen}
onOpenChange={handlePopoverOpenChange}
/>
</div>
Expand Down
Loading