Просмотр исходного кода

refactor: more consistent history UI (#4684)

Dicha Zelianivan Arkana 7 месяцев назад
Родитель
Сommit
e618fa958c
36 измененных файлов с 1430 добавлено и 757 удалено
  1. 4 5
      webview-ui/src/components/history/CopyButton.tsx
  2. 38 0
      webview-ui/src/components/history/DeleteButton.tsx
  3. 12 5
      webview-ui/src/components/history/ExportButton.tsx
  4. 75 60
      webview-ui/src/components/history/HistoryView.tsx
  5. 9 40
      webview-ui/src/components/history/TaskItem.tsx
  6. 42 97
      webview-ui/src/components/history/TaskItemFooter.tsx
  7. 10 47
      webview-ui/src/components/history/TaskItemHeader.tsx
  8. 87 0
      webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.test.tsx
  9. 31 0
      webview-ui/src/components/history/__tests__/CopyButton.test.tsx
  10. 20 0
      webview-ui/src/components/history/__tests__/DeleteButton.test.tsx
  11. 143 0
      webview-ui/src/components/history/__tests__/DeleteTaskDialog.test.tsx
  12. 28 0
      webview-ui/src/components/history/__tests__/ExportButton.test.tsx
  13. 199 0
      webview-ui/src/components/history/__tests__/HistoryPreview.test.tsx
  14. 20 295
      webview-ui/src/components/history/__tests__/HistoryView.test.tsx
  15. 75 73
      webview-ui/src/components/history/__tests__/TaskItem.test.tsx
  16. 72 0
      webview-ui/src/components/history/__tests__/TaskItemFooter.test.tsx
  17. 34 0
      webview-ui/src/components/history/__tests__/TaskItemHeader.test.tsx
  18. 285 0
      webview-ui/src/components/history/__tests__/useTaskSearch.test.tsx
  19. 13 1
      webview-ui/src/i18n/locales/ca/history.json
  20. 13 1
      webview-ui/src/i18n/locales/de/history.json
  21. 13 9
      webview-ui/src/i18n/locales/en/history.json
  22. 13 1
      webview-ui/src/i18n/locales/es/history.json
  23. 13 1
      webview-ui/src/i18n/locales/fr/history.json
  24. 13 9
      webview-ui/src/i18n/locales/hi/history.json
  25. 13 2
      webview-ui/src/i18n/locales/id/history.json
  26. 14 10
      webview-ui/src/i18n/locales/it/history.json
  27. 13 9
      webview-ui/src/i18n/locales/ja/history.json
  28. 13 9
      webview-ui/src/i18n/locales/ko/history.json
  29. 13 9
      webview-ui/src/i18n/locales/nl/history.json
  30. 14 10
      webview-ui/src/i18n/locales/pl/history.json
  31. 14 10
      webview-ui/src/i18n/locales/pt-BR/history.json
  32. 13 9
      webview-ui/src/i18n/locales/ru/history.json
  33. 13 9
      webview-ui/src/i18n/locales/tr/history.json
  34. 15 11
      webview-ui/src/i18n/locales/vi/history.json
  35. 20 16
      webview-ui/src/i18n/locales/zh-CN/history.json
  36. 13 9
      webview-ui/src/i18n/locales/zh-TW/history.json

+ 4 - 5
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">
 			<span className={cn("codicon scale-80", { "codicon-check": isCopied, "codicon-copy": !isCopied })} />
 		</Button>
 	)

+ 38 - 0
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 (
+		<Button
+			variant="ghost"
+			size="icon"
+			title={t("history:deleteTaskTitle")}
+			data-testid="delete-task-button"
+			onClick={handleDeleteClick}
+			className="group-hover:opacity-100 opacity-50 transition-opacity">
+			<span className="codicon codicon-trash size-4 align-middle text-vscode-descriptionForeground" />
+		</Button>
+	)
+}

+ 12 - 5
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 (
 		<Button
 			data-testid="export"
 			variant="ghost"
 			size="icon"
 			title={t("history:exportTask")}
-			onClick={(e) => {
-				e.stopPropagation()
-				vscode.postMessage({ type: "exportTaskWithId", text: itemId })
-			}}>
-			<span className="codicon codicon-desktop-download" />
+			className="group-hover:opacity-100 opacity-50 transition-opacity"
+			onClick={handleExportClick}>
+			<span className="codicon codicon-desktop-download scale-80" />
 		</Button>
 	)
 }

+ 75 - 60
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) => {
 				</div>
 				<div className="flex flex-col gap-2">
 					<VSCodeTextField
-						style={{ width: "100%" }}
+						className="w-full"
 						placeholder={t("history:searchPlaceholder")}
 						value={searchQuery}
 						data-testid="history-search-input"
@@ -107,62 +106,83 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
 								setSortOption("mostRelevant")
 							}
 						}}>
-						<div
-							slot="start"
-							className="codicon codicon-search"
-							style={{ fontSize: 13, marginTop: 2.5, opacity: 0.8 }}
-						/>
+						<div slot="start" className="codicon codicon-search mt-0.5 opacity-80 text-sm!" />
 						{searchQuery && (
 							<div
-								className="input-icon-button codicon codicon-close"
+								className="input-icon-button codicon codicon-close flex justify-center items-center h-full"
 								aria-label="Clear search"
 								onClick={() => setSearchQuery("")}
 								slot="end"
-								style={{
-									display: "flex",
-									justifyContent: "center",
-									alignItems: "center",
-									height: "100%",
-								}}
 							/>
 						)}
 					</VSCodeTextField>
-					<VSCodeRadioGroup
-						style={{ display: "flex", flexWrap: "wrap" }}
-						value={sortOption}
-						role="radiogroup"
-						onChange={(e) => setSortOption((e.target as HTMLInputElement).value as SortOption)}>
-						<VSCodeRadio value="newest" data-testid="radio-newest">
-							{t("history:newest")}
-						</VSCodeRadio>
-						<VSCodeRadio value="oldest" data-testid="radio-oldest">
-							{t("history:oldest")}
-						</VSCodeRadio>
-						<VSCodeRadio value="mostExpensive" data-testid="radio-most-expensive">
-							{t("history:mostExpensive")}
-						</VSCodeRadio>
-						<VSCodeRadio value="mostTokens" data-testid="radio-most-tokens">
-							{t("history:mostTokens")}
-						</VSCodeRadio>
-						<VSCodeRadio
-							value="mostRelevant"
-							disabled={!searchQuery}
-							data-testid="radio-most-relevant"
-							style={{ opacity: searchQuery ? 1 : 0.5 }}>
-							{t("history:mostRelevant")}
-						</VSCodeRadio>
-					</VSCodeRadioGroup>
-
-					<div className="flex items-center gap-2">
-						<Checkbox
-							id="show-all-workspaces-view"
-							checked={showAllWorkspaces}
-							onCheckedChange={(checked) => setShowAllWorkspaces(checked === true)}
-							variant="description"
-						/>
-						<label htmlFor="show-all-workspaces-view" className="text-vscode-foreground cursor-pointer">
-							{t("history:showAllWorkspaces")}
-						</label>
+					<div className="flex gap-2">
+						<Select
+							value={showAllWorkspaces ? "all" : "current"}
+							onValueChange={(value) => setShowAllWorkspaces(value === "all")}>
+							<SelectTrigger className="flex-1">
+								<SelectValue>
+									{t("history:workspace.prefix")}{" "}
+									{t(`history:workspace.${showAllWorkspaces ? "all" : "current"}`)}
+								</SelectValue>
+							</SelectTrigger>
+							<SelectContent>
+								<SelectItem value="current">
+									<div className="flex items-center gap-2">
+										<span className="codicon codicon-folder" />
+										{t("history:workspace.current")}
+									</div>
+								</SelectItem>
+								<SelectItem value="all">
+									<div className="flex items-center gap-2">
+										<span className="codicon codicon-folder-opened" />
+										{t("history:workspace.all")}
+									</div>
+								</SelectItem>
+							</SelectContent>
+						</Select>
+						<Select value={sortOption} onValueChange={(value) => setSortOption(value as SortOption)}>
+							<SelectTrigger className="flex-1">
+								<SelectValue>
+									{t("history:sort.prefix")} {t(`history:sort.${sortOption}`)}
+								</SelectValue>
+							</SelectTrigger>
+							<SelectContent>
+								<SelectItem value="newest" data-testid="select-newest">
+									<div className="flex items-center gap-2">
+										<span className="codicon codicon-arrow-down" />
+										{t("history:newest")}
+									</div>
+								</SelectItem>
+								<SelectItem value="oldest" data-testid="select-oldest">
+									<div className="flex items-center gap-2">
+										<span className="codicon codicon-arrow-up" />
+										{t("history:oldest")}
+									</div>
+								</SelectItem>
+								<SelectItem value="mostExpensive" data-testid="select-most-expensive">
+									<div className="flex items-center gap-2">
+										<span className="codicon codicon-credit-card" />
+										{t("history:mostExpensive")}
+									</div>
+								</SelectItem>
+								<SelectItem value="mostTokens" data-testid="select-most-tokens">
+									<div className="flex items-center gap-2">
+										<span className="codicon codicon-symbol-numeric" />
+										{t("history:mostTokens")}
+									</div>
+								</SelectItem>
+								<SelectItem
+									value="mostRelevant"
+									disabled={!searchQuery}
+									data-testid="select-most-relevant">
+									<div className="flex items-center gap-2">
+										<span className="codicon codicon-search" />
+										{t("history:mostRelevant")}
+									</div>
+								</SelectItem>
+							</SelectContent>
+						</Select>
 					</div>
 
 					{/* Select all control in selection mode */}
@@ -193,10 +213,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
 
 			<TabContent className="p-0">
 				<Virtuoso
-					style={{
-						flexGrow: 1,
-						overflowY: "scroll",
-					}}
+					className="flex-1 overflow-y-scroll"
 					data={tasks}
 					data-testid="virtuoso-container"
 					initialTopMostItemIndex={0}
@@ -205,7 +222,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
 							<div {...props} ref={ref} data-testid="virtuoso-item-list" />
 						)),
 					}}
-					itemContent={(index, item) => (
+					itemContent={(_index, item) => (
 						<TaskItem
 							key={item.id}
 							item={item}
@@ -215,9 +232,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
 							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"
 						/>
 					)}
 				/>

+ 9 - 40
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 (
 		<div
 			key={item.id}
-			data-testid={isCompact ? undefined : `task-item-${item.id}`}
+			data-testid={`task-item-${item.id}`}
 			className={cn(
-				"cursor-pointer",
-				{
-					// Compact variant styling
-					"group bg-vscode-editor-background rounded relative overflow-hidden border border-vscode-toolbar-hoverBackground/30 hover:border-vscode-toolbar-hoverBackground/60":
-						isCompact,
-					// Full variant styling
-					"bg-vscode-list-activeSelectionBackground": !isCompact && isSelectionMode && isSelected,
-				},
+				"cursor-pointer group bg-vscode-editor-background rounded relative overflow-hidden hover:border-vscode-toolbar-hoverBackground/60",
 				className,
 			)}
 			onClick={handleClick}>
-			<div
-				className={cn("flex gap-2", {
-					"flex-col p-3 pt-1": isCompact,
-					"items-start p-3 ml-2": !isCompact,
-				})}>
+			<div className="flex gap-2 p-3">
 				{/* Selection checkbox - only in full variant */}
 				{!isCompact && isSelectionMode && (
 					<div
@@ -84,29 +70,15 @@ const TaskItem = ({
 
 				<div className="flex-1">
 					{/* Header with metadata */}
-					<TaskItemHeader
-						item={item}
-						variant={variant}
-						isSelectionMode={isSelectionMode}
-						t={t}
-						onDelete={onDelete}
-					/>
+					<TaskItemHeader item={item} isSelectionMode={isSelectionMode} onDelete={onDelete} />
 
 					{/* Task content */}
 					<div
-						className={cn("overflow-hidden whitespace-pre-wrap", {
-							"text-vscode-foreground": isCompact,
+						className={cn("overflow-hidden whitespace-pre-wrap text-vscode-foreground text-ellipsis", {
+							"text-base line-clamp-3": !isCompact,
+							"line-clamp-2": isCompact,
 						})}
-						style={{
-							fontSize: isCompact ? undefined : "var(--vscode-font-size)",
-							color: isCompact ? undefined : "var(--vscode-foreground)",
-							display: "-webkit-box",
-							WebkitLineClamp: isCompact ? 2 : 3,
-							WebkitBoxOrient: "vertical",
-							wordBreak: "break-word",
-							overflowWrap: "anywhere",
-						}}
-						data-testid={isCompact ? undefined : "task-content"}
+						data-testid="task-content"
 						{...(item.highlight ? { dangerouslySetInnerHTML: { __html: item.highlight } } : {})}>
 						{item.highlight ? undefined : item.task}
 					</div>
@@ -116,10 +88,7 @@ const TaskItem = ({
 
 					{/* Workspace info */}
 					{showWorkspace && item.workspace && (
-						<div
-							className={cn("flex flex-row gap-1 text-vscode-descriptionForeground text-xs", {
-								"mt-1": isCompact,
-							})}>
+						<div className="flex flex-row gap-1 text-vscode-descriptionForeground text-xs mt-1">
 							<span className="codicon codicon-folder scale-80" />
 							<span>{item.workspace}</span>
 						</div>

+ 42 - 97
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<TaskItemFooterProps> = ({ item, variant, isSelectionMode = false }) => {
-	const { t } = useAppTranslation()
-	const isCompact = variant === "compact"
+	return (
+		<div className="text-xs text-vscode-descriptionForeground flex justify-between items-center mt-1">
+			<div className="flex gap-2">
+				{!!(item.cacheReads || item.cacheWrites) && (
+					<span className="flex items-center" data-testid="cache-compact">
+						<i className="mr-1 codicon codicon-cloud-upload text-sm! text-vscode-descriptionForeground" />
+						<span className="inline-block mr-1">{formatLargeNumber(item.cacheWrites || 0)}</span>
+						<i className="mr-1 codicon codicon-cloud-download text-sm! text-vscode-descriptionForeground" />
+						<span>{formatLargeNumber(item.cacheReads || 0)}</span>
+					</span>
+				)}
 
-	const metadataIconWithTextAdjustStyle: React.CSSProperties = {
-		fontSize: "12px",
-		color: "var(--vscode-descriptionForeground)",
-		verticalAlign: "middle",
-		marginBottom: "-2px",
-		fontWeight: "bold",
-	}
+				{/* Full Tokens */}
+				{!!(item.tokensIn || item.tokensOut) && (
+					<span className="flex items-center gap-1">
+						<span data-testid="tokens-in-footer-compact">↑ {formatLargeNumber(item.tokensIn || 0)}</span>
+						<span data-testid="tokens-out-footer-compact">↓ {formatLargeNumber(item.tokensOut || 0)}</span>
+					</span>
+				)}
 
-	return (
-		<div
-			className={cn("text-xs text-vscode-descriptionForeground", {
-				"mt-2 flex items-center flex-wrap gap-x-2": isCompact,
-				"mt-1 flex justify-between items-end": !isCompact,
-			})}>
-			{isCompact ? (
-				<>
-					{/* Compact Cache */}
-					{!!item.cacheWrites && (
-						<span className="flex items-center gap-px" data-testid="cache-compact">
-							<i className="codicon codicon-database" style={metadataIconWithTextAdjustStyle} />
-							{formatLargeNumber(item.cacheWrites || 0)}
-							<i className="codicon codicon-arrow-right" style={metadataIconWithTextAdjustStyle} />
-							{formatLargeNumber(item.cacheReads || 0)}
-						</span>
-					)}
-					
-					{/* Compact Tokens */}
-					{(item.tokensIn || item.tokensOut) && (
-						<>
-							<span data-testid="tokens-in-footer-compact">
-								↑ {formatLargeNumber(item.tokensIn || 0)}
-							</span>
-							<span data-testid="tokens-out-footer-compact">
-								↓ {formatLargeNumber(item.tokensOut || 0)}
-							</span>
-						</>
-					)}
-					{/* Compact Cost */}
-					{!!item.totalCost && (
-						<span className="flex items-center">
-							<Coins className="inline-block size-[1em] mr-1" />
-							<span data-testid="cost-footer-compact">{"$" + item.totalCost.toFixed(2)}</span>
-						</span>
-					)}
-				</>
-			) : (
-				<>
-					<div className="flex flex-col gap-1">
-						{/* Cache Info */}
-						{!!item.cacheWrites && (
-							<div className="flex items-center flex-wrap gap-x-1">
-								<span className="font-medium">{t("history:cacheLabel")}</span>
-								<span className="flex items-center gap-px" data-testid="cache-writes">
-									<i className="codicon codicon-database" style={metadataIconWithTextAdjustStyle} />
-									<span className="font-medium">{formatLargeNumber(item.cacheWrites || 0)}</span>
-								</span>
-								<span className="flex items-center gap-px" data-testid="cache-reads">
-									<i className="codicon codicon-arrow-right" style={metadataIconWithTextAdjustStyle} />
-									<span className="font-medium">{formatLargeNumber(item.cacheReads || 0)}</span>
-								</span>
-							</div>
-						)}
-						
-						{/* Full Tokens */}
-						{(item.tokensIn || item.tokensOut) && (
-							<div className="flex items-center flex-wrap gap-x-1">
-								<span className="font-medium">{t("history:tokensLabel")}</span>
-								<span className="flex items-center gap-px" data-testid="tokens-in-footer-full">
-									<i className="codicon codicon-arrow-up" style={metadataIconWithTextAdjustStyle} />
-									<span className="font-medium">{formatLargeNumber(item.tokensIn || 0)}</span>
-								</span>
-								<span className="flex items-center gap-px" data-testid="tokens-out-footer-full">
-									<i className="codicon codicon-arrow-down" style={metadataIconWithTextAdjustStyle} />
-									<span className="font-medium">{formatLargeNumber(item.tokensOut || 0)}</span>
-								</span>
-							</div>
-						)}
-						{/* Full Cost */}
-						{!!item.totalCost && (
-							<div className="flex items-center flex-wrap gap-x-1">
-								<span className="font-medium">{t("history:apiCostLabel")}</span>
-								<span data-testid="cost-footer-full">{"$" + item.totalCost.toFixed(4)}</span>
-							</div>
-						)}
-					</div>
-					{/* Action Buttons for non-compact view */}
-					{!isSelectionMode && (
-						<div className="flex flex-row gap-0 items-center opacity-50 hover:opacity-100">
-							<CopyButton itemTask={item.task} />
-							<ExportButton itemId={item.id} />
-						</div>
-					)}
-				</>
+				{/* Full Cost */}
+				{!!item.totalCost && (
+					<span className="flex items-center">
+						<Coins className="inline-block size-[1em] mr-1" />
+						<span data-testid="cost-footer-compact">{"$" + item.totalCost.toFixed(2)}</span>
+					</span>
+				)}
+
+				{!!item.size && (
+					<span className="flex items-center">
+						<FileIcon className="inline-block size-[1em] mr-1" />
+						<span data-testid="size-footer-compact">{prettyBytes(item.size)}</span>
+					</span>
+				)}
+			</div>
+
+			{/* Action Buttons for non-compact view */}
+			{!isSelectionMode && (
+				<div className="flex flex-row gap-0 items-center opacity-50 hover:opacity-100">
+					<CopyButton itemTask={item.task} />
+					{variant === "full" && <ExportButton itemId={item.id} />}
+				</div>
 			)}
 		</div>
 	)

+ 10 - 47
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<TaskItemHeaderProps> = ({ 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<TaskItemHeaderProps> = ({ item, isSelectionMode, onDelete }) => {
 	return (
-		<div className="flex justify-between items-center pb-0">
+		<div
+			className={cn("flex justify-between items-center", {
+				// this is to balance out the margin when we don't have a delete button
+				// because the delete button sorta pushes the date up due to its size
+				"mb-1": !onDelete,
+			})}>
 			<div className="flex items-center flex-wrap gap-x-2 text-xs">
 				<span className="text-vscode-descriptionForeground font-medium text-sm uppercase">
 					{formatDate(item.ts)}
@@ -44,27 +27,7 @@ const TaskItemHeader: React.FC<TaskItemHeaderProps> = ({ item, variant, isSelect
 			{/* Action Buttons */}
 			{!isSelectionMode && (
 				<div className="flex flex-row gap-0 items-center opacity-20 group-hover:opacity-50 hover:opacity-100">
-					{isCompact ? (
-						<CopyButton itemTask={item.task} />
-					) : (
-						<>
-							{onDelete && (
-								<Button
-									variant="ghost"
-									size="icon"
-									title={t("history:deleteTaskTitle")}
-									data-testid="delete-task-button"
-									onClick={handleDeleteClick}>
-									<span className="codicon codicon-trash" style={actionIconStyle} />
-								</Button>
-							)}
-							{!isCompact && item.size && (
-								<span className="text-vscode-descriptionForeground ml-1 text-sm">
-									{prettyBytes(item.size)}
-								</span>
-							)}
-						</>
-					)}
+					{onDelete && <DeleteButton itemId={item.id} onDelete={onDelete} />}
 				</div>
 			)}
 		</div>

+ 87 - 0
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<string, any>) => {
+			const translations: Record<string, string> = {
+				"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(<BatchDeleteTaskDialog taskIds={mockTaskIds} open={true} onOpenChange={mockOnOpenChange} />)
+
+		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(<BatchDeleteTaskDialog taskIds={mockTaskIds} open={true} onOpenChange={mockOnOpenChange} />)
+
+		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(<BatchDeleteTaskDialog taskIds={mockTaskIds} open={true} onOpenChange={mockOnOpenChange} />)
+
+		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(<BatchDeleteTaskDialog taskIds={[]} open={true} onOpenChange={mockOnOpenChange} />)
+
+		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(<BatchDeleteTaskDialog taskIds={singleTaskId} open={true} onOpenChange={mockOnOpenChange} />)
+
+		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(<BatchDeleteTaskDialog taskIds={mockTaskIds} open={true} onOpenChange={mockOnOpenChange} />)
+
+		const deleteButton = screen.getByText("Delete 3 items")
+		const trashIcon = deleteButton.querySelector(".codicon-trash")
+		expect(trashIcon).toBeInTheDocument()
+	})
+})

+ 31 - 0
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(<CopyButton itemTask="Test task content" />)
+
+		const copyButton = screen.getByRole("button")
+		fireEvent.click(copyButton)
+
+		expect(mockCopy).toHaveBeenCalledWith("Test task content")
+	})
+})

+ 20 - 0
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(<DeleteButton itemId="test-id" onDelete={onDelete} />)
+
+		const deleteButton = screen.getByRole("button")
+		fireEvent.click(deleteButton)
+
+		expect(onDelete).toHaveBeenCalledWith("test-id")
+	})
+})

+ 143 - 0
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<string, string> = {
+				"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<typeof useKeyPress>
+
+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(<DeleteTaskDialog taskId={mockTaskId} open={true} onOpenChange={mockOnOpenChange} />)
+
+		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(<DeleteTaskDialog taskId={mockTaskId} open={true} onOpenChange={mockOnOpenChange} />)
+
+		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(<DeleteTaskDialog taskId={mockTaskId} open={true} onOpenChange={mockOnOpenChange} />)
+
+		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(<DeleteTaskDialog taskId="" open={true} onOpenChange={mockOnOpenChange} />)
+
+		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(<DeleteTaskDialog taskId={mockTaskId} open={true} onOpenChange={mockOnOpenChange} />)
+
+		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(<DeleteTaskDialog taskId="" open={true} onOpenChange={mockOnOpenChange} />)
+
+		expect(vscode.postMessage).not.toHaveBeenCalled()
+		expect(mockOnOpenChange).not.toHaveBeenCalled()
+	})
+
+	it("calls onOpenChange on escape key", () => {
+		render(<DeleteTaskDialog taskId={mockTaskId} open={true} onOpenChange={mockOnOpenChange} />)
+
+		// 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(<DeleteTaskDialog taskId={mockTaskId} open={true} onOpenChange={mockOnOpenChange} />)
+
+		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(
+			<DeleteTaskDialog taskId={mockTaskId} open={true} onOpenChange={mockOnOpenChange} />,
+		)
+
+		expect(vscode.postMessage).not.toHaveBeenCalled()
+
+		// Then simulate Enter key press
+		mockUseKeyPress.mockReturnValue([true, null])
+		rerender(<DeleteTaskDialog taskId={mockTaskId} open={true} onOpenChange={mockOnOpenChange} />)
+
+		expect(vscode.postMessage).toHaveBeenCalledTimes(1)
+		expect(vscode.postMessage).toHaveBeenCalledWith({
+			type: "deleteTaskWithId",
+			text: mockTaskId,
+		})
+	})
+})

+ 28 - 0
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(<ExportButton itemId="1" />)
+
+		const exportButton = screen.getByRole("button")
+		fireEvent.click(exportButton)
+
+		expect(vscode.postMessage).toHaveBeenCalledWith({
+			type: "exportTaskWithId",
+			text: "1",
+		})
+	})
+})

+ 199 - 0
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 }) => (
+			<div data-testid={`task-item-${item.id}`} data-variant={variant}>
+				{item.task}
+			</div>
+		)),
+	}
+})
+
+import { useTaskSearch } from "../useTaskSearch"
+import TaskItem from "../TaskItem"
+
+const mockUseTaskSearch = useTaskSearch as jest.MockedFunction<typeof useTaskSearch>
+const mockTaskItem = TaskItem as jest.MockedFunction<typeof TaskItem>
+
+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(<HistoryPreview />)
+
+		// 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(<HistoryPreview />)
+
+		// 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(<HistoryPreview />)
+
+		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(<HistoryPreview />)
+
+		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(<HistoryPreview />)
+
+		// 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(<HistoryPreview />)
+
+		expect(container.firstChild).toHaveClass("flex", "flex-col", "gap-3")
+	})
+})

+ 20 - 295
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 }) => (
-		<input
-			type="checkbox"
-			data-testid={props["data-testid"] || "mock-checkbox"}
-			checked={checked}
-			onChange={(e) => onCheckedChange(e.target.checked)}
-			{...props}
-		/>
-	)),
-}))
-jest.mock("lucide-react", () => ({
-	DollarSign: () => <span data-testid="dollar-sign">$</span>,
-}))
-jest.mock("react-virtuoso", () => ({
-	Virtuoso: ({ data, itemContent }: any) => (
-		<div data-testid="virtuoso-container">
-			{data.map((item: any, index: number) => (
-				<div key={item.id} data-testid={`virtuoso-item-${item.id}`}>
-					{itemContent(index, item)}
-				</div>
-			))}
-		</div>
-	),
+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(<HistoryView onDone={onDone} />)
-
-		// 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(<HistoryView onDone={onDone} />)
-
-		// 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(<HistoryView onDone={onDone} />)
 
-		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(<HistoryView onDone={onDone} />)
 
-		// 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(<HistoryView onDone={onDone} />)
+		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(<HistoryView onDone={onDone} />)
-
-			// 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(<HistoryView onDone={onDone} />)
-
-			// 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(<HistoryView onDone={onDone} />)
-
-		// 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(<HistoryView onDone={onDone} />)
-
-		// 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(<HistoryView onDone={onDone} />)
-
-		// 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(<HistoryView onDone={onDone} />)
-
-		// 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(<HistoryView onDone={onDone} />)
-
-		// 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()
 	})
 })

+ 75 - 73
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: () => <span data-testid="dollar-sign">$</span>,
-	Coins: () => <span data-testid="coins-icon" />, // Mock for Coins icon used in TaskItemFooter compact
-}))
-jest.mock("../CopyButton", () => ({
-	CopyButton: jest.fn(() => <button data-testid="mock-copy-button">Copy</button>),
-}))
-jest.mock("../ExportButton", () => ({
-	ExportButton: jest.fn(() => <button data-testid="mock-export-button">Export</button>),
+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(<TaskItem item={mockTask} variant="compact" />)
-
-		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(<TaskItem item={mockTask} variant="full" />)
-
-		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(<TaskItem item={mockTask} variant="compact" showWorkspace={true} />)
+	it("renders task information", () => {
+		render(
+			<TaskItem
+				item={mockTask}
+				variant="full"
+				isSelected={false}
+				onToggleSelection={jest.fn()}
+				isSelectionMode={false}
+			/>,
+		)
 
-		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(<TaskItem item={mockTask} variant="compact" />)
+	it("handles selection in selection mode", () => {
+		const onToggleSelection = jest.fn()
+		render(
+			<TaskItem
+				item={mockTask}
+				variant="full"
+				isSelected={false}
+				onToggleSelection={onToggleSelection}
+				isSelectionMode={true}
+			/>,
+		)
 
-		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(
 			<TaskItem
 				item={mockTask}
 				variant="full"
-				isSelectionMode={true}
 				isSelected={false}
-				onToggleSelection={mockToggleSelection}
+				onToggleSelection={jest.fn()}
+				isSelectionMode={false}
 			/>,
 		)
 
-		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(<TaskItem item={mockTask} variant="full" onDelete={mockOnDelete} />)
-
-		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(
+			<TaskItem
+				item={mockTaskWithCache}
+				variant="full"
+				isSelected={false}
+				onToggleSelection={jest.fn()}
+				isSelectionMode={false}
+			/>,
+		)
 
-		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(<TaskItem item={taskWithCache} variant="full" />)
+		render(
+			<TaskItem
+				item={mockTaskWithoutCache}
+				variant="full"
+				isSelected={false}
+				onToggleSelection={jest.fn()}
+				isSelectionMode={false}
+			/>,
+		)
 
-		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()
 	})
 })

+ 72 - 0
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(<TaskItemFooter item={mockItem} variant="full" />)
+
+		// 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(<TaskItemFooter item={mockItem} variant="full" />)
+
+		// The component shows $0.00 for small amounts, not the exact value
+		expect(screen.getByText("$0.00")).toBeInTheDocument()
+	})
+
+	it("shows action buttons", () => {
+		render(<TaskItemFooter item={mockItem} variant="full" />)
+
+		// 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(<TaskItemFooter item={mockItemWithCache} variant="full" />)
+
+		// 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(<TaskItemFooter item={mockItemWithoutCache} variant="full" />)
+
+		// Cache section should not be present
+		expect(screen.queryByTestId("cache-compact")).not.toBeInTheDocument()
+	})
+})

+ 34 - 0
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 item={mockItem} isSelectionMode={false} onDelete={jest.fn()} />)
+
+		// 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(<TaskItemHeader item={mockItem} isSelectionMode={false} onDelete={jest.fn()} />)
+
+		expect(screen.getByRole("button")).toBeInTheDocument()
+	})
+})

+ 285 - 0
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) => `<mark>${text}</mark>`),
+}))
+
+import { useExtensionState } from "@/context/ExtensionStateContext"
+
+const mockUseExtensionState = useExtensionState as jest.MockedFunction<typeof useExtensionState>
+
+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("<mark>Create a React component</mark>")
+	})
+
+	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")
+	})
+})

+ 13 - 1
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"
+	}
 }

+ 13 - 1
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"
+	}
 }

+ 13 - 9
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"
+	}
 }

+ 13 - 1
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"
+	}
 }

+ 13 - 1
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"
+	}
 }

+ 13 - 9
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": "सबसे प्रासंगिक"
+	}
 }

+ 13 - 2
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"
+	}
 }

+ 14 - 10
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"
+	}
 }

+ 13 - 9
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": "最も関連性の高い"
+	}
 }

+ 13 - 9
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": "관련성 높은순"
+	}
 }

+ 13 - 9
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"
+	}
 }

+ 14 - 10
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"
+	}
 }

+ 14 - 10
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"
+	}
 }

+ 13 - 9
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": "Наиболее релевантные"
+	}
 }

+ 13 - 9
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"
+	}
 }

+ 15 - 11
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"
+	}
 }

+ 20 - 16
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": "最相关"
+	}
 }

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