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": "最相關" + } }