Skip to content

Commit 0ad6be7

Browse files
daniel-lxsEric Wheeler
andauthored
Unify history item UI with TaskItem and TaskItemHeader (#4151)
* 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]> * test: fix TaskItem and HistoryView test failures after refactor Fixes test failures that occurred after the major refactoring that introduced the shared TaskItem component. The original implementation was correct but the tests needed updates to match the new component structure. - Add data-testid attributes to TaskItemHeader for reliable test selection - Update TaskItem.test.tsx assertions to use new test IDs for tokens/cache - Fix Checkbox import path in TaskItem.tsx (ui/checkbox vs ui) - Add missing mocks for lucide-react and Checkbox in HistoryView.test.tsx - Update HistoryView test assertions to use correct selectors - Ensure all 19 history component tests pass successfully The refactoring reduced code duplication by ~250+ lines while maintaining functionality, and these test fixes ensure the quality gates remain intact. Signed-off-by: Eric Wheeler <[email protected]> * ui: move token and cost info from header to dedicated footer Create a new TaskItemFooter component that displays token and cost information with different styles for compact and full views. Move the CopyButton and ExportButton from header to footer in full view. Adjust file size display positioning in the header for better visual alignment. Signed-off-by: Eric Wheeler <[email protected]> * test: update history component tests to match implementation Update data-testid attributes in HistoryView and TaskItem tests to match the actual implementation in TaskItemFooter component. The tests were looking for generic "tokens-in" and "tokens-out" attributes, but the implementation uses variant-specific attributes like "tokens-in-footer-full". Also added mocks for CopyButton and ExportButton components to resolve "Element type is invalid" errors during test rendering. Signed-off-by: Eric Wheeler <[email protected]> * Move cache information from header to footer to be consistent with cost/token data * Fix ESLint warnings: remove unused imports and variables --------- Signed-off-by: Eric Wheeler <[email protected]> Co-authored-by: Eric Wheeler <[email protected]>
1 parent 662d6ab commit 0ad6be7

File tree

7 files changed

+496
-308
lines changed

7 files changed

+496
-308
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)