Skip to content

Commit da2fe80

Browse files
authored
Merge pull request #855 from hannesrudolph/patch-5
Add copy button to prompt preview view
2 parents 9bace96 + 44e00fa commit da2fe80

File tree

5 files changed

+118
-25
lines changed

5 files changed

+118
-25
lines changed

.changeset/cyan-insects-marry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": patch
3+
---
4+
5+
Add a copy button to the recent tasks

webview-ui/src/components/chat/ChatRow.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { VSCodeBadge, VSCodeButton, VSCodeProgressRing } from "@vscode/webview-u
22
import deepEqual from "fast-deep-equal"
33
import React, { memo, useEffect, useMemo, useRef, useState } from "react"
44
import { useSize } from "react-use"
5+
import { useCopyToClipboard } from "../../utils/clipboard"
56
import {
67
ClineApiReqInfo,
78
ClineAskUseMcpServer,
@@ -985,6 +986,7 @@ export const ProgressIndicator = () => (
985986

986987
const Markdown = memo(({ markdown, partial }: { markdown?: string; partial?: boolean }) => {
987988
const [isHovering, setIsHovering] = useState(false)
989+
const { copyWithFeedback } = useCopyToClipboard(200) // shorter feedback duration for copy button flash
988990

989991
return (
990992
<div
@@ -1021,15 +1023,16 @@ const Markdown = memo(({ markdown, partial }: { markdown?: string; partial?: boo
10211023
background: "var(--vscode-editor-background)",
10221024
transition: "background 0.2s ease-in-out",
10231025
}}
1024-
onClick={() => {
1025-
navigator.clipboard.writeText(markdown)
1026-
// Flash the button background briefly to indicate success
1027-
const button = document.activeElement as HTMLElement
1028-
if (button) {
1029-
button.style.background = "var(--vscode-button-background)"
1030-
setTimeout(() => {
1031-
button.style.background = ""
1032-
}, 200)
1026+
onClick={async () => {
1027+
const success = await copyWithFeedback(markdown)
1028+
if (success) {
1029+
const button = document.activeElement as HTMLElement
1030+
if (button) {
1031+
button.style.background = "var(--vscode-button-background)"
1032+
setTimeout(() => {
1033+
button.style.background = ""
1034+
}, 200)
1035+
}
10331036
}
10341037
}}
10351038
title="Copy as markdown">

webview-ui/src/components/history/HistoryPreview.tsx

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import { useExtensionState } from "../../context/ExtensionStateContext"
33
import { vscode } from "../../utils/vscode"
44
import { memo } from "react"
55
import { formatLargeNumber } from "../../utils/format"
6+
import { useCopyToClipboard } from "../../utils/clipboard"
67

78
type HistoryPreviewProps = {
89
showHistoryView: () => void
910
}
1011

1112
const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
1213
const { taskHistory } = useExtensionState()
14+
const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard()
1315
const handleHistorySelect = (id: string) => {
1416
vscode.postMessage({ type: "showTaskWithId", text: id })
1517
}
@@ -31,8 +33,30 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
3133

3234
return (
3335
<div style={{ flexShrink: 0 }}>
36+
{showCopyFeedback && <div className="copy-modal">Prompt Copied to Clipboard</div>}
3437
<style>
3538
{`
39+
.copy-modal {
40+
position: fixed;
41+
top: 50%;
42+
left: 50%;
43+
transform: translate(-50%, -50%);
44+
background-color: var(--vscode-notifications-background);
45+
color: var(--vscode-notifications-foreground);
46+
padding: 12px 20px;
47+
border-radius: 4px;
48+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
49+
z-index: 1000;
50+
transition: opacity 0.2s ease-in-out;
51+
}
52+
.copy-button {
53+
opacity: 0;
54+
pointer-events: none;
55+
}
56+
.history-preview-item:hover .copy-button {
57+
opacity: 1;
58+
pointer-events: auto;
59+
}
3660
.history-preview-item {
3761
background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 65%, transparent);
3862
border-radius: 4px;
@@ -79,8 +103,14 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
79103
key={item.id}
80104
className="history-preview-item"
81105
onClick={() => handleHistorySelect(item.id)}>
82-
<div style={{ padding: "12px" }}>
83-
<div style={{ marginBottom: "8px" }}>
106+
<div style={{ padding: "12px", position: "relative" }}>
107+
<div
108+
style={{
109+
marginBottom: "8px",
110+
display: "flex",
111+
justifyContent: "space-between",
112+
alignItems: "center",
113+
}}>
84114
<span
85115
style={{
86116
color: "var(--vscode-descriptionForeground)",
@@ -90,6 +120,14 @@ const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
90120
}}>
91121
{formatDate(item.ts)}
92122
</span>
123+
<button
124+
title="Copy Prompt"
125+
aria-label="Copy Prompt"
126+
className="copy-button"
127+
data-appearance="icon"
128+
onClick={(e) => copyWithFeedback(item.task, e)}>
129+
<span className="codicon codicon-copy"></span>
130+
</button>
93131
</div>
94132
<div
95133
style={{

webview-ui/src/components/history/HistoryView.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import React, { memo, useMemo, useState, useEffect } from "react"
66
import { Fzf } from "fzf"
77
import { formatLargeNumber } from "../../utils/format"
88
import { highlightFzfMatch } from "../../utils/highlight"
9+
import { useCopyToClipboard } from "../../utils/clipboard"
910

1011
type HistoryViewProps = {
1112
onDone: () => void
@@ -18,7 +19,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
1819
const [searchQuery, setSearchQuery] = useState("")
1920
const [sortOption, setSortOption] = useState<SortOption>("newest")
2021
const [lastNonRelevantSort, setLastNonRelevantSort] = useState<SortOption | null>("newest")
21-
const [showCopyModal, setShowCopyModal] = useState(false)
22+
const { showCopyFeedback, copyWithFeedback } = useCopyToClipboard()
2223

2324
useEffect(() => {
2425
if (searchQuery && sortOption !== "mostRelevant" && !lastNonRelevantSort) {
@@ -38,17 +39,6 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
3839
vscode.postMessage({ type: "deleteTaskWithId", text: id })
3940
}
4041

41-
const handleCopyTask = async (e: React.MouseEvent, task: string) => {
42-
e.stopPropagation()
43-
try {
44-
await navigator.clipboard.writeText(task)
45-
setShowCopyModal(true)
46-
setTimeout(() => setShowCopyModal(false), 2000)
47-
} catch (error) {
48-
console.error("Failed to copy to clipboard:", error)
49-
}
50-
}
51-
5242
const formatDate = (timestamp: number) => {
5343
const date = new Date(timestamp)
5444
return date
@@ -144,7 +134,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
144134
}
145135
`}
146136
</style>
147-
{showCopyModal && <div className="copy-modal">Prompt Copied to Clipboard</div>}
137+
{showCopyFeedback && <div className="copy-modal">Prompt Copied to Clipboard</div>}
148138
<div
149139
style={{
150140
position: "fixed",
@@ -271,7 +261,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
271261
title="Copy Prompt"
272262
className="copy-button"
273263
data-appearance="icon"
274-
onClick={(e) => handleCopyTask(e, item.task)}>
264+
onClick={(e) => copyWithFeedback(item.task, e)}>
275265
<span className="codicon codicon-copy"></span>
276266
</button>
277267
<button

webview-ui/src/utils/clipboard.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { useState, useCallback } from "react"
2+
3+
/**
4+
* Options for copying text to clipboard
5+
*/
6+
interface CopyOptions {
7+
/** Duration in ms to show success feedback (default: 2000) */
8+
feedbackDuration?: number
9+
/** Optional callback when copy succeeds */
10+
onSuccess?: () => void
11+
/** Optional callback when copy fails */
12+
onError?: (error: Error) => void
13+
}
14+
15+
/**
16+
* Copy text to clipboard with error handling
17+
*/
18+
export const copyToClipboard = async (text: string, options?: CopyOptions): Promise<boolean> => {
19+
try {
20+
await navigator.clipboard.writeText(text)
21+
options?.onSuccess?.()
22+
return true
23+
} catch (error) {
24+
const err = error instanceof Error ? error : new Error("Failed to copy to clipboard")
25+
options?.onError?.(err)
26+
console.error("Failed to copy to clipboard:", err)
27+
return false
28+
}
29+
}
30+
31+
/**
32+
* React hook for managing clipboard copy state with feedback
33+
*/
34+
export const useCopyToClipboard = (feedbackDuration = 2000) => {
35+
const [showCopyFeedback, setShowCopyFeedback] = useState(false)
36+
37+
const copyWithFeedback = useCallback(
38+
async (text: string, e?: React.MouseEvent) => {
39+
e?.stopPropagation()
40+
41+
const success = await copyToClipboard(text, {
42+
onSuccess: () => {
43+
setShowCopyFeedback(true)
44+
setTimeout(() => setShowCopyFeedback(false), feedbackDuration)
45+
},
46+
})
47+
48+
return success
49+
},
50+
[feedbackDuration],
51+
)
52+
53+
return {
54+
showCopyFeedback,
55+
copyWithFeedback,
56+
}
57+
}

0 commit comments

Comments
 (0)