Skip to content

Commit c980e25

Browse files
author
Eric Wheeler
committed
refactor: Unify history item UI with TaskItem and TaskItemHeader
Introduces `TaskItem.tsx` and `TaskItemHeader.tsx` to centralize and standardize the rendering of history entries. `TaskItem` handles the overall structure for "compact" (Preview) and "full" (HistoryView) variants. `TaskItemHeader` consolidates all metadata (timestamp, tokens, cost, cache, file size) into a single, consistent line above the task content, enhancing visual clarity and reducing UI clutter. This refactor significantly simplifies `HistoryPreview.tsx` and `HistoryView.tsx`. Approximately 314 lines of previous rendering logic were removed from these components and replaced by 242 lines in the new, focused, and reusable `TaskItem` and `TaskItemHeader` components, resulting in a net reduction and improved maintainability. Most importantly, rendering logic happens in one place. Key UI Changes: - Metadata (timestamp, tokens, cost, cache, file size) now displayed inline on a single header row in both variants. - Removed explicit "Tokens:" and "API Cost:" labels for a cleaner look. - Action buttons (Copy, Export, Delete) in the full view are now aligned with the metadata header. - File size is displayed in the header for the "full" variant only. - Workspace information is no longer displayed in the "compact" preview. Component Changes: - Created `webview-ui/src/components/history/TaskItem.tsx` (125 lines) - Created `webview-ui/src/components/history/TaskItemHeader.tsx` (117 lines) - Modified `webview-ui/src/components/history/HistoryPreview.tsx` (-65 lines, +3 lines) - Modified `webview-ui/src/components/history/HistoryView.tsx` (-249 lines, +3 lines) - Uses `HistoryItem` type for standardized data handling. Fixes: #4018 Signed-off-by: Eric Wheeler <[email protected]>
1 parent 1e5bf74 commit c980e25

File tree

4 files changed

+264
-298
lines changed

4 files changed

+264
-298
lines changed

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

Lines changed: 11 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,21 @@
11
import { memo } from "react"
22

3-
import { vscode } from "@/utils/vscode"
4-
import { formatLargeNumber, formatDate } from "@/utils/format"
5-
6-
import { CopyButton } from "./CopyButton"
73
import { useTaskSearch } from "./useTaskSearch"
8-
9-
import { Coins } from "lucide-react"
4+
import TaskItem from "./TaskItem"
105

116
const HistoryPreview = () => {
12-
const { tasks, showAllWorkspaces } = useTaskSearch()
7+
const { tasks } = useTaskSearch()
138

149
return (
15-
<>
16-
<div className="flex flex-col gap-3">
17-
{tasks.length !== 0 && (
18-
<>
19-
{tasks.slice(0, 3).map((item) => (
20-
<div
21-
key={item.id}
22-
className="bg-vscode-editor-background rounded relative overflow-hidden cursor-pointer border border-vscode-toolbar-hoverBackground/30 hover:border-vscode-toolbar-hoverBackground/60"
23-
onClick={() => vscode.postMessage({ type: "showTaskWithId", text: item.id })}>
24-
<div className="flex flex-col gap-2 p-3 pt-1">
25-
<div className="flex justify-between items-center">
26-
<span className="text-xs font-medium text-vscode-descriptionForeground uppercase">
27-
{formatDate(item.ts)}
28-
</span>
29-
<CopyButton itemTask={item.task} />
30-
</div>
31-
<div
32-
className="text-vscode-foreground overflow-hidden whitespace-pre-wrap"
33-
style={{
34-
display: "-webkit-box",
35-
WebkitLineClamp: 2,
36-
WebkitBoxOrient: "vertical",
37-
wordBreak: "break-word",
38-
overflowWrap: "anywhere",
39-
}}>
40-
{item.task}
41-
</div>
42-
<div className="flex flex-row gap-2 text-xs text-vscode-descriptionForeground">
43-
<span>{formatLargeNumber(item.tokensIn || 0)}</span>
44-
<span>{formatLargeNumber(item.tokensOut || 0)}</span>
45-
{!!item.totalCost && (
46-
<span>
47-
<Coins className="inline-block size-[1em]" />{" "}
48-
{"$" + item.totalCost?.toFixed(2)}
49-
</span>
50-
)}
51-
</div>
52-
{showAllWorkspaces && item.workspace && (
53-
<div className="flex flex-row gap-1 text-vscode-descriptionForeground text-xs mt-1">
54-
<span className="codicon codicon-folder scale-80" />
55-
<span>{item.workspace}</span>
56-
</div>
57-
)}
58-
</div>
59-
</div>
60-
))}
61-
</>
62-
)}
63-
</div>
64-
</>
10+
<div className="flex flex-col gap-3">
11+
{tasks.length !== 0 && (
12+
<>
13+
{tasks.slice(0, 3).map((item) => (
14+
<TaskItem key={item.id} item={item} variant="compact" />
15+
))}
16+
</>
17+
)}
18+
</div>
6519
)
6620
}
6721

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

Lines changed: 11 additions & 241 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
import React, { memo, useState } from "react"
22
import { DeleteTaskDialog } from "./DeleteTaskDialog"
33
import { BatchDeleteTaskDialog } from "./BatchDeleteTaskDialog"
4-
import prettyBytes from "pretty-bytes"
54
import { Virtuoso } from "react-virtuoso"
65

76
import { VSCodeTextField, VSCodeRadioGroup, VSCodeRadio } from "@vscode/webview-ui-toolkit/react"
87

9-
import { vscode } from "@/utils/vscode"
10-
import { formatLargeNumber, formatDate } from "@/utils/format"
118
import { cn } from "@/lib/utils"
129
import { Button, Checkbox } from "@/components/ui"
1310
import { useAppTranslation } from "@/i18n/TranslationContext"
1411

1512
import { Tab, TabContent, TabHeader } from "../common/Tab"
1613
import { useTaskSearch } from "./useTaskSearch"
17-
import { ExportButton } from "./ExportButton"
18-
import { CopyButton } from "./CopyButton"
14+
import TaskItem from "./TaskItem"
1915

2016
type HistoryViewProps = {
2117
onDone: () => void
@@ -210,245 +206,19 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
210206
)),
211207
}}
212208
itemContent={(index, item) => (
213-
<div
214-
data-testid={`task-item-${item.id}`}
209+
<TaskItem
215210
key={item.id}
216-
className={cn("cursor-pointer", {
211+
item={item}
212+
variant="full"
213+
showWorkspace={showAllWorkspaces}
214+
isSelectionMode={isSelectionMode}
215+
isSelected={selectedTaskIds.includes(item.id)}
216+
onToggleSelection={toggleTaskSelection}
217+
onDelete={setDeleteTaskId}
218+
className={cn({
217219
"border-b border-vscode-panel-border": index < tasks.length - 1,
218-
"bg-vscode-list-activeSelectionBackground":
219-
isSelectionMode && selectedTaskIds.includes(item.id),
220220
})}
221-
onClick={() => {
222-
if (isSelectionMode) {
223-
toggleTaskSelection(item.id, !selectedTaskIds.includes(item.id))
224-
} else {
225-
vscode.postMessage({ type: "showTaskWithId", text: item.id })
226-
}
227-
}}>
228-
<div className="flex items-start p-3 gap-2 ml-2">
229-
{/* Show checkbox in selection mode */}
230-
{isSelectionMode && (
231-
<div
232-
className="task-checkbox mt-1"
233-
onClick={(e) => {
234-
e.stopPropagation()
235-
}}>
236-
<Checkbox
237-
checked={selectedTaskIds.includes(item.id)}
238-
onCheckedChange={(checked) =>
239-
toggleTaskSelection(item.id, checked === true)
240-
}
241-
variant="description"
242-
/>
243-
</div>
244-
)}
245-
246-
<div className="flex-1">
247-
<div className="flex justify-between items-center">
248-
<span className="text-vscode-descriptionForeground font-medium text-sm uppercase">
249-
{formatDate(item.ts)}
250-
</span>
251-
<div className="flex flex-row">
252-
{!isSelectionMode && (
253-
<Button
254-
variant="ghost"
255-
size="sm"
256-
title={t("history:deleteTaskTitle")}
257-
data-testid="delete-task-button"
258-
onClick={(e) => {
259-
e.stopPropagation()
260-
261-
if (e.shiftKey) {
262-
vscode.postMessage({
263-
type: "deleteTaskWithId",
264-
text: item.id,
265-
})
266-
} else {
267-
setDeleteTaskId(item.id)
268-
}
269-
}}>
270-
<span className="codicon codicon-trash" />
271-
{item.size && prettyBytes(item.size)}
272-
</Button>
273-
)}
274-
</div>
275-
</div>
276-
<div
277-
style={{
278-
fontSize: "var(--vscode-font-size)",
279-
color: "var(--vscode-foreground)",
280-
display: "-webkit-box",
281-
WebkitLineClamp: 3,
282-
WebkitBoxOrient: "vertical",
283-
overflow: "hidden",
284-
whiteSpace: "pre-wrap",
285-
wordBreak: "break-word",
286-
overflowWrap: "anywhere",
287-
}}
288-
data-testid="task-content"
289-
dangerouslySetInnerHTML={{ __html: item.task }}
290-
/>
291-
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
292-
<div
293-
data-testid="tokens-container"
294-
style={{
295-
display: "flex",
296-
justifyContent: "space-between",
297-
alignItems: "center",
298-
}}>
299-
<div
300-
style={{
301-
display: "flex",
302-
alignItems: "center",
303-
gap: "4px",
304-
flexWrap: "wrap",
305-
}}>
306-
<span
307-
style={{
308-
fontWeight: 500,
309-
color: "var(--vscode-descriptionForeground)",
310-
}}>
311-
{t("history:tokensLabel")}
312-
</span>
313-
<span
314-
data-testid="tokens-in"
315-
style={{
316-
display: "flex",
317-
alignItems: "center",
318-
gap: "3px",
319-
color: "var(--vscode-descriptionForeground)",
320-
}}>
321-
<i
322-
className="codicon codicon-arrow-up"
323-
style={{
324-
fontSize: "12px",
325-
fontWeight: "bold",
326-
marginBottom: "-2px",
327-
}}
328-
/>
329-
{formatLargeNumber(item.tokensIn || 0)}
330-
</span>
331-
<span
332-
data-testid="tokens-out"
333-
style={{
334-
display: "flex",
335-
alignItems: "center",
336-
gap: "3px",
337-
color: "var(--vscode-descriptionForeground)",
338-
}}>
339-
<i
340-
className="codicon codicon-arrow-down"
341-
style={{
342-
fontSize: "12px",
343-
fontWeight: "bold",
344-
marginBottom: "-2px",
345-
}}
346-
/>
347-
{formatLargeNumber(item.tokensOut || 0)}
348-
</span>
349-
</div>
350-
{!item.totalCost && !isSelectionMode && (
351-
<div className="flex flex-row gap-1">
352-
<CopyButton itemTask={item.task} />
353-
<ExportButton itemId={item.id} />
354-
</div>
355-
)}
356-
</div>
357-
358-
{!!item.cacheWrites && (
359-
<div
360-
data-testid="cache-container"
361-
style={{
362-
display: "flex",
363-
alignItems: "center",
364-
gap: "4px",
365-
flexWrap: "wrap",
366-
}}>
367-
<span
368-
style={{
369-
fontWeight: 500,
370-
color: "var(--vscode-descriptionForeground)",
371-
}}>
372-
{t("history:cacheLabel")}
373-
</span>
374-
<span
375-
data-testid="cache-writes"
376-
style={{
377-
display: "flex",
378-
alignItems: "center",
379-
gap: "3px",
380-
color: "var(--vscode-descriptionForeground)",
381-
}}>
382-
<i
383-
className="codicon codicon-database"
384-
style={{
385-
fontSize: "12px",
386-
fontWeight: "bold",
387-
marginBottom: "-1px",
388-
}}
389-
/>
390-
+{formatLargeNumber(item.cacheWrites || 0)}
391-
</span>
392-
<span
393-
data-testid="cache-reads"
394-
style={{
395-
display: "flex",
396-
alignItems: "center",
397-
gap: "3px",
398-
color: "var(--vscode-descriptionForeground)",
399-
}}>
400-
<i
401-
className="codicon codicon-arrow-right"
402-
style={{
403-
fontSize: "12px",
404-
fontWeight: "bold",
405-
marginBottom: 0,
406-
}}
407-
/>
408-
{formatLargeNumber(item.cacheReads || 0)}
409-
</span>
410-
</div>
411-
)}
412-
413-
{!!item.totalCost && (
414-
<div
415-
style={{
416-
display: "flex",
417-
justifyContent: "space-between",
418-
alignItems: "center",
419-
marginTop: -2,
420-
}}>
421-
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
422-
<span
423-
style={{
424-
fontWeight: 500,
425-
color: "var(--vscode-descriptionForeground)",
426-
}}>
427-
{t("history:apiCostLabel")}
428-
</span>
429-
<span style={{ color: "var(--vscode-descriptionForeground)" }}>
430-
${item.totalCost?.toFixed(4)}
431-
</span>
432-
</div>
433-
{!isSelectionMode && (
434-
<div className="flex flex-row gap-1">
435-
<CopyButton itemTask={item.task} />
436-
<ExportButton itemId={item.id} />
437-
</div>
438-
)}
439-
</div>
440-
)}
441-
442-
{showAllWorkspaces && item.workspace && (
443-
<div className="flex flex-row gap-1 text-vscode-descriptionForeground text-xs">
444-
<span className="codicon codicon-folder scale-80" />
445-
<span>{item.workspace}</span>
446-
</div>
447-
)}
448-
</div>
449-
</div>
450-
</div>
451-
</div>
221+
/>
452222
)}
453223
/>
454224
</TabContent>

0 commit comments

Comments
 (0)