Make the copy action on history and history preview consistent

This commit is contained in:
Chris Estreich 2025-02-28 08:25:04 -08:00
parent b6a9bc9d98
commit 9b30065231
7 changed files with 147 additions and 144 deletions

View file

@ -0,0 +1,32 @@
import { useCallback } from "react"
import { useClipboard } from "@/components/ui/hooks"
import { Button } from "@/components/ui"
import { cn } from "@/lib/utils"
type CopyButtonProps = {
itemTask: string
}
export const CopyButton = ({ itemTask }: CopyButtonProps) => {
const { isCopied, copy } = useClipboard()
const onCopy = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation()
!isCopied && copy(itemTask)
},
[isCopied, copy, itemTask],
)
return (
<Button
variant="ghost"
size="icon"
title="Copy Prompt"
onClick={onCopy}
className="opacity-50 hover:opacity-100">
<span className={cn("codicon scale-80", { "codicon-check": isCopied, "codicon-copy": !isCopied })} />
</Button>
)
}

View file

@ -0,0 +1,16 @@
import { vscode } from "@/utils/vscode"
import { Button } from "@/components/ui"
export const ExportButton = ({ itemId }: { itemId: string }) => (
<Button
data-testid="export"
variant="ghost"
size="icon"
title="Export Task"
onClick={(e) => {
e.stopPropagation()
vscode.postMessage({ type: "exportTaskWithId", text: itemId })
}}>
<span className="codicon codicon-cloud-download" />
</Button>
)

View file

@ -1,9 +1,11 @@
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { vscode } from "../../utils/vscode"
import { memo } from "react"
import { formatLargeNumber } from "../../utils/format"
import { useCopyToClipboard } from "../../utils/clipboard"
import { vscode } from "@/utils/vscode"
import { formatLargeNumber, formatDate } from "@/utils/format"
import { Button } from "@/components/ui"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { CopyButton } from "./CopyButton"
type HistoryPreviewProps = {
showHistoryView: () => void
@ -11,52 +13,15 @@ type HistoryPreviewProps = {
const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
const { taskHistory } = useExtensionState()
const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard()
const handleHistorySelect = (id: string) => {
vscode.postMessage({ type: "showTaskWithId", text: id })
}
const formatDate = (timestamp: number) => {
const date = new Date(timestamp)
return date
?.toLocaleString("en-US", {
month: "long",
day: "numeric",
hour: "numeric",
minute: "2-digit",
hour12: true,
})
.replace(", ", " ")
.replace(" at", ",")
.toUpperCase()
}
return (
<div style={{ flexShrink: 0 }}>
{showCopyFeedback && <div className="copy-modal">Prompt Copied to Clipboard</div>}
<style>
{`
.copy-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: var(--vscode-notifications-background);
color: var(--vscode-notifications-foreground);
padding: 12px 20px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 1000;
transition: opacity 0.2s ease-in-out;
}
.copy-button {
opacity: 0;
pointer-events: none;
}
.history-preview-item:hover .copy-button {
opacity: 1;
pointer-events: auto;
}
.history-preview-item {
background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 65%, transparent);
border-radius: 4px;
@ -73,7 +38,6 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
}
`}
</style>
<div
style={{
color: "var(--vscode-descriptionForeground)",
@ -81,20 +45,10 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
display: "flex",
alignItems: "center",
}}>
<span
className="codicon codicon-comment-discussion"
style={{ marginRight: "4px", transform: "scale(0.9)" }}></span>
<span
style={{
fontWeight: 500,
fontSize: "0.85em",
textTransform: "uppercase",
}}>
Recent Tasks
</span>
<span className="codicon codicon-comment-discussion scale-90 mr-1" />
<span className="font-medium text-xs uppercase">Recent Tasks</span>
</div>
<div style={{ padding: "0px 20px 0 20px" }}>
<div className="px-5">
{taskHistory
.filter((item) => item.ts && item.task)
.slice(0, 3)
@ -103,48 +57,25 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
key={item.id}
className="history-preview-item"
onClick={() => handleHistorySelect(item.id)}>
<div style={{ padding: "12px", position: "relative" }}>
<div
style={{
marginBottom: "8px",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}>
<span
style={{
color: "var(--vscode-descriptionForeground)",
fontWeight: 500,
fontSize: "0.85em",
textTransform: "uppercase",
}}>
<div className="flex flex-col gap-2 p-3 pt-1">
<div className="flex justify-between items-center">
<span className="text-xs font-medium text-vscode-descriptionForeground uppercase">
{formatDate(item.ts)}
</span>
<button
title="Copy Prompt"
aria-label="Copy Prompt"
className="copy-button"
data-appearance="icon"
onClick={(e) => copyWithFeedback(item.task, e)}>
<span className="codicon codicon-copy"></span>
</button>
<CopyButton itemTask={item.task} />
</div>
<div
className="text-vscode-descriptionForeground overflow-hidden whitespace-pre-wrap"
style={{
fontSize: "var(--vscode-font-size)",
color: "var(--vscode-descriptionForeground)",
marginBottom: "8px",
display: "-webkit-box",
WebkitLineClamp: 3,
WebkitBoxOrient: "vertical",
overflow: "hidden",
whiteSpace: "pre-wrap",
wordBreak: "break-word",
overflowWrap: "anywhere",
}}>
{item.task}
</div>
<div style={{ fontSize: "0.85em", color: "var(--vscode-descriptionForeground)" }}>
<div className="text-xs text-vscode-descriptionForeground">
<span>
Tokens: {formatLargeNumber(item.tokensIn || 0)}
{formatLargeNumber(item.tokensOut || 0)}
@ -168,21 +99,14 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
</div>
</div>
))}
<div style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
<VSCodeButton
appearance="icon"
<div className="flex justify-center">
<Button
variant="ghost"
size="sm"
onClick={() => showHistoryView()}
style={{
opacity: 0.9,
}}>
<div
style={{
fontSize: "var(--vscode-font-size)",
color: "var(--vscode-descriptionForeground)",
}}>
View all history
</div>
</VSCodeButton>
className="font-normal text-vscode-descriptionForeground">
View all history
</Button>
</div>
</div>
</div>

View file

@ -5,12 +5,14 @@ import prettyBytes from "pretty-bytes"
import { Virtuoso } from "react-virtuoso"
import { VSCodeButton, VSCodeTextField, VSCodeRadioGroup, VSCodeRadio } from "@vscode/webview-ui-toolkit/react"
import { vscode } from "@/utils/vscode"
import { formatLargeNumber, formatDate } from "@/utils/format"
import { highlightFzfMatch } from "@/utils/highlight"
import { Button } from "@/components/ui"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { vscode } from "../../utils/vscode"
import { formatLargeNumber } from "../../utils/format"
import { highlightFzfMatch } from "../../utils/highlight"
import { useCopyToClipboard } from "../../utils/clipboard"
import { Button } from "../ui"
import { ExportButton } from "./ExportButton"
import { CopyButton } from "./CopyButton"
type HistoryViewProps = {
onDone: () => void
@ -40,21 +42,6 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
const [deleteTaskId, setDeleteTaskId] = useState<string | null>(null)
const formatDate = (timestamp: number) => {
const date = new Date(timestamp)
return date
?.toLocaleString("en-US", {
month: "long",
day: "numeric",
hour: "numeric",
minute: "2-digit",
hour12: true,
})
.replace(", ", " ")
.replace(" at", ",")
.toUpperCase()
}
const presentableTasks = useMemo(() => {
return taskHistory.filter((item) => item.ts && item.task)
}, [taskHistory])
@ -409,28 +396,4 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
)
}
const CopyButton = ({ itemTask }: { itemTask: string }) => {
const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard()
return (
<Button variant="ghost" size="icon" title="Copy Prompt" onClick={(e) => copyWithFeedback(itemTask, e)}>
{showCopyFeedback ? <span className="codicon codicon-check" /> : <span className="codicon codicon-copy" />}
</Button>
)
}
const ExportButton = ({ itemId }: { itemId: string }) => (
<Button
data-testid="export"
variant="ghost"
size="icon"
title="Export Task"
onClick={(e) => {
e.stopPropagation()
vscode.postMessage({ type: "exportTaskWithId", text: itemId })
}}>
<span className="codicon codicon-cloud-download" />
</Button>
)
export default memo(HistoryView)

View file

@ -23,6 +23,8 @@
@theme {
--font-display: var(--vscode-font-family);
--text-xs: calc(var(--vscode-font-size) * 0.85);
--text-sm: calc(var(--vscode-font-size) * 0.9);
--text-base: var(--vscode-font-size);
--text-lg: calc(var(--vscode-font-size) * 1.1);

View file

@ -0,0 +1,51 @@
// npx jest src/utils/__tests__/format.test.ts
import { formatDate } from "../format"
describe("formatDate", () => {
it("formats a timestamp correctly", () => {
// January 15, 2023, 10:30 AM
const timestamp = new Date(2023, 0, 15, 10, 30).getTime()
const result = formatDate(timestamp)
expect(result).toBe("JANUARY 15, 10:30 AM")
})
it("handles different months correctly", () => {
// February 28, 2023, 3:45 PM
const timestamp1 = new Date(2023, 1, 28, 15, 45).getTime()
expect(formatDate(timestamp1)).toBe("FEBRUARY 28, 3:45 PM")
// December 31, 2023, 11:59 PM
const timestamp2 = new Date(2023, 11, 31, 23, 59).getTime()
expect(formatDate(timestamp2)).toBe("DECEMBER 31, 11:59 PM")
})
it("handles AM/PM correctly", () => {
// Morning time - 7:05 AM
const morningTimestamp = new Date(2023, 5, 15, 7, 5).getTime()
expect(formatDate(morningTimestamp)).toBe("JUNE 15, 7:05 AM")
// Noon - 12:00 PM
const noonTimestamp = new Date(2023, 5, 15, 12, 0).getTime()
expect(formatDate(noonTimestamp)).toBe("JUNE 15, 12:00 PM")
// Evening time - 8:15 PM
const eveningTimestamp = new Date(2023, 5, 15, 20, 15).getTime()
expect(formatDate(eveningTimestamp)).toBe("JUNE 15, 8:15 PM")
})
it("handles single-digit minutes with leading zeros", () => {
// 9:05 AM
const timestamp = new Date(2023, 3, 10, 9, 5).getTime()
expect(formatDate(timestamp)).toBe("APRIL 10, 9:05 AM")
})
it("converts the result to uppercase", () => {
const timestamp = new Date(2023, 8, 21, 16, 45).getTime()
const result = formatDate(timestamp)
expect(result).toBe(result.toUpperCase())
expect(result).toBe("SEPTEMBER 21, 4:45 PM")
})
})

View file

@ -10,3 +10,18 @@ export function formatLargeNumber(num: number): string {
}
return num.toString()
}
export const formatDate = (timestamp: number) => {
const date = new Date(timestamp)
return date
.toLocaleString("en-US", {
month: "long",
day: "numeric",
hour: "numeric",
minute: "2-digit",
hour12: true,
})
.replace(", ", " ")
.replace(" at", ",")
.toUpperCase()
}