+ {isCompact ? (
+ <>
+ {/* Compact Cache */}
+ {!!item.cacheWrites && (
+
+
+ {formatLargeNumber(item.cacheWrites || 0)}
+
+ {formatLargeNumber(item.cacheReads || 0)}
+
+ )}
+
+ {/* Compact Tokens */}
+ {(item.tokensIn || item.tokensOut) && (
+ <>
+
+ ↑ {formatLargeNumber(item.tokensIn || 0)}
+
+
+ ↓ {formatLargeNumber(item.tokensOut || 0)}
+
+ >
+ )}
+ {/* Compact Cost */}
+ {!!item.totalCost && (
+
+
+ {"$" + item.totalCost.toFixed(2)}
+
+ )}
+ >
+ ) : (
+ <>
+
+ {/* Cache Info */}
+ {!!item.cacheWrites && (
+
+ {t("history:cacheLabel")}
+
+
+ {formatLargeNumber(item.cacheWrites || 0)}
+
+
+
+ {formatLargeNumber(item.cacheReads || 0)}
+
+
+ )}
+
+ {/* Full Tokens */}
+ {(item.tokensIn || item.tokensOut) && (
+
+ {t("history:tokensLabel")}
+
+
+ {formatLargeNumber(item.tokensIn || 0)}
+
+
+
+ {formatLargeNumber(item.tokensOut || 0)}
+
+
+ )}
+ {/* Full Cost */}
+ {!!item.totalCost && (
+
+ {t("history:apiCostLabel")}
+ {"$" + item.totalCost.toFixed(4)}
+
+ )}
+
+ {/* Action Buttons for non-compact view */}
+ {!isSelectionMode && (
+
+
+
+
+ )}
+ >
+ )}
+
+ )
+}
+
+export default TaskItemFooter
diff --git a/webview-ui/src/components/history/TaskItemHeader.tsx b/webview-ui/src/components/history/TaskItemHeader.tsx
new file mode 100644
index 0000000000..0837d7e0b8
--- /dev/null
+++ b/webview-ui/src/components/history/TaskItemHeader.tsx
@@ -0,0 +1,74 @@
+import React from "react"
+import type { HistoryItem } from "@roo-code/types"
+import prettyBytes from "pretty-bytes"
+import { vscode } from "@/utils/vscode"
+import { formatDate } from "@/utils/format"
+import { Button } from "@/components/ui"
+import { CopyButton } from "./CopyButton"
+
+export interface TaskItemHeaderProps {
+ item: HistoryItem
+ variant: "compact" | "full"
+ isSelectionMode: boolean
+ t: (key: string, options?: any) => string
+ onDelete?: (taskId: string) => void
+}
+
+const TaskItemHeader: React.FC = ({ item, variant, isSelectionMode, t, onDelete }) => {
+ const isCompact = variant === "compact"
+
+ // Standardized icon styles
+ const actionIconStyle: React.CSSProperties = {
+ fontSize: "16px",
+ color: "var(--vscode-descriptionForeground)",
+ verticalAlign: "middle",
+ }
+
+ const handleDeleteClick = (e: React.MouseEvent) => {
+ e.stopPropagation()
+ if (e.shiftKey) {
+ vscode.postMessage({ type: "deleteTaskWithId", text: item.id })
+ } else if (onDelete) {
+ onDelete(item.id)
+ }
+ }
+
+ return (
+
+
+
+ {formatDate(item.ts)}
+
+
+
+ {/* Action Buttons */}
+ {!isSelectionMode && (
+
+ {isCompact ? (
+
+ ) : (
+ <>
+ {onDelete && (
+
+ )}
+ {!isCompact && item.size && (
+
+ {prettyBytes(item.size)}
+
+ )}
+ >
+ )}
+
+ )}
+
+ )
+}
+
+export default TaskItemHeader
diff --git a/webview-ui/src/components/history/__tests__/HistoryView.test.tsx b/webview-ui/src/components/history/__tests__/HistoryView.test.tsx
index b4d9f22c48..9057b23067 100644
--- a/webview-ui/src/components/history/__tests__/HistoryView.test.tsx
+++ b/webview-ui/src/components/history/__tests__/HistoryView.test.tsx
@@ -8,6 +8,20 @@ import { vscode } from "@src/utils/vscode"
jest.mock("@src/context/ExtensionStateContext")
jest.mock("@src/utils/vscode")
jest.mock("@src/i18n/TranslationContext")
+jest.mock("@/components/ui/checkbox", () => ({
+ Checkbox: jest.fn(({ checked, onCheckedChange, ...props }) => (
+ onCheckedChange(e.target.checked)}
+ {...props}
+ />
+ )),
+}))
+jest.mock("lucide-react", () => ({
+ DollarSign: () => $,
+}))
jest.mock("react-virtuoso", () => ({
Virtuoso: ({ data, itemContent }: any) => (
@@ -259,8 +273,22 @@ describe("HistoryView", () => {
// Find first task container and check date format
const taskContainer = screen.getByTestId("virtuoso-item-1")
- const dateElement = within(taskContainer).getByText((content) => {
- return content.includes("FEBRUARY 16") && content.includes("12:00 AM")
+ // Date is directly in TaskItemHeader, which is a child of TaskItem (rendered by virtuoso)
+ const dateElement = within(taskContainer).getByText((content, element) => {
+ if (!element) {
+ return false
+ }
+ const parent = element.parentElement
+ if (!parent) {
+ return false
+ }
+ return (
+ element.tagName.toLowerCase() === "span" &&
+ parent.classList.contains("flex") &&
+ parent.classList.contains("items-center") &&
+ content.includes("FEBRUARY 16") &&
+ content.includes("12:00 AM")
+ )
})
expect(dateElement).toBeInTheDocument()
})
@@ -272,10 +300,9 @@ describe("HistoryView", () => {
// Find first task container
const taskContainer = screen.getByTestId("virtuoso-item-1")
- // Find token counts within the task container
- const tokensContainer = within(taskContainer).getByTestId("tokens-container")
- expect(within(tokensContainer).getByTestId("tokens-in")).toHaveTextContent("100")
- expect(within(tokensContainer).getByTestId("tokens-out")).toHaveTextContent("50")
+ // Find token counts within the task container (TaskItem -> TaskItemFooter)
+ expect(within(taskContainer).getByTestId("tokens-in-footer-full")).toHaveTextContent("100")
+ expect(within(taskContainer).getByTestId("tokens-out-footer-full")).toHaveTextContent("50")
})
it("displays cache information when available", () => {
@@ -285,10 +312,9 @@ describe("HistoryView", () => {
// Find second task container
const taskContainer = screen.getByTestId("virtuoso-item-2")
- // Find cache info within the task container
- const cacheContainer = within(taskContainer).getByTestId("cache-container")
- expect(within(cacheContainer).getByTestId("cache-writes")).toHaveTextContent("+50")
- expect(within(cacheContainer).getByTestId("cache-reads")).toHaveTextContent("25")
+ // Find cache info within the task container (TaskItem -> TaskItemHeader)
+ expect(within(taskContainer).getByTestId("cache-writes")).toHaveTextContent("50") // No plus sign in formatLargeNumber
+ expect(within(taskContainer).getByTestId("cache-reads")).toHaveTextContent("25")
})
it("handles export functionality", () => {
diff --git a/webview-ui/src/components/history/__tests__/TaskItem.test.tsx b/webview-ui/src/components/history/__tests__/TaskItem.test.tsx
new file mode 100644
index 0000000000..f2f368350e
--- /dev/null
+++ b/webview-ui/src/components/history/__tests__/TaskItem.test.tsx
@@ -0,0 +1,117 @@
+import { render, screen, fireEvent } from "@testing-library/react"
+import type { HistoryItem } from "@roo-code/types"
+import TaskItem from "../TaskItem"
+import { vscode } from "@src/utils/vscode"
+
+jest.mock("@src/utils/vscode")
+jest.mock("@src/i18n/TranslationContext")
+jest.mock("lucide-react", () => ({
+ DollarSign: () => $,
+ Coins: () => , // Mock for Coins icon used in TaskItemFooter compact
+}))
+jest.mock("../CopyButton", () => ({
+ CopyButton: jest.fn(() => ),
+}))
+jest.mock("../ExportButton", () => ({
+ ExportButton: jest.fn(() => ),
+}))
+
+const mockTask: HistoryItem = {
+ number: 1,
+ id: "test-task-1",
+ task: "Test task content",
+ ts: new Date("2022-02-16T00:00:00").getTime(),
+ tokensIn: 100,
+ tokensOut: 50,
+ totalCost: 0.002,
+ workspace: "test-workspace",
+}
+
+describe("TaskItem", () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ it("renders compact variant correctly", () => {
+ render()
+
+ expect(screen.getByText("Test task content")).toBeInTheDocument()
+ // Check for tokens display
+ expect(screen.getByTestId("tokens-in-footer-compact")).toHaveTextContent("100")
+ expect(screen.getByTestId("tokens-out-footer-compact")).toHaveTextContent("50")
+ expect(screen.getByTestId("cost-footer-compact")).toHaveTextContent("$0.00") // Cost
+ })
+
+ it("renders full variant correctly", () => {
+ render()
+
+ expect(screen.getByTestId("task-item-test-task-1")).toBeInTheDocument()
+ expect(screen.getByTestId("task-content")).toBeInTheDocument()
+ expect(screen.getByTestId("tokens-in-footer-full")).toHaveTextContent("100")
+ expect(screen.getByTestId("tokens-out-footer-full")).toHaveTextContent("50")
+ })
+
+ it("shows workspace when showWorkspace is true", () => {
+ render()
+
+ expect(screen.getByText("test-workspace")).toBeInTheDocument()
+ })
+
+ it("handles click events correctly", () => {
+ render()
+
+ fireEvent.click(screen.getByText("Test task content"))
+
+ expect(vscode.postMessage).toHaveBeenCalledWith({
+ type: "showTaskWithId",
+ text: "test-task-1",
+ })
+ })
+
+ it("handles selection mode correctly", () => {
+ const mockToggleSelection = jest.fn()
+ render(
+ ,
+ )
+
+ const checkbox = screen.getByRole("checkbox")
+ expect(checkbox).toBeInTheDocument()
+ expect(checkbox).not.toBeChecked()
+
+ fireEvent.click(screen.getByTestId("task-item-test-task-1"))
+
+ expect(mockToggleSelection).toHaveBeenCalledWith("test-task-1", true)
+ expect(vscode.postMessage).not.toHaveBeenCalled()
+ })
+
+ it("shows delete button in full variant when not in selection mode", () => {
+ const mockOnDelete = jest.fn()
+ render()
+
+ const deleteButton = screen.getByTestId("delete-task-button")
+ expect(deleteButton).toBeInTheDocument()
+
+ fireEvent.click(deleteButton)
+
+ expect(mockOnDelete).toHaveBeenCalledWith("test-task-1")
+ })
+
+ it("displays cache information when available", () => {
+ const taskWithCache: HistoryItem = {
+ ...mockTask,
+ cacheWrites: 25,
+ cacheReads: 10,
+ }
+
+ render()
+
+ expect(screen.getByTestId("cache-writes")).toHaveTextContent("25")
+ expect(screen.getByTestId("cache-reads")).toHaveTextContent("10")
+ })
+})