Centralize clipboard functionality

This commit is contained in:
Roo Code 2025-02-07 22:23:04 -05:00
parent 30cf0d5319
commit ecb4509bf8
4 changed files with 85 additions and 39 deletions

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

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

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