Parcourir la source

Merge pull request #855 from hannesrudolph/patch-5

Add copy button to prompt preview view
Matt Rubens il y a 10 mois
Parent
commit
da2fe80a9b

+ 5 - 0
.changeset/cyan-insects-marry.md

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

+ 12 - 9
webview-ui/src/components/chat/ChatRow.tsx

@@ -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">

+ 40 - 2
webview-ui/src/components/history/HistoryPreview.tsx

@@ -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={{

+ 4 - 14
webview-ui/src/components/history/HistoryView.tsx

@@ -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

+ 57 - 0
webview-ui/src/utils/clipboard.ts

@@ -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,
+	}
+}