mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
refactor: more consistent history UI (#4684)
This commit is contained in:
parent
f51f89454f
commit
e618fa958c
36 changed files with 1436 additions and 763 deletions
|
|
@ -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
webview-ui/src/components/history/DeleteButton.tsx
Normal file
38
webview-ui/src/components/history/DeleteButton.tsx
Normal file
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
const metadataIconWithTextAdjustStyle: React.CSSProperties = {
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
verticalAlign: "middle",
|
||||
marginBottom: "-2px",
|
||||
fontWeight: "bold",
|
||||
}
|
||||
|
||||
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>
|
||||
)}
|
||||
</>
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
|
@ -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", () => {
|
||||
it("renders the history interface", () => {
|
||||
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()
|
||||
// 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 search functionality", () => {
|
||||
// Setup clipboard mock that resolves immediately
|
||||
const mockClipboard = {
|
||||
writeText: jest.fn().mockResolvedValue(undefined),
|
||||
}
|
||||
Object.assign(navigator, { clipboard: mockClipboard })
|
||||
|
||||
it("calls onDone when done button is clicked", () => {
|
||||
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")
|
||||
const doneButton = screen.getByText("history:done")
|
||||
fireEvent.click(doneButton)
|
||||
|
||||
// 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 () => {
|
||||
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()
|
||||
})
|
||||
|
||||
it("handles task selection", () => {
|
||||
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} />)
|
||||
|
||||
// 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()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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} />)
|
||||
|
||||
expect(screen.getByText("test-workspace")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("handles click events correctly", () => {
|
||||
render(<TaskItem item={mockTask} variant="compact" />)
|
||||
|
||||
fireEvent.click(screen.getByText("Test task content"))
|
||||
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "showTaskWithId",
|
||||
text: "test-task-1",
|
||||
})
|
||||
})
|
||||
|
||||
it("handles selection mode correctly", () => {
|
||||
const mockToggleSelection = jest.fn()
|
||||
it("renders task information", () => {
|
||||
render(
|
||||
<TaskItem
|
||||
item={mockTask}
|
||||
variant="full"
|
||||
isSelectionMode={true}
|
||||
isSelected={false}
|
||||
onToggleSelection={mockToggleSelection}
|
||||
onToggleSelection={jest.fn()}
|
||||
isSelectionMode={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText("Test task")).toBeInTheDocument()
|
||||
expect(screen.getByText("$0.00")).toBeInTheDocument() // Component shows $0.00 for small amounts
|
||||
})
|
||||
|
||||
it("handles selection in selection mode", () => {
|
||||
const onToggleSelection = jest.fn()
|
||||
render(
|
||||
<TaskItem
|
||||
item={mockTask}
|
||||
variant="full"
|
||||
isSelected={false}
|
||||
onToggleSelection={onToggleSelection}
|
||||
isSelectionMode={true}
|
||||
/>,
|
||||
)
|
||||
|
||||
const checkbox = screen.getByRole("checkbox")
|
||||
expect(checkbox).toBeInTheDocument()
|
||||
expect(checkbox).not.toBeChecked()
|
||||
fireEvent.click(checkbox)
|
||||
|
||||
fireEvent.click(screen.getByTestId("task-item-test-task-1"))
|
||||
|
||||
expect(mockToggleSelection).toHaveBeenCalledWith("test-task-1", true)
|
||||
expect(vscode.postMessage).not.toHaveBeenCalled()
|
||||
expect(onToggleSelection).toHaveBeenCalledWith("1", true)
|
||||
})
|
||||
|
||||
it("shows delete button in full variant when not in selection mode", () => {
|
||||
const mockOnDelete = jest.fn()
|
||||
render(<TaskItem item={mockTask} variant="full" onDelete={mockOnDelete} />)
|
||||
it("shows action buttons", () => {
|
||||
render(
|
||||
<TaskItem
|
||||
item={mockTask}
|
||||
variant="full"
|
||||
isSelected={false}
|
||||
onToggleSelection={jest.fn()}
|
||||
isSelectionMode={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
const deleteButton = screen.getByTestId("delete-task-button")
|
||||
expect(deleteButton).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(deleteButton)
|
||||
|
||||
expect(mockOnDelete).toHaveBeenCalledWith("test-task-1")
|
||||
// Should show copy and export buttons
|
||||
expect(screen.getByTestId("copy-prompt-button")).toBeInTheDocument()
|
||||
expect(screen.getByTestId("export")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("displays cache information when available", () => {
|
||||
const taskWithCache: HistoryItem = {
|
||||
it("displays cache information when present", () => {
|
||||
const mockTaskWithCache = {
|
||||
...mockTask,
|
||||
cacheWrites: 25,
|
||||
cacheReads: 10,
|
||||
cacheWrites: 5,
|
||||
}
|
||||
|
||||
render(<TaskItem item={taskWithCache} variant="full" />)
|
||||
render(
|
||||
<TaskItem
|
||||
item={mockTaskWithCache}
|
||||
variant="full"
|
||||
isSelected={false}
|
||||
onToggleSelection={jest.fn()}
|
||||
isSelectionMode={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId("cache-writes")).toHaveTextContent("25")
|
||||
expect(screen.getByTestId("cache-reads")).toHaveTextContent("10")
|
||||
// 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("does not display cache information when not present", () => {
|
||||
const mockTaskWithoutCache = {
|
||||
...mockTask,
|
||||
cacheReads: 0,
|
||||
cacheWrites: 0,
|
||||
}
|
||||
|
||||
render(
|
||||
<TaskItem
|
||||
item={mockTaskWithoutCache}
|
||||
variant="full"
|
||||
isSelected={false}
|
||||
onToggleSelection={jest.fn()}
|
||||
isSelectionMode={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
// Cache section should not be present
|
||||
expect(screen.queryByTestId("cache-compact")).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "सबसे प्रासंगिक"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "最も関連性の高い"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "관련성 높은순"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "Наиболее релевантные"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "最相关"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "最相關"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue