diff --git a/webview-ui/src/components/history/CopyButton.tsx b/webview-ui/src/components/history/CopyButton.tsx
index 1db3ea01f5..743b150aae 100644
--- a/webview-ui/src/components/history/CopyButton.tsx
+++ b/webview-ui/src/components/history/CopyButton.tsx
@@ -2,15 +2,14 @@ import { useCallback } from "react"
import { useClipboard } from "@/components/ui/hooks"
import { Button } from "@/components/ui"
-import { cn } from "@/lib/utils"
import { useAppTranslation } from "@/i18n/TranslationContext"
+import { cn } from "@/lib/utils"
type CopyButtonProps = {
itemTask: string
- className?: string
}
-export const CopyButton = ({ itemTask, className }: CopyButtonProps) => {
+export const CopyButton = ({ itemTask }: CopyButtonProps) => {
const { isCopied, copy } = useClipboard()
const { t } = useAppTranslation()
@@ -31,8 +30,8 @@ export const CopyButton = ({ itemTask, className }: CopyButtonProps) => {
size="icon"
title={t("history:copyPrompt")}
onClick={onCopy}
- data-testid="copy-prompt-button"
- className={cn("opacity-50 hover:opacity-100", className)}>
+ className="group-hover:opacity-100 opacity-50 transition-opacity"
+ data-testid="copy-prompt-button">
)
diff --git a/webview-ui/src/components/history/DeleteButton.tsx b/webview-ui/src/components/history/DeleteButton.tsx
new file mode 100644
index 0000000000..b91f13bd50
--- /dev/null
+++ b/webview-ui/src/components/history/DeleteButton.tsx
@@ -0,0 +1,38 @@
+import { useCallback } from "react"
+
+import { Button } from "@/components/ui"
+import { useAppTranslation } from "@/i18n/TranslationContext"
+import { vscode } from "@/utils/vscode"
+
+type DeleteButtonProps = {
+ itemId: string
+ onDelete?: (taskId: string) => void
+}
+
+export const DeleteButton = ({ itemId, onDelete }: DeleteButtonProps) => {
+ const { t } = useAppTranslation()
+
+ const handleDeleteClick = useCallback(
+ (e: React.MouseEvent) => {
+ e.stopPropagation()
+ if (e.shiftKey) {
+ vscode.postMessage({ type: "deleteTaskWithId", text: itemId })
+ } else if (onDelete) {
+ onDelete(itemId)
+ }
+ },
+ [itemId, onDelete],
+ )
+
+ return (
+
+ )
+}
diff --git a/webview-ui/src/components/history/ExportButton.tsx b/webview-ui/src/components/history/ExportButton.tsx
index 2089c3dcfb..eeba0ccaf4 100644
--- a/webview-ui/src/components/history/ExportButton.tsx
+++ b/webview-ui/src/components/history/ExportButton.tsx
@@ -1,21 +1,28 @@
import { vscode } from "@/utils/vscode"
import { Button } from "@/components/ui"
import { useAppTranslation } from "@/i18n/TranslationContext"
+import { useCallback } from "react"
export const ExportButton = ({ itemId }: { itemId: string }) => {
const { t } = useAppTranslation()
+ const handleExportClick = useCallback(
+ (e: React.MouseEvent) => {
+ e.stopPropagation()
+ vscode.postMessage({ type: "exportTaskWithId", text: itemId })
+ },
+ [itemId],
+ )
+
return (
)
}
diff --git a/webview-ui/src/components/history/HistoryView.tsx b/webview-ui/src/components/history/HistoryView.tsx
index ab771e492f..2d6ee5fa3d 100644
--- a/webview-ui/src/components/history/HistoryView.tsx
+++ b/webview-ui/src/components/history/HistoryView.tsx
@@ -3,10 +3,9 @@ import { DeleteTaskDialog } from "./DeleteTaskDialog"
import { BatchDeleteTaskDialog } from "./BatchDeleteTaskDialog"
import { Virtuoso } from "react-virtuoso"
-import { VSCodeTextField, VSCodeRadioGroup, VSCodeRadio } from "@vscode/webview-ui-toolkit/react"
+import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
-import { cn } from "@/lib/utils"
-import { Button, Checkbox } from "@/components/ui"
+import { Button, Checkbox, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { Tab, TabContent, TabHeader } from "../common/Tab"
@@ -95,7 +94,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
{
setSortOption("mostRelevant")
}
}}>
-
+
{searchQuery && (
setSearchQuery("")}
slot="end"
- style={{
- display: "flex",
- justifyContent: "center",
- alignItems: "center",
- height: "100%",
- }}
/>
)}
-
setSortOption((e.target as HTMLInputElement).value as SortOption)}>
-
- {t("history:newest")}
-
-
- {t("history:oldest")}
-
-
- {t("history:mostExpensive")}
-
-
- {t("history:mostTokens")}
-
-
- {t("history:mostRelevant")}
-
-
-
-
-
setShowAllWorkspaces(checked === true)}
- variant="description"
- />
-
+
+
+
{/* Select all control in selection mode */}
@@ -193,10 +213,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
{
)),
}}
- itemContent={(index, item) => (
+ itemContent={(_index, item) => (
{
isSelected={selectedTaskIds.includes(item.id)}
onToggleSelection={toggleTaskSelection}
onDelete={setDeleteTaskId}
- className={cn({
- "border-b border-vscode-panel-border": index < tasks.length - 1,
- })}
+ className="m-2 mr-0"
/>
)}
/>
diff --git a/webview-ui/src/components/history/TaskItem.tsx b/webview-ui/src/components/history/TaskItem.tsx
index 29b0775b3e..5ebe9f9831 100644
--- a/webview-ui/src/components/history/TaskItem.tsx
+++ b/webview-ui/src/components/history/TaskItem.tsx
@@ -4,7 +4,6 @@ import type { HistoryItem } from "@roo-code/types"
import { vscode } from "@/utils/vscode"
import { cn } from "@/lib/utils"
import { Checkbox } from "@/components/ui/checkbox"
-import { useAppTranslation } from "@/i18n/TranslationContext"
import TaskItemHeader from "./TaskItemHeader"
import TaskItemFooter from "./TaskItemFooter"
@@ -34,8 +33,6 @@ const TaskItem = ({
onDelete,
className,
}: TaskItemProps) => {
- const { t } = useAppTranslation()
-
const handleClick = () => {
if (isSelectionMode && onToggleSelection) {
onToggleSelection(item.id, !isSelected)
@@ -49,24 +46,13 @@ const TaskItem = ({
return (
-
+
{/* Selection checkbox - only in full variant */}
{!isCompact && isSelectionMode && (
{/* Header with metadata */}
-
+
{/* Task content */}
{item.highlight ? undefined : item.task}
@@ -116,10 +88,7 @@ const TaskItem = ({
{/* Workspace info */}
{showWorkspace && item.workspace && (
-
+
{item.workspace}
diff --git a/webview-ui/src/components/history/TaskItemFooter.tsx b/webview-ui/src/components/history/TaskItemFooter.tsx
index b3e6e56371..424cf1eadb 100644
--- a/webview-ui/src/components/history/TaskItemFooter.tsx
+++ b/webview-ui/src/components/history/TaskItemFooter.tsx
@@ -1,9 +1,8 @@
import React from "react"
import type { HistoryItem } from "@roo-code/types"
-import { Coins } from "lucide-react"
+import { Coins, FileIcon } from "lucide-react"
+import prettyBytes from "pretty-bytes"
import { formatLargeNumber } from "@/utils/format"
-import { cn } from "@/lib/utils"
-import { useAppTranslation } from "@/i18n/TranslationContext"
import { CopyButton } from "./CopyButton"
import { ExportButton } from "./ExportButton"
@@ -14,102 +13,48 @@ export interface TaskItemFooterProps {
}
const TaskItemFooter: React.FC
= ({ item, variant, isSelectionMode = false }) => {
- const { t } = useAppTranslation()
- const isCompact = variant === "compact"
+ return (
+
+
+ {!!(item.cacheReads || item.cacheWrites) && (
+
+
+ {formatLargeNumber(item.cacheWrites || 0)}
+
+ {formatLargeNumber(item.cacheReads || 0)}
+
+ )}
- const metadataIconWithTextAdjustStyle: React.CSSProperties = {
- fontSize: "12px",
- color: "var(--vscode-descriptionForeground)",
- verticalAlign: "middle",
- marginBottom: "-2px",
- fontWeight: "bold",
- }
+ {/* Full Tokens */}
+ {!!(item.tokensIn || item.tokensOut) && (
+
+ ↑ {formatLargeNumber(item.tokensIn || 0)}
+ ↓ {formatLargeNumber(item.tokensOut || 0)}
+
+ )}
- return (
-
- {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 && (
-
-
-
-
- )}
- >
+ {/* Full Cost */}
+ {!!item.totalCost && (
+
+
+ {"$" + item.totalCost.toFixed(2)}
+
+ )}
+
+ {!!item.size && (
+
+
+ {prettyBytes(item.size)}
+
+ )}
+
+
+ {/* Action Buttons for non-compact view */}
+ {!isSelectionMode && (
+
+
+ {variant === "full" && }
+
)}
)
diff --git a/webview-ui/src/components/history/TaskItemHeader.tsx b/webview-ui/src/components/history/TaskItemHeader.tsx
index 611676714d..bdddb090c8 100644
--- a/webview-ui/src/components/history/TaskItemHeader.tsx
+++ b/webview-ui/src/components/history/TaskItemHeader.tsx
@@ -1,40 +1,23 @@
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"
+import { DeleteButton } from "./DeleteButton"
+import { cn } from "@/lib/utils"
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)
- }
- }
-
+const TaskItemHeader: React.FC = ({ item, isSelectionMode, onDelete }) => {
return (
-
+
{formatDate(item.ts)}
@@ -44,27 +27,7 @@ const TaskItemHeader: React.FC = ({ item, variant, isSelect
{/* Action Buttons */}
{!isSelectionMode && (
- {isCompact ? (
-
- ) : (
- <>
- {onDelete && (
-
- )}
- {!isCompact && item.size && (
-
- {prettyBytes(item.size)}
-
- )}
- >
- )}
+ {onDelete && }
)}
diff --git a/webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.test.tsx b/webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.test.tsx
new file mode 100644
index 0000000000..6b13c92b06
--- /dev/null
+++ b/webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.test.tsx
@@ -0,0 +1,87 @@
+import { render, screen, fireEvent } from "@testing-library/react"
+import { BatchDeleteTaskDialog } from "../BatchDeleteTaskDialog"
+import { vscode } from "@/utils/vscode"
+
+jest.mock("@/utils/vscode")
+jest.mock("@/i18n/TranslationContext", () => ({
+ useAppTranslation: () => ({
+ t: (key: string, options?: Record
) => {
+ const translations: Record = {
+ "history:deleteTasks": "Delete Tasks",
+ "history:confirmDeleteTasks": `Are you sure you want to delete ${options?.count || 0} tasks?`,
+ "history:deleteTasksWarning": "This action cannot be undone.",
+ "history:cancel": "Cancel",
+ "history:deleteItems": `Delete ${options?.count || 0} items`,
+ }
+ return translations[key] || key
+ },
+ }),
+}))
+
+describe("BatchDeleteTaskDialog", () => {
+ const mockTaskIds = ["task-1", "task-2", "task-3"]
+ const mockOnOpenChange = jest.fn()
+
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ it("renders dialog with correct content", () => {
+ render()
+
+ expect(screen.getByText("Delete Tasks")).toBeInTheDocument()
+ expect(screen.getByText("Are you sure you want to delete 3 tasks?")).toBeInTheDocument()
+ expect(screen.getByText("This action cannot be undone.")).toBeInTheDocument()
+ expect(screen.getByText("Cancel")).toBeInTheDocument()
+ expect(screen.getByText("Delete 3 items")).toBeInTheDocument()
+ })
+
+ it("calls vscode.postMessage when delete is confirmed", () => {
+ render()
+
+ const deleteButton = screen.getByText("Delete 3 items")
+ fireEvent.click(deleteButton)
+
+ expect(vscode.postMessage).toHaveBeenCalledWith({
+ type: "deleteMultipleTasksWithIds",
+ ids: mockTaskIds,
+ })
+ expect(mockOnOpenChange).toHaveBeenCalledWith(false)
+ })
+
+ it("calls onOpenChange when cancel is clicked", () => {
+ render()
+
+ const cancelButton = screen.getByText("Cancel")
+ fireEvent.click(cancelButton)
+
+ expect(vscode.postMessage).not.toHaveBeenCalled()
+ expect(mockOnOpenChange).toHaveBeenCalledWith(false)
+ })
+
+ it("does not call vscode.postMessage when taskIds is empty", () => {
+ render()
+
+ const deleteButton = screen.getByText("Delete 0 items")
+ fireEvent.click(deleteButton)
+
+ expect(vscode.postMessage).not.toHaveBeenCalled()
+ expect(mockOnOpenChange).toHaveBeenCalledWith(false)
+ })
+
+ it("renders with correct task count in messages", () => {
+ const singleTaskId = ["task-1"]
+ render()
+
+ expect(screen.getByText("Are you sure you want to delete 1 tasks?")).toBeInTheDocument()
+ expect(screen.getByText("Delete 1 items")).toBeInTheDocument()
+ })
+
+ it("renders trash icon in delete button", () => {
+ render()
+
+ const deleteButton = screen.getByText("Delete 3 items")
+ const trashIcon = deleteButton.querySelector(".codicon-trash")
+ expect(trashIcon).toBeInTheDocument()
+ })
+})
diff --git a/webview-ui/src/components/history/__tests__/CopyButton.test.tsx b/webview-ui/src/components/history/__tests__/CopyButton.test.tsx
new file mode 100644
index 0000000000..f1de9bf8ea
--- /dev/null
+++ b/webview-ui/src/components/history/__tests__/CopyButton.test.tsx
@@ -0,0 +1,31 @@
+import { render, screen, fireEvent } from "@testing-library/react"
+import { CopyButton } from "../CopyButton"
+import { useClipboard } from "@/components/ui/hooks"
+
+jest.mock("@/components/ui/hooks")
+jest.mock("@src/i18n/TranslationContext", () => ({
+ useAppTranslation: () => ({
+ t: (key: string) => key,
+ }),
+}))
+
+describe("CopyButton", () => {
+ const mockCopy = jest.fn()
+
+ beforeEach(() => {
+ jest.clearAllMocks()
+ ;(useClipboard as jest.Mock).mockReturnValue({
+ isCopied: false,
+ copy: mockCopy,
+ })
+ })
+
+ it("copies task content when clicked", () => {
+ render()
+
+ const copyButton = screen.getByRole("button")
+ fireEvent.click(copyButton)
+
+ expect(mockCopy).toHaveBeenCalledWith("Test task content")
+ })
+})
diff --git a/webview-ui/src/components/history/__tests__/DeleteButton.test.tsx b/webview-ui/src/components/history/__tests__/DeleteButton.test.tsx
new file mode 100644
index 0000000000..d6c10ad6ca
--- /dev/null
+++ b/webview-ui/src/components/history/__tests__/DeleteButton.test.tsx
@@ -0,0 +1,20 @@
+import { render, screen, fireEvent } from "@testing-library/react"
+import { DeleteButton } from "../DeleteButton"
+
+jest.mock("@src/i18n/TranslationContext", () => ({
+ useAppTranslation: () => ({
+ t: (key: string) => key,
+ }),
+}))
+
+describe("DeleteButton", () => {
+ it("calls onDelete when clicked", () => {
+ const onDelete = jest.fn()
+ render()
+
+ const deleteButton = screen.getByRole("button")
+ fireEvent.click(deleteButton)
+
+ expect(onDelete).toHaveBeenCalledWith("test-id")
+ })
+})
diff --git a/webview-ui/src/components/history/__tests__/DeleteTaskDialog.test.tsx b/webview-ui/src/components/history/__tests__/DeleteTaskDialog.test.tsx
new file mode 100644
index 0000000000..ceecb42063
--- /dev/null
+++ b/webview-ui/src/components/history/__tests__/DeleteTaskDialog.test.tsx
@@ -0,0 +1,143 @@
+import { render, screen, fireEvent } from "@testing-library/react"
+import { DeleteTaskDialog } from "../DeleteTaskDialog"
+import { vscode } from "@/utils/vscode"
+
+jest.mock("@/utils/vscode")
+jest.mock("@/i18n/TranslationContext", () => ({
+ useAppTranslation: () => ({
+ t: (key: string) => {
+ const translations: Record = {
+ "history:deleteTask": "Delete Task",
+ "history:deleteTaskMessage": "Are you sure you want to delete this task? This action cannot be undone.",
+ "history:cancel": "Cancel",
+ "history:delete": "Delete",
+ }
+ return translations[key] || key
+ },
+ }),
+}))
+
+jest.mock("react-use", () => ({
+ useKeyPress: jest.fn(),
+}))
+
+import { useKeyPress } from "react-use"
+
+const mockUseKeyPress = useKeyPress as jest.MockedFunction
+
+describe("DeleteTaskDialog", () => {
+ const mockTaskId = "test-task-id"
+ const mockOnOpenChange = jest.fn()
+
+ beforeEach(() => {
+ jest.clearAllMocks()
+ mockUseKeyPress.mockReturnValue([false, null])
+ })
+
+ it("renders dialog with correct content", () => {
+ render()
+
+ expect(screen.getByText("Delete Task")).toBeInTheDocument()
+ expect(
+ screen.getByText("Are you sure you want to delete this task? This action cannot be undone."),
+ ).toBeInTheDocument()
+ expect(screen.getByText("Cancel")).toBeInTheDocument()
+ expect(screen.getByText("Delete")).toBeInTheDocument()
+ })
+
+ it("calls vscode.postMessage when delete is confirmed", () => {
+ render()
+
+ const deleteButton = screen.getByText("Delete")
+ fireEvent.click(deleteButton)
+
+ expect(vscode.postMessage).toHaveBeenCalledWith({
+ type: "deleteTaskWithId",
+ text: mockTaskId,
+ })
+ expect(mockOnOpenChange).toHaveBeenCalledWith(false)
+ })
+
+ it("calls onOpenChange when cancel is clicked", () => {
+ render()
+
+ const cancelButton = screen.getByText("Cancel")
+ fireEvent.click(cancelButton)
+
+ expect(vscode.postMessage).not.toHaveBeenCalled()
+ expect(mockOnOpenChange).toHaveBeenCalledWith(false)
+ })
+
+ it("does not call vscode.postMessage when taskId is empty", () => {
+ render()
+
+ const deleteButton = screen.getByText("Delete")
+ fireEvent.click(deleteButton)
+
+ expect(vscode.postMessage).not.toHaveBeenCalled()
+ expect(mockOnOpenChange).toHaveBeenCalledWith(false)
+ })
+
+ it("handles Enter key press to delete task", () => {
+ // Mock Enter key being pressed
+ mockUseKeyPress.mockReturnValue([true, null])
+
+ render()
+
+ expect(vscode.postMessage).toHaveBeenCalledWith({
+ type: "deleteTaskWithId",
+ text: mockTaskId,
+ })
+ expect(mockOnOpenChange).toHaveBeenCalledWith(false)
+ })
+
+ it("does not delete on Enter key press when taskId is empty", () => {
+ // Mock Enter key being pressed
+ mockUseKeyPress.mockReturnValue([true, null])
+
+ render()
+
+ expect(vscode.postMessage).not.toHaveBeenCalled()
+ expect(mockOnOpenChange).not.toHaveBeenCalled()
+ })
+
+ it("calls onOpenChange on escape key", () => {
+ render()
+
+ // Simulate escape key press on the dialog content
+ const dialogContent = screen.getByRole("alertdialog")
+ fireEvent.keyDown(dialogContent, { key: "Escape" })
+
+ expect(mockOnOpenChange).toHaveBeenCalledWith(false)
+ })
+
+ it("has correct button variants", () => {
+ render()
+
+ const cancelButton = screen.getByText("Cancel")
+ const deleteButton = screen.getByText("Delete")
+
+ // These should have the correct styling classes based on the component
+ expect(cancelButton).toBeInTheDocument()
+ expect(deleteButton).toBeInTheDocument()
+ })
+
+ it("handles multiple Enter key presses correctly", () => {
+ // First render with Enter not pressed
+ const { rerender } = render(
+ ,
+ )
+
+ expect(vscode.postMessage).not.toHaveBeenCalled()
+
+ // Then simulate Enter key press
+ mockUseKeyPress.mockReturnValue([true, null])
+ rerender()
+
+ expect(vscode.postMessage).toHaveBeenCalledTimes(1)
+ expect(vscode.postMessage).toHaveBeenCalledWith({
+ type: "deleteTaskWithId",
+ text: mockTaskId,
+ })
+ })
+})
diff --git a/webview-ui/src/components/history/__tests__/ExportButton.test.tsx b/webview-ui/src/components/history/__tests__/ExportButton.test.tsx
new file mode 100644
index 0000000000..a2d68e5682
--- /dev/null
+++ b/webview-ui/src/components/history/__tests__/ExportButton.test.tsx
@@ -0,0 +1,28 @@
+import { render, screen, fireEvent } from "@testing-library/react"
+import { ExportButton } from "../ExportButton"
+import { vscode } from "@src/utils/vscode"
+
+jest.mock("@src/utils/vscode")
+jest.mock("@src/i18n/TranslationContext", () => ({
+ useAppTranslation: () => ({
+ t: (key: string) => key,
+ }),
+}))
+
+describe("ExportButton", () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ it("sends export message when clicked", () => {
+ render()
+
+ const exportButton = screen.getByRole("button")
+ fireEvent.click(exportButton)
+
+ expect(vscode.postMessage).toHaveBeenCalledWith({
+ type: "exportTaskWithId",
+ text: "1",
+ })
+ })
+})
diff --git a/webview-ui/src/components/history/__tests__/HistoryPreview.test.tsx b/webview-ui/src/components/history/__tests__/HistoryPreview.test.tsx
new file mode 100644
index 0000000000..c4c7fb3e95
--- /dev/null
+++ b/webview-ui/src/components/history/__tests__/HistoryPreview.test.tsx
@@ -0,0 +1,199 @@
+import { render, screen } from "@testing-library/react"
+import HistoryPreview from "../HistoryPreview"
+import type { HistoryItem } from "@roo-code/types"
+
+jest.mock("../useTaskSearch")
+jest.mock("../TaskItem", () => {
+ return {
+ __esModule: true,
+ default: jest.fn(({ item, variant }) => (
+
+ {item.task}
+
+ )),
+ }
+})
+
+import { useTaskSearch } from "../useTaskSearch"
+import TaskItem from "../TaskItem"
+
+const mockUseTaskSearch = useTaskSearch as jest.MockedFunction
+const mockTaskItem = TaskItem as jest.MockedFunction
+
+const mockTasks: HistoryItem[] = [
+ {
+ id: "task-1",
+ number: 1,
+ task: "First task",
+ ts: Date.now(),
+ tokensIn: 100,
+ tokensOut: 50,
+ totalCost: 0.01,
+ },
+ {
+ id: "task-2",
+ number: 2,
+ task: "Second task",
+ ts: Date.now(),
+ tokensIn: 200,
+ tokensOut: 100,
+ totalCost: 0.02,
+ },
+ {
+ id: "task-3",
+ number: 3,
+ task: "Third task",
+ ts: Date.now(),
+ tokensIn: 150,
+ tokensOut: 75,
+ totalCost: 0.015,
+ },
+ {
+ id: "task-4",
+ number: 4,
+ task: "Fourth task",
+ ts: Date.now(),
+ tokensIn: 300,
+ tokensOut: 150,
+ totalCost: 0.03,
+ },
+]
+
+describe("HistoryPreview", () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ it("renders nothing when no tasks are available", () => {
+ mockUseTaskSearch.mockReturnValue({
+ tasks: [],
+ searchQuery: "",
+ setSearchQuery: jest.fn(),
+ sortOption: "newest",
+ setSortOption: jest.fn(),
+ lastNonRelevantSort: null,
+ setLastNonRelevantSort: jest.fn(),
+ showAllWorkspaces: false,
+ setShowAllWorkspaces: jest.fn(),
+ })
+
+ const { container } = render()
+
+ // Should render the container but no task items
+ expect(container.firstChild).toHaveClass("flex", "flex-col", "gap-3")
+ expect(screen.queryByTestId(/task-item-/)).not.toBeInTheDocument()
+ })
+
+ it("renders up to 3 tasks when tasks are available", () => {
+ mockUseTaskSearch.mockReturnValue({
+ tasks: mockTasks,
+ searchQuery: "",
+ setSearchQuery: jest.fn(),
+ sortOption: "newest",
+ setSortOption: jest.fn(),
+ lastNonRelevantSort: null,
+ setLastNonRelevantSort: jest.fn(),
+ showAllWorkspaces: false,
+ setShowAllWorkspaces: jest.fn(),
+ })
+
+ render()
+
+ // Should render only the first 3 tasks
+ expect(screen.getByTestId("task-item-task-1")).toBeInTheDocument()
+ expect(screen.getByTestId("task-item-task-2")).toBeInTheDocument()
+ expect(screen.getByTestId("task-item-task-3")).toBeInTheDocument()
+ expect(screen.queryByTestId("task-item-task-4")).not.toBeInTheDocument()
+ })
+
+ it("renders all tasks when there are 3 or fewer", () => {
+ const threeTasks = mockTasks.slice(0, 3)
+ mockUseTaskSearch.mockReturnValue({
+ tasks: threeTasks,
+ searchQuery: "",
+ setSearchQuery: jest.fn(),
+ sortOption: "newest",
+ setSortOption: jest.fn(),
+ lastNonRelevantSort: null,
+ setLastNonRelevantSort: jest.fn(),
+ showAllWorkspaces: false,
+ setShowAllWorkspaces: jest.fn(),
+ })
+
+ render()
+
+ expect(screen.getByTestId("task-item-task-1")).toBeInTheDocument()
+ expect(screen.getByTestId("task-item-task-2")).toBeInTheDocument()
+ expect(screen.getByTestId("task-item-task-3")).toBeInTheDocument()
+ })
+
+ it("renders only 1 task when there is only 1 task", () => {
+ const oneTask = mockTasks.slice(0, 1)
+ mockUseTaskSearch.mockReturnValue({
+ tasks: oneTask,
+ searchQuery: "",
+ setSearchQuery: jest.fn(),
+ sortOption: "newest",
+ setSortOption: jest.fn(),
+ lastNonRelevantSort: null,
+ setLastNonRelevantSort: jest.fn(),
+ showAllWorkspaces: false,
+ setShowAllWorkspaces: jest.fn(),
+ })
+
+ render()
+
+ expect(screen.getByTestId("task-item-task-1")).toBeInTheDocument()
+ expect(screen.queryByTestId("task-item-task-2")).not.toBeInTheDocument()
+ })
+
+ it("passes correct props to TaskItem components", () => {
+ mockUseTaskSearch.mockReturnValue({
+ tasks: mockTasks.slice(0, 2),
+ searchQuery: "",
+ setSearchQuery: jest.fn(),
+ sortOption: "newest",
+ setSortOption: jest.fn(),
+ lastNonRelevantSort: null,
+ setLastNonRelevantSort: jest.fn(),
+ showAllWorkspaces: false,
+ setShowAllWorkspaces: jest.fn(),
+ })
+
+ render()
+
+ // Verify TaskItem was called with correct props
+ expect(mockTaskItem).toHaveBeenCalledWith(
+ expect.objectContaining({
+ item: mockTasks[0],
+ variant: "compact",
+ }),
+ expect.anything(),
+ )
+ expect(mockTaskItem).toHaveBeenCalledWith(
+ expect.objectContaining({
+ item: mockTasks[1],
+ variant: "compact",
+ }),
+ expect.anything(),
+ )
+ })
+
+ it("renders with correct container classes", () => {
+ mockUseTaskSearch.mockReturnValue({
+ tasks: mockTasks.slice(0, 1),
+ searchQuery: "",
+ setSearchQuery: jest.fn(),
+ sortOption: "newest",
+ setSortOption: jest.fn(),
+ lastNonRelevantSort: null,
+ setLastNonRelevantSort: jest.fn(),
+ showAllWorkspaces: false,
+ setShowAllWorkspaces: jest.fn(),
+ })
+
+ const { container } = render()
+
+ expect(container.firstChild).toHaveClass("flex", "flex-col", "gap-3")
+ })
+})
diff --git a/webview-ui/src/components/history/__tests__/HistoryView.test.tsx b/webview-ui/src/components/history/__tests__/HistoryView.test.tsx
index 9057b23067..1c63abc837 100644
--- a/webview-ui/src/components/history/__tests__/HistoryView.test.tsx
+++ b/webview-ui/src/components/history/__tests__/HistoryView.test.tsx
@@ -1,337 +1,62 @@
-// cd webview-ui && npx jest src/components/history/__tests__/HistoryView.test.ts
-
-import { render, screen, fireEvent, within, act } from "@testing-library/react"
+import { render, screen, fireEvent } from "@testing-library/react"
import HistoryView from "../HistoryView"
import { useExtensionState } from "@src/context/ExtensionStateContext"
-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) => (
-
- {data.map((item: any, index: number) => (
-
- {itemContent(index, item)}
-
- ))}
-
- ),
+jest.mock("@src/i18n/TranslationContext", () => ({
+ useAppTranslation: () => ({
+ t: (key: string) => key,
+ }),
}))
const mockTaskHistory = [
{
id: "1",
- number: 0,
task: "Test task 1",
- ts: new Date("2022-02-16T00:00:00").getTime(),
+ ts: Date.now(),
tokensIn: 100,
tokensOut: 50,
totalCost: 0.002,
+ workspace: "/test/workspace",
},
{
id: "2",
- number: 0,
task: "Test task 2",
- ts: new Date("2022-02-17T00:00:00").getTime(),
+ ts: Date.now() + 1000,
tokensIn: 200,
tokensOut: 100,
- cacheWrites: 50,
- cacheReads: 25,
+ totalCost: 0.003,
+ workspace: "/test/workspace",
},
]
describe("HistoryView", () => {
- beforeAll(() => {
- jest.useFakeTimers()
- })
-
- afterAll(() => {
- jest.useRealTimers()
- })
-
beforeEach(() => {
jest.clearAllMocks()
;(useExtensionState as jest.Mock).mockReturnValue({
taskHistory: mockTaskHistory,
+ cwd: "/test/workspace",
})
})
- it("renders history items correctly", () => {
- const onDone = jest.fn()
- render()
-
- // Check if both tasks are rendered
- expect(screen.getByTestId("virtuoso-item-1")).toBeInTheDocument()
- expect(screen.getByTestId("virtuoso-item-2")).toBeInTheDocument()
- expect(screen.getByText("Test task 1")).toBeInTheDocument()
- expect(screen.getByText("Test task 2")).toBeInTheDocument()
- })
-
- it("handles search functionality", () => {
- // Setup clipboard mock that resolves immediately
- const mockClipboard = {
- writeText: jest.fn().mockResolvedValue(undefined),
- }
- Object.assign(navigator, { clipboard: mockClipboard })
-
- const onDone = jest.fn()
- render()
-
- // Get search input and radio group
- const searchInput = screen.getByTestId("history-search-input")
- const radioGroup = screen.getByRole("radiogroup")
-
- // Type in search
- fireEvent.input(searchInput, { target: { value: "task 1" } })
-
- // Advance timers to process search state update
- jest.advanceTimersByTime(100)
-
- // Check if sort option automatically changes to "Most Relevant"
- const mostRelevantRadio = within(radioGroup).getByTestId("radio-most-relevant")
- expect(mostRelevantRadio).not.toBeDisabled()
-
- // Click the radio button
- fireEvent.click(mostRelevantRadio)
-
- // Advance timers to process radio button state update
- jest.advanceTimersByTime(100)
-
- // Verify radio button is checked
- const updatedRadio = within(radioGroup).getByTestId("radio-most-relevant")
- expect(updatedRadio).toBeInTheDocument()
-
- // Verify copy the plain text content of the task when the copy button is clicked
- const taskContainer = screen.getByTestId("virtuoso-item-1")
- fireEvent.mouseEnter(taskContainer)
- const copyButton = within(taskContainer).getByTestId("copy-prompt-button")
- fireEvent.click(copyButton)
- const taskContent = within(taskContainer).getByTestId("task-content")
- expect(navigator.clipboard.writeText).toHaveBeenCalledWith(taskContent.textContent)
- })
-
- it("handles sort options correctly", async () => {
+ it("renders the history interface", () => {
const onDone = jest.fn()
render()
- const radioGroup = screen.getByRole("radiogroup")
-
- // Test changing sort options
- const oldestRadio = within(radioGroup).getByTestId("radio-oldest")
- fireEvent.click(oldestRadio)
-
- // Wait for oldest radio to be checked
- const checkedOldestRadio = within(radioGroup).getByTestId("radio-oldest")
- expect(checkedOldestRadio).toBeInTheDocument()
-
- const mostExpensiveRadio = within(radioGroup).getByTestId("radio-most-expensive")
- fireEvent.click(mostExpensiveRadio)
-
- // Wait for most expensive radio to be checked
- const checkedExpensiveRadio = within(radioGroup).getByTestId("radio-most-expensive")
- expect(checkedExpensiveRadio).toBeInTheDocument()
+ // Check for main UI elements
+ expect(screen.getByText("history:history")).toBeInTheDocument()
+ expect(screen.getByText("history:done")).toBeInTheDocument()
+ expect(screen.getByPlaceholderText("history:searchPlaceholder")).toBeInTheDocument()
})
- it("handles task selection", () => {
+ it("calls onDone when done button is clicked", () => {
const onDone = jest.fn()
render()
- // Click on first task
- fireEvent.click(screen.getByText("Test task 1"))
-
- // Verify vscode message was sent
- expect(vscode.postMessage).toHaveBeenCalledWith({
- type: "showTaskWithId",
- text: "1",
- })
- })
-
- it("handles selection mode clicks", async () => {
- const onDone = jest.fn()
- render()
+ const doneButton = screen.getByText("history:done")
+ fireEvent.click(doneButton)
- // Go to selection mode
- fireEvent.click(screen.getByTestId("toggle-selection-mode-button"))
-
- const taskContainer = screen.getByTestId("task-item-1")
-
- // Click anywhere in the task item
- fireEvent.click(taskContainer)
-
- // Check the box instead of sending a message to open the task
- expect(within(taskContainer).getByRole("checkbox")).toBeChecked()
- expect(vscode.postMessage).not.toHaveBeenCalled()
- })
-
- describe("task deletion", () => {
- it("shows confirmation dialog on regular click", () => {
- const onDone = jest.fn()
- render()
-
- // Find and hover over first task
- const taskContainer = screen.getByTestId("virtuoso-item-1")
- fireEvent.mouseEnter(taskContainer)
-
- // Click delete button to open confirmation dialog
- const deleteButton = within(taskContainer).getByTestId("delete-task-button")
- fireEvent.click(deleteButton)
-
- // Verify dialog is shown
- const dialog = screen.getByRole("alertdialog")
- expect(dialog).toBeInTheDocument()
-
- // Find and click the confirm delete button in the dialog
- const confirmDeleteButton = within(dialog).getByRole("button", { name: /delete/i })
- fireEvent.click(confirmDeleteButton)
-
- // Verify vscode message was sent
- expect(vscode.postMessage).toHaveBeenCalledWith({
- type: "deleteTaskWithId",
- text: "1",
- })
- })
-
- it("deletes immediately on shift-click without confirmation", () => {
- const onDone = jest.fn()
- render()
-
- // Find and hover over first task
- const taskContainer = screen.getByTestId("virtuoso-item-1")
- fireEvent.mouseEnter(taskContainer)
-
- // Shift-click delete button
- const deleteButton = within(taskContainer).getByTestId("delete-task-button")
- fireEvent.click(deleteButton, { shiftKey: true })
-
- // Verify no dialog is shown
- expect(screen.queryByRole("alertdialog")).not.toBeInTheDocument()
-
- // Verify vscode message was sent
- expect(vscode.postMessage).toHaveBeenCalledWith({
- type: "deleteTaskWithId",
- text: "1",
- })
- })
- })
-
- it("handles task copying", async () => {
- // Setup clipboard mock that resolves immediately
- const mockClipboard = {
- writeText: jest.fn().mockResolvedValue(undefined),
- }
- Object.assign(navigator, { clipboard: mockClipboard })
-
- const onDone = jest.fn()
- render()
-
- // Find and hover over first task
- const taskContainer = screen.getByTestId("virtuoso-item-1")
- fireEvent.mouseEnter(taskContainer)
-
- const copyButton = within(taskContainer).getByTestId("copy-prompt-button")
-
- // Click the copy button and wait for clipboard operation
- await act(async () => {
- fireEvent.click(copyButton)
- // Let the clipboard Promise resolve
- await Promise.resolve()
- // Let React process the first state update
- await Promise.resolve()
- })
-
- // Verify clipboard was called
- expect(navigator.clipboard.writeText).toHaveBeenCalledWith("Test task 1")
-
- // Advance timer to trigger the setTimeout for modal disappearance
- act(() => {
- jest.advanceTimersByTime(2000)
- })
-
- // Verify modal is gone
- expect(screen.queryByText("Prompt Copied to Clipboard")).not.toBeInTheDocument()
- })
-
- it("formats dates correctly", () => {
- const onDone = jest.fn()
- render()
-
- // Find first task container and check date format
- const taskContainer = screen.getByTestId("virtuoso-item-1")
- // 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()
- })
-
- it("displays token counts correctly", () => {
- const onDone = jest.fn()
- render()
-
- // Find first task container
- const taskContainer = screen.getByTestId("virtuoso-item-1")
-
- // 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", () => {
- const onDone = jest.fn()
- render()
-
- // Find second task container
- const taskContainer = screen.getByTestId("virtuoso-item-2")
-
- // 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", () => {
- const onDone = jest.fn()
- render()
-
- // Find and hover over second task
- const taskContainer = screen.getByTestId("virtuoso-item-2")
- fireEvent.mouseEnter(taskContainer)
-
- const exportButton = within(taskContainer).getByTestId("export")
- fireEvent.click(exportButton)
-
- // Verify vscode message was sent
- expect(vscode.postMessage).toHaveBeenCalledWith({
- type: "exportTaskWithId",
- text: "2",
- })
+ expect(onDone).toHaveBeenCalled()
})
})
diff --git a/webview-ui/src/components/history/__tests__/TaskItem.test.tsx b/webview-ui/src/components/history/__tests__/TaskItem.test.tsx
index f2f368350e..c57eec8567 100644
--- a/webview-ui/src/components/history/__tests__/TaskItem.test.tsx
+++ b/webview-ui/src/components/history/__tests__/TaskItem.test.tsx
@@ -1,30 +1,22 @@
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(() => ),
+jest.mock("@src/i18n/TranslationContext", () => ({
+ useAppTranslation: () => ({
+ t: (key: string) => key,
+ }),
}))
-const mockTask: HistoryItem = {
+const mockTask = {
+ id: "1",
number: 1,
- id: "test-task-1",
- task: "Test task content",
- ts: new Date("2022-02-16T00:00:00").getTime(),
+ task: "Test task",
+ ts: Date.now(),
tokensIn: 100,
tokensOut: 50,
totalCost: 0.002,
- workspace: "test-workspace",
+ workspace: "/test/workspace",
}
describe("TaskItem", () => {
@@ -32,86 +24,96 @@ describe("TaskItem", () => {
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()
+ it("renders task information", () => {
+ render(
+ ,
+ )
- expect(screen.getByText("test-workspace")).toBeInTheDocument()
+ expect(screen.getByText("Test task")).toBeInTheDocument()
+ expect(screen.getByText("$0.00")).toBeInTheDocument() // Component shows $0.00 for small amounts
})
- it("handles click events correctly", () => {
- render()
+ it("handles selection in selection mode", () => {
+ const onToggleSelection = jest.fn()
+ render(
+ ,
+ )
- fireEvent.click(screen.getByText("Test task content"))
+ const checkbox = screen.getByRole("checkbox")
+ fireEvent.click(checkbox)
- expect(vscode.postMessage).toHaveBeenCalledWith({
- type: "showTaskWithId",
- text: "test-task-1",
- })
+ expect(onToggleSelection).toHaveBeenCalledWith("1", true)
})
- it("handles selection mode correctly", () => {
- const mockToggleSelection = jest.fn()
+ it("shows action buttons", () => {
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()
+ // Should show copy and export buttons
+ expect(screen.getByTestId("copy-prompt-button")).toBeInTheDocument()
+ expect(screen.getByTestId("export")).toBeInTheDocument()
})
- 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()
+ it("displays cache information when present", () => {
+ const mockTaskWithCache = {
+ ...mockTask,
+ cacheReads: 10,
+ cacheWrites: 5,
+ }
- fireEvent.click(deleteButton)
+ render(
+ ,
+ )
- expect(mockOnDelete).toHaveBeenCalledWith("test-task-1")
+ // Should display cache information in the footer
+ expect(screen.getByTestId("cache-compact")).toBeInTheDocument()
+ expect(screen.getByText("5")).toBeInTheDocument() // cache writes
+ expect(screen.getByText("10")).toBeInTheDocument() // cache reads
})
- it("displays cache information when available", () => {
- const taskWithCache: HistoryItem = {
+ it("does not display cache information when not present", () => {
+ const mockTaskWithoutCache = {
...mockTask,
- cacheWrites: 25,
- cacheReads: 10,
+ cacheReads: 0,
+ cacheWrites: 0,
}
- render()
+ render(
+ ,
+ )
- expect(screen.getByTestId("cache-writes")).toHaveTextContent("25")
- expect(screen.getByTestId("cache-reads")).toHaveTextContent("10")
+ // Cache section should not be present
+ expect(screen.queryByTestId("cache-compact")).not.toBeInTheDocument()
})
})
diff --git a/webview-ui/src/components/history/__tests__/TaskItemFooter.test.tsx b/webview-ui/src/components/history/__tests__/TaskItemFooter.test.tsx
new file mode 100644
index 0000000000..f7ed5640f1
--- /dev/null
+++ b/webview-ui/src/components/history/__tests__/TaskItemFooter.test.tsx
@@ -0,0 +1,72 @@
+import { render, screen } from "@testing-library/react"
+import TaskItemFooter from "../TaskItemFooter"
+
+jest.mock("@src/i18n/TranslationContext", () => ({
+ useAppTranslation: () => ({
+ t: (key: string) => key,
+ }),
+}))
+
+const mockItem = {
+ id: "1",
+ number: 1,
+ task: "Test task",
+ ts: Date.now(),
+ tokensIn: 100,
+ tokensOut: 50,
+ totalCost: 0.002,
+ workspace: "/test/workspace",
+}
+
+describe("TaskItemFooter", () => {
+ it("renders token information", () => {
+ render()
+
+ // Check for token counts using testids since the text is split across elements
+ expect(screen.getByTestId("tokens-in-footer-compact")).toBeInTheDocument()
+ expect(screen.getByTestId("tokens-out-footer-compact")).toBeInTheDocument()
+ })
+
+ it("renders cost information", () => {
+ render()
+
+ // The component shows $0.00 for small amounts, not the exact value
+ expect(screen.getByText("$0.00")).toBeInTheDocument()
+ })
+
+ it("shows action buttons", () => {
+ render()
+
+ // Should show copy and export buttons
+ expect(screen.getByTestId("copy-prompt-button")).toBeInTheDocument()
+ expect(screen.getByTestId("export")).toBeInTheDocument()
+ })
+
+ it("renders cache information when present", () => {
+ const mockItemWithCache = {
+ ...mockItem,
+ cacheReads: 5,
+ cacheWrites: 3,
+ }
+
+ render()
+
+ // Check for cache display using testid
+ expect(screen.getByTestId("cache-compact")).toBeInTheDocument()
+ expect(screen.getByText("3")).toBeInTheDocument() // cache writes
+ expect(screen.getByText("5")).toBeInTheDocument() // cache reads
+ })
+
+ it("does not render cache information when not present", () => {
+ const mockItemWithoutCache = {
+ ...mockItem,
+ cacheReads: 0,
+ cacheWrites: 0,
+ }
+
+ render()
+
+ // Cache section should not be present
+ expect(screen.queryByTestId("cache-compact")).not.toBeInTheDocument()
+ })
+})
diff --git a/webview-ui/src/components/history/__tests__/TaskItemHeader.test.tsx b/webview-ui/src/components/history/__tests__/TaskItemHeader.test.tsx
new file mode 100644
index 0000000000..10ce7ca0f5
--- /dev/null
+++ b/webview-ui/src/components/history/__tests__/TaskItemHeader.test.tsx
@@ -0,0 +1,34 @@
+import { render, screen } from "@testing-library/react"
+import TaskItemHeader from "../TaskItemHeader"
+
+jest.mock("@src/i18n/TranslationContext", () => ({
+ useAppTranslation: () => ({
+ t: (key: string) => key,
+ }),
+}))
+
+const mockItem = {
+ id: "1",
+ number: 1,
+ task: "Test task",
+ ts: Date.now(),
+ tokensIn: 100,
+ tokensOut: 50,
+ totalCost: 0.002,
+ workspace: "/test/workspace",
+}
+
+describe("TaskItemHeader", () => {
+ it("renders date information", () => {
+ render()
+
+ // TaskItemHeader shows the formatted date, not the task text
+ expect(screen.getByText(/\w+ \d{1,2}, \d{1,2}:\d{2} \w{2}/)).toBeInTheDocument() // Date format like "JUNE 14, 10:15 AM"
+ })
+
+ it("shows delete button when not in selection mode", () => {
+ render()
+
+ expect(screen.getByRole("button")).toBeInTheDocument()
+ })
+})
diff --git a/webview-ui/src/components/history/__tests__/useTaskSearch.test.tsx b/webview-ui/src/components/history/__tests__/useTaskSearch.test.tsx
new file mode 100644
index 0000000000..077ec93f55
--- /dev/null
+++ b/webview-ui/src/components/history/__tests__/useTaskSearch.test.tsx
@@ -0,0 +1,285 @@
+import { renderHook, act } from "@testing-library/react"
+import { useTaskSearch } from "../useTaskSearch"
+import type { HistoryItem } from "@roo-code/types"
+
+jest.mock("@/context/ExtensionStateContext", () => ({
+ useExtensionState: jest.fn(),
+}))
+
+jest.mock("@/utils/highlight", () => ({
+ highlightFzfMatch: jest.fn((text) => `${text}`),
+}))
+
+import { useExtensionState } from "@/context/ExtensionStateContext"
+
+const mockUseExtensionState = useExtensionState as jest.MockedFunction
+
+const mockTaskHistory: HistoryItem[] = [
+ {
+ id: "task-1",
+ number: 1,
+ task: "Create a React component",
+ ts: new Date("2022-02-16T12:00:00").getTime(),
+ tokensIn: 100,
+ tokensOut: 50,
+ totalCost: 0.01,
+ workspace: "/workspace/project1",
+ },
+ {
+ id: "task-2",
+ number: 2,
+ task: "Write unit tests",
+ ts: new Date("2022-02-17T12:00:00").getTime(),
+ tokensIn: 200,
+ tokensOut: 100,
+ totalCost: 0.02,
+ cacheWrites: 25,
+ cacheReads: 10,
+ workspace: "/workspace/project1",
+ },
+ {
+ id: "task-3",
+ number: 3,
+ task: "Fix bug in authentication",
+ ts: new Date("2022-02-15T12:00:00").getTime(),
+ tokensIn: 150,
+ tokensOut: 75,
+ totalCost: 0.05,
+ workspace: "/workspace/project2",
+ },
+]
+
+describe("useTaskSearch", () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ mockUseExtensionState.mockReturnValue({
+ taskHistory: mockTaskHistory,
+ cwd: "/workspace/project1",
+ } as any)
+ })
+
+ it("returns all tasks by default", () => {
+ const { result } = renderHook(() => useTaskSearch())
+
+ expect(result.current.tasks).toHaveLength(2) // Only tasks from current workspace
+ expect(result.current.tasks[0].id).toBe("task-2") // Newest first
+ expect(result.current.tasks[1].id).toBe("task-1")
+ })
+
+ it("filters tasks by current workspace by default", () => {
+ const { result } = renderHook(() => useTaskSearch())
+
+ expect(result.current.tasks).toHaveLength(2)
+ expect(result.current.tasks.every((task) => task.workspace === "/workspace/project1")).toBe(true)
+ })
+
+ it("shows all workspaces when showAllWorkspaces is true", () => {
+ const { result } = renderHook(() => useTaskSearch())
+
+ act(() => {
+ result.current.setShowAllWorkspaces(true)
+ })
+
+ expect(result.current.tasks).toHaveLength(3)
+ expect(result.current.showAllWorkspaces).toBe(true)
+ })
+
+ it("sorts by newest by default", () => {
+ const { result } = renderHook(() => useTaskSearch())
+
+ act(() => {
+ result.current.setShowAllWorkspaces(true)
+ })
+
+ expect(result.current.sortOption).toBe("newest")
+ expect(result.current.tasks[0].id).toBe("task-2") // Feb 17
+ expect(result.current.tasks[1].id).toBe("task-1") // Feb 16
+ expect(result.current.tasks[2].id).toBe("task-3") // Feb 15
+ })
+
+ it("sorts by oldest", () => {
+ const { result } = renderHook(() => useTaskSearch())
+
+ act(() => {
+ result.current.setShowAllWorkspaces(true)
+ result.current.setSortOption("oldest")
+ })
+
+ expect(result.current.tasks[0].id).toBe("task-3") // Feb 15
+ expect(result.current.tasks[1].id).toBe("task-1") // Feb 16
+ expect(result.current.tasks[2].id).toBe("task-2") // Feb 17
+ })
+
+ it("sorts by most expensive", () => {
+ const { result } = renderHook(() => useTaskSearch())
+
+ act(() => {
+ result.current.setShowAllWorkspaces(true)
+ result.current.setSortOption("mostExpensive")
+ })
+
+ expect(result.current.tasks[0].id).toBe("task-3") // $0.05
+ expect(result.current.tasks[1].id).toBe("task-2") // $0.02
+ expect(result.current.tasks[2].id).toBe("task-1") // $0.01
+ })
+
+ it("sorts by most tokens", () => {
+ const { result } = renderHook(() => useTaskSearch())
+
+ act(() => {
+ result.current.setShowAllWorkspaces(true)
+ result.current.setSortOption("mostTokens")
+ })
+
+ // task-2: 200 + 100 + 25 + 10 = 335 tokens
+ // task-3: 150 + 75 = 225 tokens
+ // task-1: 100 + 50 = 150 tokens
+ expect(result.current.tasks[0].id).toBe("task-2")
+ expect(result.current.tasks[1].id).toBe("task-3")
+ expect(result.current.tasks[2].id).toBe("task-1")
+ })
+
+ it("filters tasks by search query", () => {
+ const { result } = renderHook(() => useTaskSearch())
+
+ act(() => {
+ result.current.setShowAllWorkspaces(true)
+ result.current.setSearchQuery("React")
+ })
+
+ expect(result.current.tasks).toHaveLength(1)
+ expect(result.current.tasks[0].id).toBe("task-1")
+ expect((result.current.tasks[0] as any).highlight).toBe("Create a React component")
+ })
+
+ it("automatically switches to mostRelevant when searching", () => {
+ const { result } = renderHook(() => useTaskSearch())
+
+ // Initially lastNonRelevantSort should be "newest" (the default)
+ expect(result.current.lastNonRelevantSort).toBe("newest")
+
+ act(() => {
+ result.current.setSortOption("oldest")
+ })
+
+ expect(result.current.sortOption).toBe("oldest")
+
+ // Clear lastNonRelevantSort to test the auto-switch behavior
+ act(() => {
+ result.current.setLastNonRelevantSort(null)
+ })
+
+ act(() => {
+ result.current.setSearchQuery("test")
+ })
+
+ // The hook should automatically switch to mostRelevant when there's a search query
+ // and the current sort is not mostRelevant and lastNonRelevantSort is null
+ expect(result.current.sortOption).toBe("mostRelevant")
+ expect(result.current.lastNonRelevantSort).toBe("oldest")
+ })
+
+ it("restores previous sort when clearing search", () => {
+ const { result } = renderHook(() => useTaskSearch())
+
+ act(() => {
+ result.current.setSortOption("mostExpensive")
+ })
+
+ expect(result.current.sortOption).toBe("mostExpensive")
+
+ // Clear lastNonRelevantSort to enable the auto-switch behavior
+ act(() => {
+ result.current.setLastNonRelevantSort(null)
+ })
+
+ act(() => {
+ result.current.setSearchQuery("test")
+ })
+
+ expect(result.current.sortOption).toBe("mostRelevant")
+ expect(result.current.lastNonRelevantSort).toBe("mostExpensive")
+
+ act(() => {
+ result.current.setSearchQuery("")
+ })
+
+ expect(result.current.sortOption).toBe("mostExpensive")
+ expect(result.current.lastNonRelevantSort).toBe(null)
+ })
+
+ it("handles empty task history", () => {
+ mockUseExtensionState.mockReturnValue({
+ taskHistory: [],
+ cwd: "/workspace/project1",
+ } as any)
+
+ const { result } = renderHook(() => useTaskSearch())
+
+ expect(result.current.tasks).toHaveLength(0)
+ })
+
+ it("filters out tasks without timestamp or task content", () => {
+ const incompleteTaskHistory = [
+ ...mockTaskHistory,
+ {
+ id: "incomplete-1",
+ number: 4,
+ task: "",
+ ts: Date.now(),
+ tokensIn: 0,
+ tokensOut: 0,
+ totalCost: 0,
+ },
+ {
+ id: "incomplete-2",
+ number: 5,
+ task: "Valid task",
+ ts: 0,
+ tokensIn: 0,
+ tokensOut: 0,
+ totalCost: 0,
+ },
+ ] as HistoryItem[]
+
+ mockUseExtensionState.mockReturnValue({
+ taskHistory: incompleteTaskHistory,
+ cwd: "/workspace/project1",
+ } as any)
+
+ const { result } = renderHook(() => useTaskSearch())
+
+ act(() => {
+ result.current.setShowAllWorkspaces(true)
+ })
+
+ // Should only include tasks with both ts and task content
+ expect(result.current.tasks).toHaveLength(3)
+ expect(result.current.tasks.every((task) => task.ts && task.task)).toBe(true)
+ })
+
+ it("handles search with no results", () => {
+ const { result } = renderHook(() => useTaskSearch())
+
+ act(() => {
+ result.current.setShowAllWorkspaces(true)
+ result.current.setSearchQuery("nonexistent")
+ })
+
+ expect(result.current.tasks).toHaveLength(0)
+ })
+
+ it("preserves search results order when using mostRelevant sort", () => {
+ const { result } = renderHook(() => useTaskSearch())
+
+ act(() => {
+ result.current.setShowAllWorkspaces(true)
+ result.current.setSearchQuery("test")
+ result.current.setSortOption("mostRelevant")
+ })
+
+ // When searching, mostRelevant should preserve fzf order
+ // When not searching, it should fall back to newest
+ expect(result.current.sortOption).toBe("mostRelevant")
+ })
+})
diff --git a/webview-ui/src/i18n/locales/ca/history.json b/webview-ui/src/i18n/locales/ca/history.json
index 9efa263823..99b39ec044 100644
--- a/webview-ui/src/i18n/locales/ca/history.json
+++ b/webview-ui/src/i18n/locales/ca/history.json
@@ -35,5 +35,17 @@
"confirmDeleteTasks": "Estàs segur que vols eliminar {{count}} tasques?",
"deleteTasksWarning": "Les tasques eliminades no es poden recuperar. Si us plau, assegura't que vols continuar.",
"deleteItems": "Eliminar {{count}} elements",
- "showAllWorkspaces": "Mostrar tasques de tots els espais de treball"
+ "workspace": {
+ "prefix": "Espai de treball:",
+ "current": "Actual",
+ "all": "Tots"
+ },
+ "sort": {
+ "prefix": "Ordenar:",
+ "newest": "Més recents",
+ "oldest": "Més antigues",
+ "mostExpensive": "Més cares",
+ "mostTokens": "Més tokens",
+ "mostRelevant": "Més rellevants"
+ }
}
diff --git a/webview-ui/src/i18n/locales/de/history.json b/webview-ui/src/i18n/locales/de/history.json
index 247d09a892..fe9df63f2c 100644
--- a/webview-ui/src/i18n/locales/de/history.json
+++ b/webview-ui/src/i18n/locales/de/history.json
@@ -35,5 +35,17 @@
"confirmDeleteTasks": "Bist du sicher, dass du {{count}} Aufgaben löschen möchtest?",
"deleteTasksWarning": "Gelöschte Aufgaben können nicht wiederhergestellt werden. Bitte vergewissere dich, dass du fortfahren möchtest.",
"deleteItems": "{{count}} Elemente löschen",
- "showAllWorkspaces": "Aufgaben aus allen Arbeitsbereichen anzeigen"
+ "workspace": {
+ "prefix": "Arbeitsbereich:",
+ "current": "Aktuell",
+ "all": "Alle"
+ },
+ "sort": {
+ "prefix": "Sortieren:",
+ "newest": "Neueste",
+ "oldest": "Älteste",
+ "mostExpensive": "Teuerste",
+ "mostTokens": "Meiste Tokens",
+ "mostRelevant": "Relevanteste"
+ }
}
diff --git a/webview-ui/src/i18n/locales/en/history.json b/webview-ui/src/i18n/locales/en/history.json
index 158d979f66..3d59b4b2c2 100644
--- a/webview-ui/src/i18n/locales/en/history.json
+++ b/webview-ui/src/i18n/locales/en/history.json
@@ -1,9 +1,4 @@
{
- "recentTasks": "Tasks",
- "viewAll": "View All Tasks",
- "tokens": "Tokens: ↑{{in}} ↓{{out}}",
- "cache": "Cache: +{{writes}} → {{reads}}",
- "apiCost": "API Cost: ${{cost}}",
"history": "History",
"exitSelectionMode": "Exit Selection Mode",
"enterSelectionMode": "Enter Selection Mode",
@@ -15,9 +10,6 @@
"mostTokens": "Most Tokens",
"mostRelevant": "Most Relevant",
"deleteTaskTitle": "Delete Task (Shift + Click to skip confirmation)",
- "tokensLabel": "Tokens:",
- "cacheLabel": "Cache:",
- "apiCostLabel": "API Cost:",
"copyPrompt": "Copy Prompt",
"exportTask": "Export Task",
"deleteTask": "Delete Task",
@@ -35,5 +27,17 @@
"confirmDeleteTasks": "Are you sure you want to delete {{count}} tasks?",
"deleteTasksWarning": "Deleted tasks cannot be recovered. Please make sure you want to proceed.",
"deleteItems": "Delete {{count}} Items",
- "showAllWorkspaces": "Show tasks from all workspaces"
+ "workspace": {
+ "prefix": "Workspace:",
+ "current": "Current",
+ "all": "All"
+ },
+ "sort": {
+ "prefix": "Sort:",
+ "newest": "Newest",
+ "oldest": "Oldest",
+ "mostExpensive": "Most Expensive",
+ "mostTokens": "Most Tokens",
+ "mostRelevant": "Most Relevant"
+ }
}
diff --git a/webview-ui/src/i18n/locales/es/history.json b/webview-ui/src/i18n/locales/es/history.json
index d7c65ef6b1..3294eeff90 100644
--- a/webview-ui/src/i18n/locales/es/history.json
+++ b/webview-ui/src/i18n/locales/es/history.json
@@ -35,5 +35,17 @@
"confirmDeleteTasks": "¿Estás seguro de que quieres eliminar {{count}} tareas?",
"deleteTasksWarning": "Las tareas eliminadas no se pueden recuperar. Por favor, asegúrate de que quieres continuar.",
"deleteItems": "Eliminar {{count}} elementos",
- "showAllWorkspaces": "Mostrar tareas de todos los espacios de trabajo"
+ "workspace": {
+ "prefix": "Espacio de trabajo:",
+ "current": "Actual",
+ "all": "Todos"
+ },
+ "sort": {
+ "prefix": "Ordenar:",
+ "newest": "Más recientes",
+ "oldest": "Más antiguas",
+ "mostExpensive": "Más costosas",
+ "mostTokens": "Más tokens",
+ "mostRelevant": "Más relevantes"
+ }
}
diff --git a/webview-ui/src/i18n/locales/fr/history.json b/webview-ui/src/i18n/locales/fr/history.json
index 4e33048753..6c4612199f 100644
--- a/webview-ui/src/i18n/locales/fr/history.json
+++ b/webview-ui/src/i18n/locales/fr/history.json
@@ -35,5 +35,17 @@
"confirmDeleteTasks": "Êtes-vous sûr de vouloir supprimer {{count}} tâches ?",
"deleteTasksWarning": "Les tâches supprimées ne peuvent pas être récupérées. Veuillez confirmer que vous souhaitez continuer.",
"deleteItems": "Supprimer {{count}} éléments",
- "showAllWorkspaces": "Afficher les tâches de tous les espaces de travail"
+ "workspace": {
+ "prefix": "Espace de travail :",
+ "current": "Actuel",
+ "all": "Tous"
+ },
+ "sort": {
+ "prefix": "Trier :",
+ "newest": "Plus récentes",
+ "oldest": "Plus anciennes",
+ "mostExpensive": "Plus coûteuses",
+ "mostTokens": "Plus de tokens",
+ "mostRelevant": "Plus pertinentes"
+ }
}
diff --git a/webview-ui/src/i18n/locales/hi/history.json b/webview-ui/src/i18n/locales/hi/history.json
index 58373f9526..becf787d62 100644
--- a/webview-ui/src/i18n/locales/hi/history.json
+++ b/webview-ui/src/i18n/locales/hi/history.json
@@ -1,9 +1,4 @@
{
- "recentTasks": "कार्य",
- "viewAll": "सभी देखें",
- "tokens": "Tokens: ↑{{in}} ↓{{out}}",
- "cache": "कैश: +{{writes}} → {{reads}}",
- "apiCost": "API लागत: ${{cost}}",
"history": "इतिहास",
"exitSelectionMode": "चयन मोड से बाहर निकलें",
"enterSelectionMode": "चयन मोड में प्रवेश करें",
@@ -15,9 +10,6 @@
"mostTokens": "सबसे अधिक टोकन",
"mostRelevant": "सबसे प्रासंगिक",
"deleteTaskTitle": "कार्य हटाएं (Shift + क्लिक पुष्टि छोड़ने के लिए)",
- "tokensLabel": "Tokens:",
- "cacheLabel": "कैश:",
- "apiCostLabel": "API लागत:",
"copyPrompt": "प्रॉम्प्ट कॉपी करें",
"exportTask": "कार्य निर्यात करें",
"deleteTask": "कार्य हटाएं",
@@ -35,5 +27,17 @@
"confirmDeleteTasks": "क्या आप वाकई {{count}} कार्य हटाना चाहते हैं?",
"deleteTasksWarning": "हटाए गए कार्य पुनर्प्राप्त नहीं किए जा सकते। कृपया सुनिश्चित करें कि आप आगे बढ़ना चाहते हैं।",
"deleteItems": "{{count}} आइटम हटाएं",
- "showAllWorkspaces": "सभी वर्कस्पेस से कार्य दिखाएं"
+ "workspace": {
+ "prefix": "कार्यस्थान:",
+ "current": "वर्तमान",
+ "all": "सभी"
+ },
+ "sort": {
+ "prefix": "क्रमबद्ध करें:",
+ "newest": "नवीनतम",
+ "oldest": "सबसे पुराना",
+ "mostExpensive": "सबसे महंगा",
+ "mostTokens": "सबसे अधिक टोकन",
+ "mostRelevant": "सबसे प्रासंगिक"
+ }
}
diff --git a/webview-ui/src/i18n/locales/id/history.json b/webview-ui/src/i18n/locales/id/history.json
index 90b12534a7..912d0c2b02 100644
--- a/webview-ui/src/i18n/locales/id/history.json
+++ b/webview-ui/src/i18n/locales/id/history.json
@@ -37,6 +37,17 @@
"deleteTaskFavoritedWarning": "Tugas ini telah ditandai sebagai favorit. Apakah kamu yakin ingin menghapusnya?",
"deleteTasksFavoritedWarning": "{{count}} tugas yang dipilih telah ditandai sebagai favorit. Apakah kamu yakin ingin menghapusnya?",
"deleteItems": "Hapus {{count}} Item",
- "showAllWorkspaces": "Tampilkan tugas dari semua workspace",
- "showFavoritesOnly": "Tampilkan hanya favorit"
+ "workspace": {
+ "prefix": "Ruang Kerja:",
+ "current": "Saat Ini",
+ "all": "Semua"
+ },
+ "sort": {
+ "prefix": "Urutkan:",
+ "newest": "Terbaru",
+ "oldest": "Terlama",
+ "mostExpensive": "Termahal",
+ "mostTokens": "Token Terbanyak",
+ "mostRelevant": "Paling Relevan"
+ }
}
diff --git a/webview-ui/src/i18n/locales/it/history.json b/webview-ui/src/i18n/locales/it/history.json
index c56f8492d3..5fce0c1639 100644
--- a/webview-ui/src/i18n/locales/it/history.json
+++ b/webview-ui/src/i18n/locales/it/history.json
@@ -1,23 +1,15 @@
{
- "recentTasks": "Compiti",
- "viewAll": "Vedi tutto",
- "tokens": "Tokens: ↑{{in}} ↓{{out}}",
- "cache": "Cache: +{{writes}} → {{reads}}",
- "apiCost": "Costo API: ${{cost}}",
"history": "Cronologia",
"exitSelectionMode": "Esci dalla modalità selezione",
"enterSelectionMode": "Entra in modalità selezione",
"done": "Fatto",
- "searchPlaceholder": "Ricerca nella cronologia...",
+ "searchPlaceholder": "Ricerca sfocata nella cronologia...",
"newest": "Più recenti",
"oldest": "Più vecchie",
"mostExpensive": "Più costose",
"mostTokens": "Più token",
"mostRelevant": "Più rilevanti",
"deleteTaskTitle": "Elimina attività (Shift + Clic per saltare conferma)",
- "tokensLabel": "Tokens:",
- "cacheLabel": "Cache:",
- "apiCostLabel": "Costo API:",
"copyPrompt": "Copia prompt",
"exportTask": "Esporta attività",
"deleteTask": "Elimina attività",
@@ -35,5 +27,17 @@
"confirmDeleteTasks": "Sei sicuro di voler eliminare {{count}} attività?",
"deleteTasksWarning": "Le attività eliminate non possono essere recuperate. Assicurati di voler continuare.",
"deleteItems": "Elimina {{count}} elementi",
- "showAllWorkspaces": "Mostra attività da tutti gli spazi di lavoro"
+ "workspace": {
+ "prefix": "Spazio di lavoro:",
+ "current": "Attuale",
+ "all": "Tutti"
+ },
+ "sort": {
+ "prefix": "Ordina:",
+ "newest": "Più recenti",
+ "oldest": "Più vecchie",
+ "mostExpensive": "Più costose",
+ "mostTokens": "Più token",
+ "mostRelevant": "Più rilevanti"
+ }
}
diff --git a/webview-ui/src/i18n/locales/ja/history.json b/webview-ui/src/i18n/locales/ja/history.json
index 561df1e52b..3fbd4f0045 100644
--- a/webview-ui/src/i18n/locales/ja/history.json
+++ b/webview-ui/src/i18n/locales/ja/history.json
@@ -1,9 +1,4 @@
{
- "recentTasks": "Recent Tasks",
- "viewAll": "すべて表示",
- "tokens": "Tokens: ↑{{in}} ↓{{out}}",
- "cache": "キャッシュ: +{{writes}} → {{reads}}",
- "apiCost": "API コスト: ${{cost}}",
"history": "履歴",
"exitSelectionMode": "選択モードを終了",
"enterSelectionMode": "選択モードに入る",
@@ -15,9 +10,6 @@
"mostTokens": "最多トークン",
"mostRelevant": "最も関連性の高い",
"deleteTaskTitle": "タスクを削除(Shift + クリックで確認をスキップ)",
- "tokensLabel": "Tokens:",
- "cacheLabel": "キャッシュ:",
- "apiCostLabel": "API コスト:",
"copyPrompt": "プロンプトをコピー",
"exportTask": "タスクをエクスポート",
"deleteTask": "タスクを削除",
@@ -35,5 +27,17 @@
"confirmDeleteTasks": "{{count}} 件のタスクを削除してもよろしいですか?",
"deleteTasksWarning": "削除されたタスクは復元できません。続行してもよろしいですか?",
"deleteItems": "{{count}} 項目を削除",
- "showAllWorkspaces": "すべてのワークスペースのタスクを表示"
+ "workspace": {
+ "prefix": "ワークスペース:",
+ "current": "現在",
+ "all": "すべて"
+ },
+ "sort": {
+ "prefix": "ソート:",
+ "newest": "最新",
+ "oldest": "最古",
+ "mostExpensive": "最も高価",
+ "mostTokens": "最多トークン",
+ "mostRelevant": "最も関連性の高い"
+ }
}
diff --git a/webview-ui/src/i18n/locales/ko/history.json b/webview-ui/src/i18n/locales/ko/history.json
index a35cc9b1f1..cd9f6d8878 100644
--- a/webview-ui/src/i18n/locales/ko/history.json
+++ b/webview-ui/src/i18n/locales/ko/history.json
@@ -1,9 +1,4 @@
{
- "recentTasks": "작업",
- "viewAll": "모두 보기",
- "tokens": "Tokens: ↑{{in}} ↓{{out}}",
- "cache": "캐시: +{{writes}} → {{reads}}",
- "apiCost": "API 비용: ${{cost}}",
"history": "기록",
"exitSelectionMode": "선택 모드 종료",
"enterSelectionMode": "선택 모드 진입",
@@ -15,9 +10,6 @@
"mostTokens": "토큰 많은순",
"mostRelevant": "관련성 높은순",
"deleteTaskTitle": "작업 삭제 (Shift + 클릭으로 확인 생략)",
- "tokensLabel": "Tokens:",
- "cacheLabel": "캐시:",
- "apiCostLabel": "API 비용:",
"copyPrompt": "프롬프트 복사",
"exportTask": "작업 내보내기",
"deleteTask": "작업 삭제",
@@ -35,5 +27,17 @@
"confirmDeleteTasks": "{{count}}개의 작업을 삭제하시겠습니까?",
"deleteTasksWarning": "삭제된 작업은 복구할 수 없습니다. 계속 진행하시겠습니까?",
"deleteItems": "{{count}}개 항목 삭제",
- "showAllWorkspaces": "모든 워크스페이스의 작업 표시"
+ "workspace": {
+ "prefix": "워크스페이스:",
+ "current": "현재",
+ "all": "모두"
+ },
+ "sort": {
+ "prefix": "정렬:",
+ "newest": "최신순",
+ "oldest": "오래된순",
+ "mostExpensive": "가장 비싼순",
+ "mostTokens": "토큰 많은순",
+ "mostRelevant": "관련성 높은순"
+ }
}
diff --git a/webview-ui/src/i18n/locales/nl/history.json b/webview-ui/src/i18n/locales/nl/history.json
index 2addee0d16..09461bfd61 100644
--- a/webview-ui/src/i18n/locales/nl/history.json
+++ b/webview-ui/src/i18n/locales/nl/history.json
@@ -1,9 +1,4 @@
{
- "recentTasks": "Taken",
- "viewAll": "Alle taken weergeven",
- "tokens": "Tokens: ↑{{in}} ↓{{out}}",
- "cache": "Cache: +{{writes}} → {{reads}}",
- "apiCost": "API-kosten: ${{cost}}",
"history": "Geschiedenis",
"exitSelectionMode": "Selectiemodus verlaten",
"enterSelectionMode": "Selectiemodus starten",
@@ -15,9 +10,6 @@
"mostTokens": "Meeste tokens",
"mostRelevant": "Meest relevant",
"deleteTaskTitle": "Taak verwijderen (Shift + Klik om bevestiging over te slaan)",
- "tokensLabel": "Tokens:",
- "cacheLabel": "Cache:",
- "apiCostLabel": "API-kosten:",
"copyPrompt": "Prompt kopiëren",
"exportTask": "Taak exporteren",
"deleteTask": "Taak verwijderen",
@@ -35,5 +27,17 @@
"confirmDeleteTasks": "Weet je zeker dat je {{count}} taken wilt verwijderen?",
"deleteTasksWarning": "Verwijderde taken kunnen niet worden hersteld. Zorg ervoor dat je wilt doorgaan.",
"deleteItems": "Verwijder {{count}} items",
- "showAllWorkspaces": "Toon taken van alle werkruimtes"
+ "workspace": {
+ "prefix": "Werkruimte:",
+ "current": "Huidig",
+ "all": "Alle"
+ },
+ "sort": {
+ "prefix": "Sorteren:",
+ "newest": "Nieuwste",
+ "oldest": "Oudste",
+ "mostExpensive": "Duurste",
+ "mostTokens": "Meeste tokens",
+ "mostRelevant": "Meest relevant"
+ }
}
diff --git a/webview-ui/src/i18n/locales/pl/history.json b/webview-ui/src/i18n/locales/pl/history.json
index f775b04de3..4f3af8b245 100644
--- a/webview-ui/src/i18n/locales/pl/history.json
+++ b/webview-ui/src/i18n/locales/pl/history.json
@@ -1,23 +1,15 @@
{
- "recentTasks": "Zadania",
- "viewAll": "Zobacz wszystkie",
- "tokens": "Tokens: ↑{{in}} ↓{{out}}",
- "cache": "Pamięć podręczna: +{{writes}} → {{reads}}",
- "apiCost": "Koszt API: ${{cost}}",
"history": "Historia",
"exitSelectionMode": "Wyłącz tryb wyboru",
"enterSelectionMode": "Włącz tryb wyboru",
"done": "Gotowe",
- "searchPlaceholder": "Szukaj w historii...",
+ "searchPlaceholder": "Rozmyte wyszukiwanie historii...",
"newest": "Najnowsze",
"oldest": "Najstarsze",
"mostExpensive": "Najdroższe",
"mostTokens": "Najwięcej tokenów",
"mostRelevant": "Najbardziej trafne",
"deleteTaskTitle": "Usuń zadanie (Shift + Klik, aby pominąć potwierdzenie)",
- "tokensLabel": "Tokens:",
- "cacheLabel": "Pamięć podręczna:",
- "apiCostLabel": "Koszt API:",
"copyPrompt": "Kopiuj prompt",
"exportTask": "Eksportuj zadanie",
"deleteTask": "Usuń zadanie",
@@ -35,5 +27,17 @@
"confirmDeleteTasks": "Czy na pewno chcesz usunąć {{count}} zadań?",
"deleteTasksWarning": "Usuniętych zadań nie można przywrócić. Upewnij się, że chcesz kontynuować.",
"deleteItems": "Usuń {{count}} elementów",
- "showAllWorkspaces": "Pokaż zadania ze wszystkich przestrzeni roboczych"
+ "workspace": {
+ "prefix": "Obszar roboczy:",
+ "current": "Bieżący",
+ "all": "Wszystkie"
+ },
+ "sort": {
+ "prefix": "Sortuj:",
+ "newest": "Najnowsze",
+ "oldest": "Najstarsze",
+ "mostExpensive": "Najdroższe",
+ "mostTokens": "Najwięcej tokenów",
+ "mostRelevant": "Najbardziej trafne"
+ }
}
diff --git a/webview-ui/src/i18n/locales/pt-BR/history.json b/webview-ui/src/i18n/locales/pt-BR/history.json
index 58fdb7e6ed..a6596a37f0 100644
--- a/webview-ui/src/i18n/locales/pt-BR/history.json
+++ b/webview-ui/src/i18n/locales/pt-BR/history.json
@@ -1,23 +1,15 @@
{
- "recentTasks": "Tarefas",
- "viewAll": "Ver todas",
- "tokens": "Tokens: ↑{{in}} ↓{{out}}",
- "cache": "Cache: +{{writes}} → {{reads}}",
- "apiCost": "Custo da API: ${{cost}}",
"history": "Histórico",
"exitSelectionMode": "Sair do modo de seleção",
"enterSelectionMode": "Entrar no modo de seleção",
"done": "Concluído",
- "searchPlaceholder": "Pesquisar no histórico...",
+ "searchPlaceholder": "Pesquisar histórico...",
"newest": "Mais recentes",
"oldest": "Mais antigas",
"mostExpensive": "Mais caras",
"mostTokens": "Mais tokens",
"mostRelevant": "Mais relevantes",
"deleteTaskTitle": "Excluir tarefa (Shift + Clique para pular confirmação)",
- "tokensLabel": "Tokens:",
- "cacheLabel": "Cache:",
- "apiCostLabel": "Custo da API:",
"copyPrompt": "Copiar prompt",
"exportTask": "Exportar tarefa",
"deleteTask": "Excluir tarefa",
@@ -35,5 +27,17 @@
"confirmDeleteTasks": "Tem certeza que deseja excluir {{count}} tarefas?",
"deleteTasksWarning": "As tarefas excluídas não podem ser recuperadas. Por favor, certifique-se de que deseja prosseguir.",
"deleteItems": "Excluir {{count}} itens",
- "showAllWorkspaces": "Mostrar tarefas de todos os espaços de trabalho"
+ "workspace": {
+ "prefix": "Espaço de trabalho:",
+ "current": "Atual",
+ "all": "Todos"
+ },
+ "sort": {
+ "prefix": "Ordenar:",
+ "newest": "Mais recentes",
+ "oldest": "Mais antigas",
+ "mostExpensive": "Mais caras",
+ "mostTokens": "Mais tokens",
+ "mostRelevant": "Mais relevantes"
+ }
}
diff --git a/webview-ui/src/i18n/locales/ru/history.json b/webview-ui/src/i18n/locales/ru/history.json
index d5e1af8085..3fd97c7bcb 100644
--- a/webview-ui/src/i18n/locales/ru/history.json
+++ b/webview-ui/src/i18n/locales/ru/history.json
@@ -1,9 +1,4 @@
{
- "recentTasks": "Недавние задачи",
- "viewAll": "Просмотреть все задачи",
- "tokens": "Токены: ↑{{in}} ↓{{out}}",
- "cache": "Кэш: +{{writes}} → {{reads}}",
- "apiCost": "Стоимость API: ${{cost}}",
"history": "История",
"exitSelectionMode": "Выйти из режима выбора",
"enterSelectionMode": "Войти в режим выбора",
@@ -15,9 +10,6 @@
"mostTokens": "Больше всего токенов",
"mostRelevant": "Наиболее релевантные",
"deleteTaskTitle": "Удалить задачу (Shift + клик для пропуска подтверждения)",
- "tokensLabel": "Токены:",
- "cacheLabel": "Кэш:",
- "apiCostLabel": "Стоимость API:",
"copyPrompt": "Скопировать запрос",
"exportTask": "Экспортировать задачу",
"deleteTask": "Удалить задачу",
@@ -35,5 +27,17 @@
"confirmDeleteTasks": "Вы уверены, что хотите удалить {{count}} задач?",
"deleteTasksWarning": "Удалённые задачи не могут быть восстановлены. Пожалуйста, убедитесь, что хотите продолжить.",
"deleteItems": "Удалить {{count}} элементов",
- "showAllWorkspaces": "Показать задачи из всех рабочих пространств"
+ "workspace": {
+ "prefix": "Рабочая область:",
+ "current": "Текущая",
+ "all": "Все"
+ },
+ "sort": {
+ "prefix": "Сортировать:",
+ "newest": "Самые новые",
+ "oldest": "Самые старые",
+ "mostExpensive": "Самые дорогие",
+ "mostTokens": "Больше всего токенов",
+ "mostRelevant": "Наиболее релевантные"
+ }
}
diff --git a/webview-ui/src/i18n/locales/tr/history.json b/webview-ui/src/i18n/locales/tr/history.json
index 672659b90d..00cb47f2d5 100644
--- a/webview-ui/src/i18n/locales/tr/history.json
+++ b/webview-ui/src/i18n/locales/tr/history.json
@@ -1,9 +1,4 @@
{
- "recentTasks": "Görevler",
- "viewAll": "Tümünü Gör",
- "tokens": "Tokens: ↑{{in}} ↓{{out}}",
- "cache": "Önbellek: +{{writes}} → {{reads}}",
- "apiCost": "API Maliyeti: ${{cost}}",
"history": "Geçmiş",
"exitSelectionMode": "Seçim Modundan Çık",
"enterSelectionMode": "Seçim Moduna Gir",
@@ -15,9 +10,6 @@
"mostTokens": "En Çok Token",
"mostRelevant": "En İlgili",
"deleteTaskTitle": "Görevi Sil (Onayı atlamak için Shift + Tıkla)",
- "tokensLabel": "Tokens:",
- "cacheLabel": "Önbellek:",
- "apiCostLabel": "API Maliyeti:",
"copyPrompt": "Promptu Kopyala",
"exportTask": "Görevi Dışa Aktar",
"deleteTask": "Görevi Sil",
@@ -35,5 +27,17 @@
"confirmDeleteTasks": "{{count}} görevi silmek istediğinizden emin misiniz?",
"deleteTasksWarning": "Silinen görevler geri alınamaz. Lütfen devam etmek istediğinizden emin olun.",
"deleteItems": "{{count}} Öğeyi Sil",
- "showAllWorkspaces": "Tüm çalışma alanlarından görevleri göster"
+ "workspace": {
+ "prefix": "Çalışma Alanı:",
+ "current": "Mevcut",
+ "all": "Tümü"
+ },
+ "sort": {
+ "prefix": "Sırala:",
+ "newest": "En Yeni",
+ "oldest": "En Eski",
+ "mostExpensive": "En Pahalı",
+ "mostTokens": "En Çok Token",
+ "mostRelevant": "En İlgili"
+ }
}
diff --git a/webview-ui/src/i18n/locales/vi/history.json b/webview-ui/src/i18n/locales/vi/history.json
index 00154d47c3..1cac2b3404 100644
--- a/webview-ui/src/i18n/locales/vi/history.json
+++ b/webview-ui/src/i18n/locales/vi/history.json
@@ -1,10 +1,7 @@
{
- "recentTasks": "Nhiệm vụ",
- "viewAll": "Xem tất cả",
- "tokens": "Token: ↑{{in}} ↓{{out}}",
- "cache": "Bộ nhớ đệm: +{{writes}} → {{reads}}",
- "apiCost": "Chi phí API: ${{cost}}",
"history": "Lịch sử",
+ "exitSelectionMode": "Thoát chế độ chọn",
+ "enterSelectionMode": "Vào chế độ chọn",
"done": "Hoàn thành",
"searchPlaceholder": "Tìm kiếm lịch sử...",
"newest": "Mới nhất",
@@ -13,17 +10,12 @@
"mostTokens": "Nhiều token nhất",
"mostRelevant": "Liên quan nhất",
"deleteTaskTitle": "Xóa nhiệm vụ (Shift + Click để bỏ qua xác nhận)",
- "tokensLabel": "Token:",
- "cacheLabel": "Bộ nhớ đệm:",
- "apiCostLabel": "Chi phí API:",
"copyPrompt": "Sao chép lời nhắc",
"exportTask": "Xuất nhiệm vụ",
"deleteTask": "Xóa nhiệm vụ",
"deleteTaskMessage": "Bạn có chắc chắn muốn xóa nhiệm vụ này không? Hành động này không thể hoàn tác.",
"cancel": "Hủy",
"delete": "Xóa",
- "exitSelectionMode": "Thoát chế độ chọn",
- "enterSelectionMode": "Vào chế độ chọn",
"exitSelection": "Thoát chọn",
"selectionMode": "Chế độ chọn",
"deselectAll": "Bỏ chọn tất cả",
@@ -35,5 +27,17 @@
"confirmDeleteTasks": "Bạn có chắc chắn muốn xóa {{count}} nhiệm vụ không?",
"deleteTasksWarning": "Các nhiệm vụ đã xóa không thể khôi phục. Vui lòng chắc chắn bạn muốn tiếp tục.",
"deleteItems": "Xóa {{count}} mục",
- "showAllWorkspaces": "Hiển thị nhiệm vụ từ tất cả không gian làm việc"
+ "workspace": {
+ "prefix": "Không gian làm việc:",
+ "current": "Hiện tại",
+ "all": "Tất cả"
+ },
+ "sort": {
+ "prefix": "Sắp xếp:",
+ "newest": "Mới nhất",
+ "oldest": "Cũ nhất",
+ "mostExpensive": "Đắt nhất",
+ "mostTokens": "Nhiều token nhất",
+ "mostRelevant": "Liên quan nhất"
+ }
}
diff --git a/webview-ui/src/i18n/locales/zh-CN/history.json b/webview-ui/src/i18n/locales/zh-CN/history.json
index 43c362aaa1..89c14f7c37 100644
--- a/webview-ui/src/i18n/locales/zh-CN/history.json
+++ b/webview-ui/src/i18n/locales/zh-CN/history.json
@@ -1,23 +1,15 @@
{
- "recentTasks": "任务",
- "viewAll": "查看全部",
- "tokens": "Token用量: ↑{{in}} ↓{{out}}",
- "cache": "缓存操作: +{{writes}} → {{reads}}",
- "apiCost": "API费用: ${{cost}}",
"history": "历史记录",
"exitSelectionMode": "退出多选模式",
"enterSelectionMode": "进入多选模式",
"done": "完成",
- "searchPlaceholder": "请输入搜索关键词",
- "newest": "时间↓",
- "oldest": "时间↑",
- "mostExpensive": "费用↓",
- "mostTokens": "上下文↓",
- "mostRelevant": "相关性↓",
+ "searchPlaceholder": "模糊搜索历史记录...",
+ "newest": "最新",
+ "oldest": "最旧",
+ "mostExpensive": "费用最高",
+ "mostTokens": "最多 Token",
+ "mostRelevant": "最相关",
"deleteTaskTitle": "删除任务(Shift + 点击跳过确认)",
- "tokensLabel": "Token用量:",
- "cacheLabel": "缓存操作:",
- "apiCostLabel": "API费用:",
"copyPrompt": "复制提示词",
"exportTask": "导出任务",
"deleteTask": "删除任务",
@@ -25,7 +17,7 @@
"cancel": "取消",
"delete": "删除",
"exitSelection": "退出多选",
- "selectionMode": "多选",
+ "selectionMode": "多选模式",
"deselectAll": "取消全选",
"selectAll": "全选",
"selectedItems": "已选 {{selected}}/{{total}} 项",
@@ -35,5 +27,17 @@
"confirmDeleteTasks": "确认删除 {{count}} 项任务?",
"deleteTasksWarning": "删除后将无法恢复,请谨慎操作。",
"deleteItems": "删除 {{count}} 项",
- "showAllWorkspaces": "显示所有工作区的任务"
+ "workspace": {
+ "prefix": "工作区:",
+ "current": "当前",
+ "all": "所有"
+ },
+ "sort": {
+ "prefix": "排序:",
+ "newest": "最新",
+ "oldest": "最旧",
+ "mostExpensive": "费用最高",
+ "mostTokens": "最多 Token",
+ "mostRelevant": "最相关"
+ }
}
diff --git a/webview-ui/src/i18n/locales/zh-TW/history.json b/webview-ui/src/i18n/locales/zh-TW/history.json
index d234ef6b3d..0321f783a3 100644
--- a/webview-ui/src/i18n/locales/zh-TW/history.json
+++ b/webview-ui/src/i18n/locales/zh-TW/history.json
@@ -1,9 +1,4 @@
{
- "recentTasks": "工作",
- "viewAll": "檢視全部",
- "tokens": "Tokens: ↑{{in}} ↓{{out}}",
- "cache": "快取:+{{writes}} → {{reads}}",
- "apiCost": "API 費用:${{cost}}",
"history": "歷史記錄",
"exitSelectionMode": "離開選擇模式",
"enterSelectionMode": "進入選擇模式",
@@ -15,9 +10,6 @@
"mostTokens": "最多 Token",
"mostRelevant": "最相關",
"deleteTaskTitle": "刪除工作(按住 Shift 並點選可跳過確認)",
- "tokensLabel": "Tokens:",
- "cacheLabel": "快取:",
- "apiCostLabel": "API 費用:",
"copyPrompt": "複製提示詞",
"exportTask": "匯出工作",
"deleteTask": "刪除工作",
@@ -35,5 +27,17 @@
"confirmDeleteTasks": "確定要刪除 {{count}} 個工作嗎?",
"deleteTasksWarning": "已刪除的工作無法還原。請確認是否要繼續。",
"deleteItems": "刪除 {{count}} 個項目",
- "showAllWorkspaces": "顯示所有工作區的工作"
+ "workspace": {
+ "prefix": "工作區:",
+ "current": "目前",
+ "all": "所有"
+ },
+ "sort": {
+ "prefix": "排序:",
+ "newest": "最新",
+ "oldest": "最舊",
+ "mostExpensive": "費用最高",
+ "mostTokens": "最多 Token",
+ "mostRelevant": "最相關"
+ }
}