mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Make the copy action on history and history preview consistent
This commit is contained in:
parent
b6a9bc9d98
commit
9b30065231
7 changed files with 147 additions and 144 deletions
32
webview-ui/src/components/history/CopyButton.tsx
Normal file
32
webview-ui/src/components/history/CopyButton.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
16
webview-ui/src/components/history/ExportButton.tsx
Normal file
16
webview-ui/src/components/history/ExportButton.tsx
Normal 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>
|
||||
)
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
51
webview-ui/src/utils/__tests__/format.test.ts
Normal file
51
webview-ui/src/utils/__tests__/format.test.ts
Normal 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")
|
||||
})
|
||||
})
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue