Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
37 changes: 34 additions & 3 deletions webview-ui/src/components/chat/TaskActions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "react"
import { useState, useMemo, useCallback } from "react"
import { useTranslation } from "react-i18next"

import type { HistoryItem } from "@roo-code/types"
Expand All @@ -9,7 +9,15 @@ import { useExtensionState } from "@/context/ExtensionStateContext"

import { DeleteTaskDialog } from "../history/DeleteTaskDialog"
import { ShareButton } from "./ShareButton"
import { CopyIcon, CheckIcon, DownloadIcon, Trash2Icon, FileJsonIcon, MessageSquareCodeIcon } from "lucide-react"
import {
CopyIcon,
CheckIcon,
DownloadIcon,
Trash2Icon,
FileJsonIcon,
MessageSquareCodeIcon,
DiffIcon,
} from "lucide-react"
import { LucideIconButton } from "./LucideIconButton"

interface TaskActionsProps {
Expand All @@ -21,7 +29,26 @@ export const TaskActions = ({ item, buttonsDisabled }: TaskActionsProps) => {
const [deleteTaskId, setDeleteTaskId] = useState<string | null>(null)
const { t } = useTranslation()
const { copyWithFeedback, showCopyFeedback } = useCopyToClipboard()
const { debug } = useExtensionState()
const { debug, enableCheckpoints, clineMessages } = useExtensionState()

const firstCheckpointHash = useMemo(() => {
const msg = (clineMessages ?? []).find((m) => m.say === "checkpoint_saved")
return msg?.text
}, [clineMessages])

const lastCheckpointHash = useMemo(() => {
const checkpointMessages = (clineMessages ?? []).filter((m) => m.say === "checkpoint_saved")
return checkpointMessages.length > 0 ? checkpointMessages[checkpointMessages.length - 1].text : undefined
}, [clineMessages])

const onViewFullDiff = useCallback(() => {
if (lastCheckpointHash) {
vscode.postMessage({
type: "checkpointDiff",
payload: { commitHash: lastCheckpointHash, mode: "full" },
})
}
}, [lastCheckpointHash])

return (
<div className="flex flex-row items-center -ml-0.5 mt-1 gap-1">
Expand All @@ -31,6 +58,10 @@ export const TaskActions = ({ item, buttonsDisabled }: TaskActionsProps) => {
onClick={() => vscode.postMessage({ type: "exportCurrentTask" })}
/>

{enableCheckpoints && firstCheckpointHash && (
<LucideIconButton icon={DiffIcon} title={t("chat:task.viewDiff")} onClick={onViewFullDiff} />
)}

{item?.task && (
<LucideIconButton
icon={showCopyFeedback ? CheckIcon : CopyIcon}
Expand Down
72 changes: 72 additions & 0 deletions webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ vi.mock("react-i18next", () => ({
"chat:task.sharingDisabledByOrganization": "Sharing disabled by organization",
"chat:task.openApiHistory": "Open API History",
"chat:task.openUiHistory": "Open UI History",
"chat:task.viewDiff": "View all changes since task started",
"cloud:cloudBenefitsTitle": "Connect to Roo Code Cloud",
"cloud:cloudBenefitHistory": "Access your task history from anywhere",
"cloud:cloudBenefitSharing": "Share tasks with your team",
Expand Down Expand Up @@ -525,4 +526,75 @@ describe("TaskActions", () => {
})
})
})

describe("View Diff Button", () => {
it("renders view diff button when checkpoints are enabled and checkpoint exists", () => {
mockUseExtensionState.mockReturnValue({
sharingEnabled: true,
cloudIsAuthenticated: true,
cloudUserInfo: { organizationName: "Test Organization" },
enableCheckpoints: true,
clineMessages: [{ say: "checkpoint_saved", text: "abc123", ts: 1000 }],
} as any)

render(<TaskActions item={mockItem} buttonsDisabled={false} />)

const viewDiffButton = screen.getByLabelText("View all changes since task started")
expect(viewDiffButton).toBeInTheDocument()
})

it("does not render view diff button when checkpoints are disabled", () => {
mockUseExtensionState.mockReturnValue({
sharingEnabled: true,
cloudIsAuthenticated: true,
cloudUserInfo: { organizationName: "Test Organization" },
enableCheckpoints: false,
clineMessages: [{ say: "checkpoint_saved", text: "abc123", ts: 1000 }],
} as any)

render(<TaskActions item={mockItem} buttonsDisabled={false} />)

const viewDiffButton = screen.queryByLabelText("View all changes since task started")
expect(viewDiffButton).toBeNull()
})

it("does not render view diff button when no checkpoints exist", () => {
mockUseExtensionState.mockReturnValue({
sharingEnabled: true,
cloudIsAuthenticated: true,
cloudUserInfo: { organizationName: "Test Organization" },
enableCheckpoints: true,
clineMessages: [],
} as any)

render(<TaskActions item={mockItem} buttonsDisabled={false} />)

const viewDiffButton = screen.queryByLabelText("View all changes since task started")
expect(viewDiffButton).toBeNull()
})

it("sends checkpointDiff message with full mode when view diff button is clicked", () => {
mockUseExtensionState.mockReturnValue({
sharingEnabled: true,
cloudIsAuthenticated: true,
cloudUserInfo: { organizationName: "Test Organization" },
enableCheckpoints: true,
clineMessages: [
{ say: "checkpoint_saved", text: "first-hash", ts: 1000 },
{ say: "text", text: "some message", ts: 2000 },
{ say: "checkpoint_saved", text: "last-hash", ts: 3000 },
],
} as any)

render(<TaskActions item={mockItem} buttonsDisabled={false} />)

const viewDiffButton = screen.getByLabelText("View all changes since task started")
fireEvent.click(viewDiffButton)

expect(mockPostMessage).toHaveBeenCalledWith({
type: "checkpointDiff",
payload: { commitHash: "last-hash", mode: "full" },
})
})
})
})
1 change: 1 addition & 0 deletions webview-ui/src/i18n/locales/en/chat.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"shareSuccessPublic": "Public link copied to clipboard",
"openApiHistory": "Open API History",
"openUiHistory": "Open UI History",
"viewDiff": "View all changes since task started",
"backToParentTask": "Parent task"
},
"unpin": "Unpin",
Expand Down
Loading