From e686deea352533898139fd12c029c206048e6144 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Sat, 18 Apr 2026 20:37:23 +0000 Subject: [PATCH] feat: add "View Diff" button to task actions for viewing all changes since task started --- .../src/components/chat/TaskActions.tsx | 37 +++++++++- .../chat/__tests__/TaskActions.spec.tsx | 72 +++++++++++++++++++ webview-ui/src/i18n/locales/en/chat.json | 1 + 3 files changed, 107 insertions(+), 3 deletions(-) diff --git a/webview-ui/src/components/chat/TaskActions.tsx b/webview-ui/src/components/chat/TaskActions.tsx index 7646f4bc0ee..5372731797d 100644 --- a/webview-ui/src/components/chat/TaskActions.tsx +++ b/webview-ui/src/components/chat/TaskActions.tsx @@ -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" @@ -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 { @@ -21,7 +29,26 @@ export const TaskActions = ({ item, buttonsDisabled }: TaskActionsProps) => { const [deleteTaskId, setDeleteTaskId] = useState(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 (
@@ -31,6 +58,10 @@ export const TaskActions = ({ item, buttonsDisabled }: TaskActionsProps) => { onClick={() => vscode.postMessage({ type: "exportCurrentTask" })} /> + {enableCheckpoints && firstCheckpointHash && ( + + )} + {item?.task && ( ({ "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", @@ -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() + + 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() + + 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() + + 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() + + const viewDiffButton = screen.getByLabelText("View all changes since task started") + fireEvent.click(viewDiffButton) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "checkpointDiff", + payload: { commitHash: "last-hash", mode: "full" }, + }) + }) + }) }) diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index c4deaab8bc3..bffeb9f0f7e 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -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",