Merge pull request #855 from hannesrudolph/patch-5

Add copy button to prompt preview view
This commit is contained in:
Matt Rubens 2025-02-07 22:28:17 -05:00 committed by GitHub
commit da2fe80a9b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 118 additions and 25 deletions

View file

@ -0,0 +1,5 @@
---
"roo-cline": patch
---
Add a copy button to the recent tasks

View file

@ -2,6 +2,7 @@ import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-u
import deepEqual from "fast-deep-equal"
import React, { memo, useEffect, useMemo, useRef, useState } from "react"
import { useSize } from "react-use"
import { useCopyToClipboard } from "../../utils/clipboard"
import {
ClineApiReqInfo,
ClineAskUseMcpServer,
@ -985,6 +986,7 @@ export const ProgressIndicator = () => (
const Markdown = memo(({ markdown, partial }: { markdown?: string; partial?: boolean }) => {
const [isHovering, setIsHovering] = useState(false)
const { copyWithFeedback } = useCopyToClipboard(200) // shorter feedback duration for copy button flash
return (
<div
@ -1021,15 +1023,16 @@ const Markdown = memo(({ markdown, partial }: { markdown?: string; partial?: boo
background: "var(--vscode-editor-background)",
transition: "background 0.2s ease-in-out",
}}
onClick={() => {
navigator.clipboard.writeText(markdown)
// Flash the button background briefly to indicate success
const button = document.activeElement as HTMLElement
if (button) {
button.style.background = "var(--vscode-button-background)"
setTimeout(() => {
button.style.background = ""
}, 200)
onClick={async () => {
const success = await copyWithFeedback(markdown)
if (success) {
const button = document.activeElement as HTMLElement
if (button) {
button.style.background = "var(--vscode-button-background)"
setTimeout(() => {
button.style.background = ""
}, 200)
}
}
}}
title="Copy as markdown">

View file

@ -3,6 +3,7 @@ import { useExtensionState } from "../../context/ExtensionStateContext"
import { vscode } from "../../utils/vscode"
import { memo } from "react"
import { formatLargeNumber } from "../../utils/format"
import { useCopyToClipboard } from "../../utils/clipboard"
type HistoryPreviewProps = {
showHistoryView: () => void
@ -10,6 +11,7 @@ type HistoryPreviewProps = {
const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
const { taskHistory } = useExtensionState()
const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard()
const handleHistorySelect = (id: string) => {
vscode.postMessage({ type: "showTaskWithId", text: id })
}
@ -31,8 +33,30 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
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;
@ -79,8 +103,14 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
key={item.id}
className="history-preview-item"
onClick={() => handleHistorySelect(item.id)}>
<div style={{ padding: "12px" }}>
<div style={{ marginBottom: "8px" }}>
<div style={{ padding: "12px", position: "relative" }}>
<div
style={{
marginBottom: "8px",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}>
<span
style={{
color: "var(--vscode-descriptionForeground)",
@ -90,6 +120,14 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
}}>
{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>
</div>
<div
style={{

View file

@ -6,6 +6,7 @@ import React, { memo, useMemo, useState, useEffect } from "react"
import { Fzf } from "fzf"
import { formatLargeNumber } from "../../utils/format"
import { highlightFzfMatch } from "../../utils/highlight"
import { useCopyToClipboard } from "../../utils/clipboard"
type HistoryViewProps = {
onDone: () => void
@ -18,7 +19,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
const [searchQuery, setSearchQuery] = useState("")
const [sortOption, setSortOption] = useState<SortOption>("newest")
const [lastNonRelevantSort, setLastNonRelevantSort] = useState<SortOption | null>("newest")
const [showCopyModal, setShowCopyModal] = useState(false)
const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard()
useEffect(() => {
if (searchQuery && sortOption !== "mostRelevant" && !lastNonRelevantSort) {
@ -38,17 +39,6 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
vscode.postMessage({ type: "deleteTaskWithId", text: id })
}
const handleCopyTask = async (e: React.MouseEvent, task: string) => {
e.stopPropagation()
try {
await navigator.clipboard.writeText(task)
setShowCopyModal(true)
setTimeout(() => setShowCopyModal(false), 2000)
} catch (error) {
console.error("Failed to copy to clipboard:", error)
}
}
const formatDate = (timestamp: number) => {
const date = new Date(timestamp)
return date
@ -144,7 +134,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
}
`}
</style>
{showCopyModal && <div className="copy-modal">Prompt Copied to Clipboard</div>}
{showCopyFeedback && <div className="copy-modal">Prompt Copied to Clipboard</div>}
<div
style={{
position: "fixed",
@ -271,7 +261,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
title="Copy Prompt"
className="copy-button"
data-appearance="icon"
onClick={(e) => handleCopyTask(e, item.task)}>
onClick={(e) => copyWithFeedback(item.task, e)}>
<span className="codicon codicon-copy"></span>
</button>
<button

View file

@ -0,0 +1,57 @@
import { useState, useCallback } from "react"
/**
* Options for copying text to clipboard
*/
interface CopyOptions {
/** Duration in ms to show success feedback (default: 2000) */
feedbackDuration?: number
/** Optional callback when copy succeeds */
onSuccess?: () => void
/** Optional callback when copy fails */
onError?: (error: Error) => void
}
/**
* Copy text to clipboard with error handling
*/
export const copyToClipboard = async (text: string, options?: CopyOptions): Promise<boolean> => {
try {
await navigator.clipboard.writeText(text)
options?.onSuccess?.()
return true
} catch (error) {
const err = error instanceof Error ? error : new Error("Failed to copy to clipboard")
options?.onError?.(err)
console.error("Failed to copy to clipboard:", err)
return false
}
}
/**
* React hook for managing clipboard copy state with feedback
*/
export const useCopyToClipboard = (feedbackDuration = 2000) => {
const [showCopyFeedback, setShowCopyFeedback] = useState(false)
const copyWithFeedback = useCallback(
async (text: string, e?: React.MouseEvent) => {
e?.stopPropagation()
const success = await copyToClipboard(text, {
onSuccess: () => {
setShowCopyFeedback(true)
setTimeout(() => setShowCopyFeedback(false), feedbackDuration)
},
})
return success
},
[feedbackDuration],
)
return {
showCopyFeedback,
copyWithFeedback,
}
}