Skip to content

Commit 9b30065

Browse files
committed
Make the copy action on history and history preview consistent
1 parent b6a9bc9 commit 9b30065

File tree

7 files changed

+147
-144
lines changed

7 files changed

+147
-144
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { useCallback } from "react"
2+
3+
import { useClipboard } from "@/components/ui/hooks"
4+
import { Button } from "@/components/ui"
5+
import { cn } from "@/lib/utils"
6+
7+
type CopyButtonProps = {
8+
itemTask: string
9+
}
10+
11+
export const CopyButton = ({ itemTask }: CopyButtonProps) => {
12+
const { isCopied, copy } = useClipboard()
13+
14+
const onCopy = useCallback(
15+
(e: React.MouseEvent) => {
16+
e.stopPropagation()
17+
!isCopied && copy(itemTask)
18+
},
19+
[isCopied, copy, itemTask],
20+
)
21+
22+
return (
23+
<Button
24+
variant="ghost"
25+
size="icon"
26+
title="Copy Prompt"
27+
onClick={onCopy}
28+
className="opacity-50 hover:opacity-100">
29+
<span className={cn("codicon scale-80", { "codicon-check": isCopied, "codicon-copy": !isCopied })} />
30+
</Button>
31+
)
32+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { vscode } from "@/utils/vscode"
2+
import { Button } from "@/components/ui"
3+
4+
export const ExportButton = ({ itemId }: { itemId: string }) => (
5+
<Button
6+
data-testid="export"
7+
variant="ghost"
8+
size="icon"
9+
title="Export Task"
10+
onClick={(e) => {
11+
e.stopPropagation()
12+
vscode.postMessage({ type: "exportTaskWithId", text: itemId })
13+
}}>
14+
<span className="codicon codicon-cloud-download" />
15+
</Button>
16+
)

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

Lines changed: 24 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,27 @@
1-
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
2-
import { useExtensionState } from "../../context/ExtensionStateContext"
3-
import { vscode } from "../../utils/vscode"
41
import { memo } from "react"
5-
import { formatLargeNumber } from "../../utils/format"
6-
import { useCopyToClipboard } from "../../utils/clipboard"
2+
3+
import { vscode } from "@/utils/vscode"
4+
import { formatLargeNumber, formatDate } from "@/utils/format"
5+
import { Button } from "@/components/ui"
6+
7+
import { useExtensionState } from "../../context/ExtensionStateContext"
8+
import { CopyButton } from "./CopyButton"
79

810
type HistoryPreviewProps = {
911
showHistoryView: () => void
1012
}
1113

1214
const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
1315
const { taskHistory } = useExtensionState()
14-
const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard()
16+
1517
const handleHistorySelect = (id: string) => {
1618
vscode.postMessage({ type: "showTaskWithId", text: id })
1719
}
1820

19-
const formatDate = (timestamp: number) => {
20-
const date = new Date(timestamp)
21-
return date
22-
?.toLocaleString("en-US", {
23-
month: "long",
24-
day: "numeric",
25-
hour: "numeric",
26-
minute: "2-digit",
27-
hour12: true,
28-
})
29-
.replace(", ", " ")
30-
.replace(" at", ",")
31-
.toUpperCase()
32-
}
33-
3421
return (
3522
<div style={{ flexShrink: 0 }}>
36-
{showCopyFeedback && <div className="copy-modal">Prompt Copied to Clipboard</div>}
3723
<style>
3824
{`
39-
.copy-modal {
40-
position: fixed;
41-
top: 50%;
42-
left: 50%;
43-
transform: translate(-50%, -50%);
44-
background-color: var(--vscode-notifications-background);
45-
color: var(--vscode-notifications-foreground);
46-
padding: 12px 20px;
47-
border-radius: 4px;
48-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
49-
z-index: 1000;
50-
transition: opacity 0.2s ease-in-out;
51-
}
52-
.copy-button {
53-
opacity: 0;
54-
pointer-events: none;
55-
}
56-
.history-preview-item:hover .copy-button {
57-
opacity: 1;
58-
pointer-events: auto;
59-
}
6025
.history-preview-item {
6126
background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 65%, transparent);
6227
border-radius: 4px;
@@ -73,28 +38,17 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
7338
}
7439
`}
7540
</style>
76-
7741
<div
7842
style={{
7943
color: "var(--vscode-descriptionForeground)",
8044
margin: "10px 20px 10px 20px",
8145
display: "flex",
8246
alignItems: "center",
8347
}}>
84-
<span
85-
className="codicon codicon-comment-discussion"
86-
style={{ marginRight: "4px", transform: "scale(0.9)" }}></span>
87-
<span
88-
style={{
89-
fontWeight: 500,
90-
fontSize: "0.85em",
91-
textTransform: "uppercase",
92-
}}>
93-
Recent Tasks
94-
</span>
48+
<span className="codicon codicon-comment-discussion scale-90 mr-1" />
49+
<span className="font-medium text-xs uppercase">Recent Tasks</span>
9550
</div>
96-
97-
<div style={{ padding: "0px 20px 0 20px" }}>
51+
<div className="px-5">
9852
{taskHistory
9953
.filter((item) => item.ts && item.task)
10054
.slice(0, 3)
@@ -103,48 +57,25 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
10357
key={item.id}
10458
className="history-preview-item"
10559
onClick={() => handleHistorySelect(item.id)}>
106-
<div style={{ padding: "12px", position: "relative" }}>
107-
<div
108-
style={{
109-
marginBottom: "8px",
110-
display: "flex",
111-
justifyContent: "space-between",
112-
alignItems: "center",
113-
}}>
114-
<span
115-
style={{
116-
color: "var(--vscode-descriptionForeground)",
117-
fontWeight: 500,
118-
fontSize: "0.85em",
119-
textTransform: "uppercase",
120-
}}>
60+
<div className="flex flex-col gap-2 p-3 pt-1">
61+
<div className="flex justify-between items-center">
62+
<span className="text-xs font-medium text-vscode-descriptionForeground uppercase">
12163
{formatDate(item.ts)}
12264
</span>
123-
<button
124-
title="Copy Prompt"
125-
aria-label="Copy Prompt"
126-
className="copy-button"
127-
data-appearance="icon"
128-
onClick={(e) => copyWithFeedback(item.task, e)}>
129-
<span className="codicon codicon-copy"></span>
130-
</button>
65+
<CopyButton itemTask={item.task} />
13166
</div>
13267
<div
68+
className="text-vscode-descriptionForeground overflow-hidden whitespace-pre-wrap"
13369
style={{
134-
fontSize: "var(--vscode-font-size)",
135-
color: "var(--vscode-descriptionForeground)",
136-
marginBottom: "8px",
13770
display: "-webkit-box",
13871
WebkitLineClamp: 3,
13972
WebkitBoxOrient: "vertical",
140-
overflow: "hidden",
141-
whiteSpace: "pre-wrap",
14273
wordBreak: "break-word",
14374
overflowWrap: "anywhere",
14475
}}>
14576
{item.task}
14677
</div>
147-
<div style={{ fontSize: "0.85em", color: "var(--vscode-descriptionForeground)" }}>
78+
<div className="text-xs text-vscode-descriptionForeground">
14879
<span>
14980
Tokens: ↑{formatLargeNumber(item.tokensIn || 0)}
15081
{formatLargeNumber(item.tokensOut || 0)}
@@ -168,21 +99,14 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
16899
</div>
169100
</div>
170101
))}
171-
<div style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
172-
<VSCodeButton
173-
appearance="icon"
102+
<div className="flex justify-center">
103+
<Button
104+
variant="ghost"
105+
size="sm"
174106
onClick={() => showHistoryView()}
175-
style={{
176-
opacity: 0.9,
177-
}}>
178-
<div
179-
style={{
180-
fontSize: "var(--vscode-font-size)",
181-
color: "var(--vscode-descriptionForeground)",
182-
}}>
183-
View all history
184-
</div>
185-
</VSCodeButton>
107+
className="font-normal text-vscode-descriptionForeground">
108+
View all history
109+
</Button>
186110
</div>
187111
</div>
188112
</div>

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

Lines changed: 7 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import prettyBytes from "pretty-bytes"
55
import { Virtuoso } from "react-virtuoso"
66
import { VSCodeButton, VSCodeTextField, VSCodeRadioGroup, VSCodeRadio } from "@vscode/webview-ui-toolkit/react"
77

8+
import { vscode } from "@/utils/vscode"
9+
import { formatLargeNumber, formatDate } from "@/utils/format"
10+
import { highlightFzfMatch } from "@/utils/highlight"
11+
import { Button } from "@/components/ui"
12+
813
import { useExtensionState } from "../../context/ExtensionStateContext"
9-
import { vscode } from "../../utils/vscode"
10-
import { formatLargeNumber } from "../../utils/format"
11-
import { highlightFzfMatch } from "../../utils/highlight"
12-
import { useCopyToClipboard } from "../../utils/clipboard"
13-
import { Button } from "../ui"
14+
import { ExportButton } from "./ExportButton"
15+
import { CopyButton } from "./CopyButton"
1416

1517
type HistoryViewProps = {
1618
onDone: () => void
@@ -40,21 +42,6 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
4042

4143
const [deleteTaskId, setDeleteTaskId] = useState<string | null>(null)
4244

43-
const formatDate = (timestamp: number) => {
44-
const date = new Date(timestamp)
45-
return date
46-
?.toLocaleString("en-US", {
47-
month: "long",
48-
day: "numeric",
49-
hour: "numeric",
50-
minute: "2-digit",
51-
hour12: true,
52-
})
53-
.replace(", ", " ")
54-
.replace(" at", ",")
55-
.toUpperCase()
56-
}
57-
5845
const presentableTasks = useMemo(() => {
5946
return taskHistory.filter((item) => item.ts && item.task)
6047
}, [taskHistory])
@@ -409,28 +396,4 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
409396
)
410397
}
411398

412-
const CopyButton = ({ itemTask }: { itemTask: string }) => {
413-
const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard()
414-
415-
return (
416-
<Button variant="ghost" size="icon" title="Copy Prompt" onClick={(e) => copyWithFeedback(itemTask, e)}>
417-
{showCopyFeedback ? <span className="codicon codicon-check" /> : <span className="codicon codicon-copy" />}
418-
</Button>
419-
)
420-
}
421-
422-
const ExportButton = ({ itemId }: { itemId: string }) => (
423-
<Button
424-
data-testid="export"
425-
variant="ghost"
426-
size="icon"
427-
title="Export Task"
428-
onClick={(e) => {
429-
e.stopPropagation()
430-
vscode.postMessage({ type: "exportTaskWithId", text: itemId })
431-
}}>
432-
<span className="codicon codicon-cloud-download" />
433-
</Button>
434-
)
435-
436399
export default memo(HistoryView)

webview-ui/src/index.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
@theme {
2525
--font-display: var(--vscode-font-family);
26+
27+
--text-xs: calc(var(--vscode-font-size) * 0.85);
2628
--text-sm: calc(var(--vscode-font-size) * 0.9);
2729
--text-base: var(--vscode-font-size);
2830
--text-lg: calc(var(--vscode-font-size) * 1.1);
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// npx jest src/utils/__tests__/format.test.ts
2+
3+
import { formatDate } from "../format"
4+
5+
describe("formatDate", () => {
6+
it("formats a timestamp correctly", () => {
7+
// January 15, 2023, 10:30 AM
8+
const timestamp = new Date(2023, 0, 15, 10, 30).getTime()
9+
const result = formatDate(timestamp)
10+
11+
expect(result).toBe("JANUARY 15, 10:30 AM")
12+
})
13+
14+
it("handles different months correctly", () => {
15+
// February 28, 2023, 3:45 PM
16+
const timestamp1 = new Date(2023, 1, 28, 15, 45).getTime()
17+
expect(formatDate(timestamp1)).toBe("FEBRUARY 28, 3:45 PM")
18+
19+
// December 31, 2023, 11:59 PM
20+
const timestamp2 = new Date(2023, 11, 31, 23, 59).getTime()
21+
expect(formatDate(timestamp2)).toBe("DECEMBER 31, 11:59 PM")
22+
})
23+
24+
it("handles AM/PM correctly", () => {
25+
// Morning time - 7:05 AM
26+
const morningTimestamp = new Date(2023, 5, 15, 7, 5).getTime()
27+
expect(formatDate(morningTimestamp)).toBe("JUNE 15, 7:05 AM")
28+
29+
// Noon - 12:00 PM
30+
const noonTimestamp = new Date(2023, 5, 15, 12, 0).getTime()
31+
expect(formatDate(noonTimestamp)).toBe("JUNE 15, 12:00 PM")
32+
33+
// Evening time - 8:15 PM
34+
const eveningTimestamp = new Date(2023, 5, 15, 20, 15).getTime()
35+
expect(formatDate(eveningTimestamp)).toBe("JUNE 15, 8:15 PM")
36+
})
37+
38+
it("handles single-digit minutes with leading zeros", () => {
39+
// 9:05 AM
40+
const timestamp = new Date(2023, 3, 10, 9, 5).getTime()
41+
expect(formatDate(timestamp)).toBe("APRIL 10, 9:05 AM")
42+
})
43+
44+
it("converts the result to uppercase", () => {
45+
const timestamp = new Date(2023, 8, 21, 16, 45).getTime()
46+
const result = formatDate(timestamp)
47+
48+
expect(result).toBe(result.toUpperCase())
49+
expect(result).toBe("SEPTEMBER 21, 4:45 PM")
50+
})
51+
})

webview-ui/src/utils/format.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,18 @@ export function formatLargeNumber(num: number): string {
1010
}
1111
return num.toString()
1212
}
13+
14+
export const formatDate = (timestamp: number) => {
15+
const date = new Date(timestamp)
16+
return date
17+
.toLocaleString("en-US", {
18+
month: "long",
19+
day: "numeric",
20+
hour: "numeric",
21+
minute: "2-digit",
22+
hour12: true,
23+
})
24+
.replace(", ", " ")
25+
.replace(" at", ",")
26+
.toUpperCase()
27+
}

0 commit comments

Comments
 (0)