Browse Source

Centralize clipboard functionality

Roo Code 10 months ago
parent
commit
ecb4509bf8

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

+ 12 - 16
webview-ui/src/components/history/HistoryPreview.tsx

@@ -1,8 +1,9 @@
 import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
 import { useExtensionState } from "../../context/ExtensionStateContext"
 import { vscode } from "../../utils/vscode"
-import { memo, useState } from "react"
+import { memo } from "react"
 import { formatLargeNumber } from "../../utils/format"
+import { useCopyToClipboard } from "../../utils/clipboard"
 
 type HistoryPreviewProps = {
 	showHistoryView: () => void
@@ -10,18 +11,7 @@ type HistoryPreviewProps = {
 
 const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
 	const { taskHistory } = useExtensionState()
-	const [showCopyModal, setShowCopyModal] = useState(false)
-
-	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 { showCopyFeedback, copyWithFeedback } = useCopyToClipboard()
 	const handleHistorySelect = (id: string) => {
 		vscode.postMessage({ type: "showTaskWithId", text: id })
 	}
@@ -43,7 +33,7 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
 
 	return (
 		<div style={{ flexShrink: 0 }}>
-			{showCopyModal && <div className="copy-modal">Prompt Copied to Clipboard</div>}
+			{showCopyFeedback && <div className="copy-modal">Prompt Copied to Clipboard</div>}
 			<style>
 				{`
 					.copy-modal {
@@ -114,7 +104,13 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
 							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" }}>
+								<div
+									style={{
+										marginBottom: "8px",
+										display: "flex",
+										justifyContent: "space-between",
+										alignItems: "center",
+									}}>
 									<span
 										style={{
 											color: "var(--vscode-descriptionForeground)",
@@ -128,7 +124,7 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
 										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>
 								</div>

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