Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions webview-ui/src/components/history/CopyButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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>
)
Expand Down
38 changes: 38 additions & 0 deletions webview-ui/src/components/history/DeleteButton.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
}
17 changes: 12 additions & 5 deletions webview-ui/src/components/history/ExportButton.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
}
135 changes: 75 additions & 60 deletions webview-ui/src/components/history/HistoryView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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 */}
Expand Down Expand Up @@ -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}
Expand All @@ -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}
Expand All @@ -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"
/>
)}
/>
Expand Down
49 changes: 9 additions & 40 deletions webview-ui/src/components/history/TaskItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -34,8 +33,6 @@ const TaskItem = ({
onDelete,
className,
}: TaskItemProps) => {
const { t } = useAppTranslation()

const handleClick = () => {
if (isSelectionMode && onToggleSelection) {
onToggleSelection(item.id, !isSelected)
Expand All @@ -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
Expand All @@ -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>
Expand All @@ -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>
Expand Down
Loading
Loading