Skip to content

Commit e618fa9

Browse files
authored
refactor: more consistent history UI (#4684)
1 parent f51f894 commit e618fa9

36 files changed

+1430
-757
lines changed

webview-ui/src/components/history/CopyButton.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@ import { useCallback } from "react"
22

33
import { useClipboard } from "@/components/ui/hooks"
44
import { Button } from "@/components/ui"
5-
import { cn } from "@/lib/utils"
65
import { useAppTranslation } from "@/i18n/TranslationContext"
6+
import { cn } from "@/lib/utils"
77

88
type CopyButtonProps = {
99
itemTask: string
10-
className?: string
1110
}
1211

13-
export const CopyButton = ({ itemTask, className }: CopyButtonProps) => {
12+
export const CopyButton = ({ itemTask }: CopyButtonProps) => {
1413
const { isCopied, copy } = useClipboard()
1514
const { t } = useAppTranslation()
1615

@@ -31,8 +30,8 @@ export const CopyButton = ({ itemTask, className }: CopyButtonProps) => {
3130
size="icon"
3231
title={t("history:copyPrompt")}
3332
onClick={onCopy}
34-
data-testid="copy-prompt-button"
35-
className={cn("opacity-50 hover:opacity-100", className)}>
33+
className="group-hover:opacity-100 opacity-50 transition-opacity"
34+
data-testid="copy-prompt-button">
3635
<span className={cn("codicon scale-80", { "codicon-check": isCopied, "codicon-copy": !isCopied })} />
3736
</Button>
3837
)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { useCallback } from "react"
2+
3+
import { Button } from "@/components/ui"
4+
import { useAppTranslation } from "@/i18n/TranslationContext"
5+
import { vscode } from "@/utils/vscode"
6+
7+
type DeleteButtonProps = {
8+
itemId: string
9+
onDelete?: (taskId: string) => void
10+
}
11+
12+
export const DeleteButton = ({ itemId, onDelete }: DeleteButtonProps) => {
13+
const { t } = useAppTranslation()
14+
15+
const handleDeleteClick = useCallback(
16+
(e: React.MouseEvent) => {
17+
e.stopPropagation()
18+
if (e.shiftKey) {
19+
vscode.postMessage({ type: "deleteTaskWithId", text: itemId })
20+
} else if (onDelete) {
21+
onDelete(itemId)
22+
}
23+
},
24+
[itemId, onDelete],
25+
)
26+
27+
return (
28+
<Button
29+
variant="ghost"
30+
size="icon"
31+
title={t("history:deleteTaskTitle")}
32+
data-testid="delete-task-button"
33+
onClick={handleDeleteClick}
34+
className="group-hover:opacity-100 opacity-50 transition-opacity">
35+
<span className="codicon codicon-trash size-4 align-middle text-vscode-descriptionForeground" />
36+
</Button>
37+
)
38+
}
Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
11
import { vscode } from "@/utils/vscode"
22
import { Button } from "@/components/ui"
33
import { useAppTranslation } from "@/i18n/TranslationContext"
4+
import { useCallback } from "react"
45

56
export const ExportButton = ({ itemId }: { itemId: string }) => {
67
const { t } = useAppTranslation()
78

9+
const handleExportClick = useCallback(
10+
(e: React.MouseEvent) => {
11+
e.stopPropagation()
12+
vscode.postMessage({ type: "exportTaskWithId", text: itemId })
13+
},
14+
[itemId],
15+
)
16+
817
return (
918
<Button
1019
data-testid="export"
1120
variant="ghost"
1221
size="icon"
1322
title={t("history:exportTask")}
14-
onClick={(e) => {
15-
e.stopPropagation()
16-
vscode.postMessage({ type: "exportTaskWithId", text: itemId })
17-
}}>
18-
<span className="codicon codicon-desktop-download" />
23+
className="group-hover:opacity-100 opacity-50 transition-opacity"
24+
onClick={handleExportClick}>
25+
<span className="codicon codicon-desktop-download scale-80" />
1926
</Button>
2027
)
2128
}

webview-ui/src/components/history/HistoryView.tsx

Lines changed: 75 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ import { DeleteTaskDialog } from "./DeleteTaskDialog"
33
import { BatchDeleteTaskDialog } from "./BatchDeleteTaskDialog"
44
import { Virtuoso } from "react-virtuoso"
55

6-
import { VSCodeTextField, VSCodeRadioGroup, VSCodeRadio } from "@vscode/webview-ui-toolkit/react"
6+
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
77

8-
import { cn } from "@/lib/utils"
9-
import { Button, Checkbox } from "@/components/ui"
8+
import { Button, Checkbox, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui"
109
import { useAppTranslation } from "@/i18n/TranslationContext"
1110

1211
import { Tab, TabContent, TabHeader } from "../common/Tab"
@@ -95,7 +94,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
9594
</div>
9695
<div className="flex flex-col gap-2">
9796
<VSCodeTextField
98-
style={{ width: "100%" }}
97+
className="w-full"
9998
placeholder={t("history:searchPlaceholder")}
10099
value={searchQuery}
101100
data-testid="history-search-input"
@@ -107,62 +106,83 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
107106
setSortOption("mostRelevant")
108107
}
109108
}}>
110-
<div
111-
slot="start"
112-
className="codicon codicon-search"
113-
style={{ fontSize: 13, marginTop: 2.5, opacity: 0.8 }}
114-
/>
109+
<div slot="start" className="codicon codicon-search mt-0.5 opacity-80 text-sm!" />
115110
{searchQuery && (
116111
<div
117-
className="input-icon-button codicon codicon-close"
112+
className="input-icon-button codicon codicon-close flex justify-center items-center h-full"
118113
aria-label="Clear search"
119114
onClick={() => setSearchQuery("")}
120115
slot="end"
121-
style={{
122-
display: "flex",
123-
justifyContent: "center",
124-
alignItems: "center",
125-
height: "100%",
126-
}}
127116
/>
128117
)}
129118
</VSCodeTextField>
130-
<VSCodeRadioGroup
131-
style={{ display: "flex", flexWrap: "wrap" }}
132-
value={sortOption}
133-
role="radiogroup"
134-
onChange={(e) => setSortOption((e.target as HTMLInputElement).value as SortOption)}>
135-
<VSCodeRadio value="newest" data-testid="radio-newest">
136-
{t("history:newest")}
137-
</VSCodeRadio>
138-
<VSCodeRadio value="oldest" data-testid="radio-oldest">
139-
{t("history:oldest")}
140-
</VSCodeRadio>
141-
<VSCodeRadio value="mostExpensive" data-testid="radio-most-expensive">
142-
{t("history:mostExpensive")}
143-
</VSCodeRadio>
144-
<VSCodeRadio value="mostTokens" data-testid="radio-most-tokens">
145-
{t("history:mostTokens")}
146-
</VSCodeRadio>
147-
<VSCodeRadio
148-
value="mostRelevant"
149-
disabled={!searchQuery}
150-
data-testid="radio-most-relevant"
151-
style={{ opacity: searchQuery ? 1 : 0.5 }}>
152-
{t("history:mostRelevant")}
153-
</VSCodeRadio>
154-
</VSCodeRadioGroup>
155-
156-
<div className="flex items-center gap-2">
157-
<Checkbox
158-
id="show-all-workspaces-view"
159-
checked={showAllWorkspaces}
160-
onCheckedChange={(checked) => setShowAllWorkspaces(checked === true)}
161-
variant="description"
162-
/>
163-
<label htmlFor="show-all-workspaces-view" className="text-vscode-foreground cursor-pointer">
164-
{t("history:showAllWorkspaces")}
165-
</label>
119+
<div className="flex gap-2">
120+
<Select
121+
value={showAllWorkspaces ? "all" : "current"}
122+
onValueChange={(value) => setShowAllWorkspaces(value === "all")}>
123+
<SelectTrigger className="flex-1">
124+
<SelectValue>
125+
{t("history:workspace.prefix")}{" "}
126+
{t(`history:workspace.${showAllWorkspaces ? "all" : "current"}`)}
127+
</SelectValue>
128+
</SelectTrigger>
129+
<SelectContent>
130+
<SelectItem value="current">
131+
<div className="flex items-center gap-2">
132+
<span className="codicon codicon-folder" />
133+
{t("history:workspace.current")}
134+
</div>
135+
</SelectItem>
136+
<SelectItem value="all">
137+
<div className="flex items-center gap-2">
138+
<span className="codicon codicon-folder-opened" />
139+
{t("history:workspace.all")}
140+
</div>
141+
</SelectItem>
142+
</SelectContent>
143+
</Select>
144+
<Select value={sortOption} onValueChange={(value) => setSortOption(value as SortOption)}>
145+
<SelectTrigger className="flex-1">
146+
<SelectValue>
147+
{t("history:sort.prefix")} {t(`history:sort.${sortOption}`)}
148+
</SelectValue>
149+
</SelectTrigger>
150+
<SelectContent>
151+
<SelectItem value="newest" data-testid="select-newest">
152+
<div className="flex items-center gap-2">
153+
<span className="codicon codicon-arrow-down" />
154+
{t("history:newest")}
155+
</div>
156+
</SelectItem>
157+
<SelectItem value="oldest" data-testid="select-oldest">
158+
<div className="flex items-center gap-2">
159+
<span className="codicon codicon-arrow-up" />
160+
{t("history:oldest")}
161+
</div>
162+
</SelectItem>
163+
<SelectItem value="mostExpensive" data-testid="select-most-expensive">
164+
<div className="flex items-center gap-2">
165+
<span className="codicon codicon-credit-card" />
166+
{t("history:mostExpensive")}
167+
</div>
168+
</SelectItem>
169+
<SelectItem value="mostTokens" data-testid="select-most-tokens">
170+
<div className="flex items-center gap-2">
171+
<span className="codicon codicon-symbol-numeric" />
172+
{t("history:mostTokens")}
173+
</div>
174+
</SelectItem>
175+
<SelectItem
176+
value="mostRelevant"
177+
disabled={!searchQuery}
178+
data-testid="select-most-relevant">
179+
<div className="flex items-center gap-2">
180+
<span className="codicon codicon-search" />
181+
{t("history:mostRelevant")}
182+
</div>
183+
</SelectItem>
184+
</SelectContent>
185+
</Select>
166186
</div>
167187

168188
{/* Select all control in selection mode */}
@@ -193,10 +213,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
193213

194214
<TabContent className="p-0">
195215
<Virtuoso
196-
style={{
197-
flexGrow: 1,
198-
overflowY: "scroll",
199-
}}
216+
className="flex-1 overflow-y-scroll"
200217
data={tasks}
201218
data-testid="virtuoso-container"
202219
initialTopMostItemIndex={0}
@@ -205,7 +222,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
205222
<div {...props} ref={ref} data-testid="virtuoso-item-list" />
206223
)),
207224
}}
208-
itemContent={(index, item) => (
225+
itemContent={(_index, item) => (
209226
<TaskItem
210227
key={item.id}
211228
item={item}
@@ -215,9 +232,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
215232
isSelected={selectedTaskIds.includes(item.id)}
216233
onToggleSelection={toggleTaskSelection}
217234
onDelete={setDeleteTaskId}
218-
className={cn({
219-
"border-b border-vscode-panel-border": index < tasks.length - 1,
220-
})}
235+
className="m-2 mr-0"
221236
/>
222237
)}
223238
/>

webview-ui/src/components/history/TaskItem.tsx

Lines changed: 9 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type { HistoryItem } from "@roo-code/types"
44
import { vscode } from "@/utils/vscode"
55
import { cn } from "@/lib/utils"
66
import { Checkbox } from "@/components/ui/checkbox"
7-
import { useAppTranslation } from "@/i18n/TranslationContext"
87

98
import TaskItemHeader from "./TaskItemHeader"
109
import TaskItemFooter from "./TaskItemFooter"
@@ -34,8 +33,6 @@ const TaskItem = ({
3433
onDelete,
3534
className,
3635
}: TaskItemProps) => {
37-
const { t } = useAppTranslation()
38-
3936
const handleClick = () => {
4037
if (isSelectionMode && onToggleSelection) {
4138
onToggleSelection(item.id, !isSelected)
@@ -49,24 +46,13 @@ const TaskItem = ({
4946
return (
5047
<div
5148
key={item.id}
52-
data-testid={isCompact ? undefined : `task-item-${item.id}`}
49+
data-testid={`task-item-${item.id}`}
5350
className={cn(
54-
"cursor-pointer",
55-
{
56-
// Compact variant styling
57-
"group bg-vscode-editor-background rounded relative overflow-hidden border border-vscode-toolbar-hoverBackground/30 hover:border-vscode-toolbar-hoverBackground/60":
58-
isCompact,
59-
// Full variant styling
60-
"bg-vscode-list-activeSelectionBackground": !isCompact && isSelectionMode && isSelected,
61-
},
51+
"cursor-pointer group bg-vscode-editor-background rounded relative overflow-hidden hover:border-vscode-toolbar-hoverBackground/60",
6252
className,
6353
)}
6454
onClick={handleClick}>
65-
<div
66-
className={cn("flex gap-2", {
67-
"flex-col p-3 pt-1": isCompact,
68-
"items-start p-3 ml-2": !isCompact,
69-
})}>
55+
<div className="flex gap-2 p-3">
7056
{/* Selection checkbox - only in full variant */}
7157
{!isCompact && isSelectionMode && (
7258
<div
@@ -84,29 +70,15 @@ const TaskItem = ({
8470

8571
<div className="flex-1">
8672
{/* Header with metadata */}
87-
<TaskItemHeader
88-
item={item}
89-
variant={variant}
90-
isSelectionMode={isSelectionMode}
91-
t={t}
92-
onDelete={onDelete}
93-
/>
73+
<TaskItemHeader item={item} isSelectionMode={isSelectionMode} onDelete={onDelete} />
9474

9575
{/* Task content */}
9676
<div
97-
className={cn("overflow-hidden whitespace-pre-wrap", {
98-
"text-vscode-foreground": isCompact,
77+
className={cn("overflow-hidden whitespace-pre-wrap text-vscode-foreground text-ellipsis", {
78+
"text-base line-clamp-3": !isCompact,
79+
"line-clamp-2": isCompact,
9980
})}
100-
style={{
101-
fontSize: isCompact ? undefined : "var(--vscode-font-size)",
102-
color: isCompact ? undefined : "var(--vscode-foreground)",
103-
display: "-webkit-box",
104-
WebkitLineClamp: isCompact ? 2 : 3,
105-
WebkitBoxOrient: "vertical",
106-
wordBreak: "break-word",
107-
overflowWrap: "anywhere",
108-
}}
109-
data-testid={isCompact ? undefined : "task-content"}
81+
data-testid="task-content"
11082
{...(item.highlight ? { dangerouslySetInnerHTML: { __html: item.highlight } } : {})}>
11183
{item.highlight ? undefined : item.task}
11284
</div>
@@ -116,10 +88,7 @@ const TaskItem = ({
11688

11789
{/* Workspace info */}
11890
{showWorkspace && item.workspace && (
119-
<div
120-
className={cn("flex flex-row gap-1 text-vscode-descriptionForeground text-xs", {
121-
"mt-1": isCompact,
122-
})}>
91+
<div className="flex flex-row gap-1 text-vscode-descriptionForeground text-xs mt-1">
12392
<span className="codicon codicon-folder scale-80" />
12493
<span>{item.workspace}</span>
12594
</div>

0 commit comments

Comments
 (0)