Skip to content

Commit 4622ad7

Browse files
pashpashpashCline Evaluation
andauthored
Copy buttons (RooCodeInc#3373)
* copy button in task header * changeset * added copy buttons to assistant messages that show up on hover * added aria --------- Co-authored-by: Cline Evaluation <[email protected]>
1 parent 96048d5 commit 4622ad7

File tree

3 files changed

+144
-42
lines changed

3 files changed

+144
-42
lines changed

.changeset/fifty-parents-clean.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"claude-dev": minor
3+
---
4+
5+
added copy buttons to task header and assistant messages

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

Lines changed: 101 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { VSCodeBadge, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
1+
import { VSCodeBadge, VSCodeProgressRing, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
22
import deepEqual from "fast-deep-equal"
33
import React, { memo, useCallback, useEffect, useMemo, useRef, useState, MouseEvent } from "react"
44

@@ -20,6 +20,62 @@ import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "@/utils
2020
import { vscode } from "@/utils/vscode"
2121
import { FileServiceClient } from "@/services/grpc-client"
2222
import { CheckmarkControl } from "@/components/common/CheckmarkControl"
23+
24+
interface CopyButtonProps {
25+
textToCopy: string | undefined
26+
}
27+
28+
const CopyButtonStyled = styled(VSCodeButton)`
29+
position: absolute;
30+
bottom: 2px;
31+
right: 2px;
32+
z-index: 1;
33+
opacity: 0;
34+
`
35+
36+
interface WithCopyButtonProps {
37+
children: React.ReactNode
38+
textToCopy?: string
39+
style?: React.CSSProperties
40+
ref?: React.Ref<HTMLDivElement>
41+
onMouseUp?: (event: MouseEvent<HTMLDivElement>) => void
42+
}
43+
44+
const StyledContainer = styled.div`
45+
position: relative;
46+
47+
&:hover ${CopyButtonStyled} {
48+
opacity: 1;
49+
}
50+
`
51+
52+
const WithCopyButton = React.forwardRef<HTMLDivElement, WithCopyButtonProps>(
53+
({ children, textToCopy, style, onMouseUp, ...props }, ref) => {
54+
const [copied, setCopied] = useState(false)
55+
56+
const handleCopy = () => {
57+
if (!textToCopy) return
58+
59+
navigator.clipboard.writeText(textToCopy).then(() => {
60+
setCopied(true)
61+
setTimeout(() => {
62+
setCopied(false)
63+
}, 1500)
64+
})
65+
}
66+
67+
return (
68+
<StyledContainer ref={ref} onMouseUp={onMouseUp} style={style} {...props}>
69+
{children}
70+
{textToCopy && (
71+
<CopyButtonStyled appearance="icon" onClick={handleCopy} aria-label={copied ? "Copied" : "Copy"}>
72+
<span className={`codicon codicon-${copied ? "check" : "copy"}`}></span>
73+
</CopyButtonStyled>
74+
)}
75+
</StyledContainer>
76+
)
77+
},
78+
)
2379
import { CheckpointControls, CheckpointOverlay } from "../common/CheckpointControls"
2480
import CodeAccordian, { cleanPathPrefix } from "../common/CodeAccordian"
2581
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
@@ -90,6 +146,7 @@ const Markdown = memo(({ markdown }: { markdown?: string }) => {
90146
overflowWrap: "anywhere",
91147
marginBottom: -15,
92148
marginTop: -15,
149+
overflow: "hidden", // contain child margins so that parent diff matches height of children
93150
}}>
94151
<MarkdownBlock markdown={markdown} />
95152
</div>
@@ -901,7 +958,7 @@ export const ChatRowContent = ({
901958
return <McpResponseDisplay responseText={message.text || ""} />
902959
case "text":
903960
return (
904-
<div ref={contentRef} onMouseUp={handleMouseUp} style={{ position: "relative" }}>
961+
<WithCopyButton ref={contentRef} onMouseUp={handleMouseUp} textToCopy={message.text}>
905962
<Markdown markdown={message.text} />
906963
{quoteButtonState.visible && (
907964
<QuoteButton
@@ -912,7 +969,7 @@ export const ChatRowContent = ({
912969
}}
913970
/>
914971
)}
915-
</div>
972+
</WithCopyButton>
916973
)
917974
case "reasoning":
918975
return (
@@ -1132,13 +1189,13 @@ export const ChatRowContent = ({
11321189
}}
11331190
/>
11341191
</div>
1135-
<div
1136-
ref={contentRef} // Added ref
1137-
onMouseUp={handleMouseUp} // Added handler
1192+
<WithCopyButton
1193+
ref={contentRef}
1194+
onMouseUp={handleMouseUp}
1195+
textToCopy={text}
11381196
style={{
11391197
color: "var(--vscode-charts-green)",
11401198
paddingTop: 10,
1141-
position: "relative", // Added position
11421199
}}>
11431200
<Markdown markdown={text} />
11441201
{quoteButtonState.visible && (
@@ -1148,7 +1205,7 @@ export const ChatRowContent = ({
11481205
onClick={handleQuoteClick}
11491206
/>
11501207
)}
1151-
</div>
1208+
</WithCopyButton>
11521209
{message.partial !== true && hasChanges && (
11531210
<div style={{ paddingTop: 17 }}>
11541211
<SuccessButton
@@ -1295,13 +1352,13 @@ export const ChatRowContent = ({
12951352
}}
12961353
/>
12971354
</div>
1298-
<div
1299-
ref={contentRef} // Added ref
1300-
onMouseUp={handleMouseUp} // Added handler
1355+
<WithCopyButton
1356+
ref={contentRef}
1357+
onMouseUp={handleMouseUp}
1358+
textToCopy={text}
13011359
style={{
13021360
color: "var(--vscode-charts-green)",
13031361
paddingTop: 10,
1304-
position: "relative", // Added position
13051362
}}>
13061363
<Markdown markdown={text} />
13071364
{quoteButtonState.visible && (
@@ -1311,30 +1368,30 @@ export const ChatRowContent = ({
13111368
onClick={handleQuoteClick}
13121369
/>
13131370
)}
1314-
{message.partial !== true && hasChanges && (
1315-
<div style={{ marginTop: 15 }}>
1316-
<SuccessButton
1317-
appearance="secondary"
1318-
disabled={seeNewChangesDisabled}
1319-
onClick={() => {
1320-
setSeeNewChangesDisabled(true)
1321-
vscode.postMessage({
1322-
type: "taskCompletionViewChanges",
1323-
number: message.ts,
1324-
})
1325-
}}>
1326-
<i
1327-
className="codicon codicon-new-file"
1328-
style={{
1329-
marginRight: 6,
1330-
cursor: seeNewChangesDisabled ? "wait" : "pointer",
1331-
}}
1332-
/>
1333-
See new changes
1334-
</SuccessButton>
1335-
</div>
1336-
)}
1337-
</div>
1371+
</WithCopyButton>
1372+
{message.partial !== true && hasChanges && (
1373+
<div style={{ marginTop: 15 }}>
1374+
<SuccessButton
1375+
appearance="secondary"
1376+
disabled={seeNewChangesDisabled}
1377+
onClick={() => {
1378+
setSeeNewChangesDisabled(true)
1379+
vscode.postMessage({
1380+
type: "taskCompletionViewChanges",
1381+
number: message.ts,
1382+
})
1383+
}}>
1384+
<i
1385+
className="codicon codicon-new-file"
1386+
style={{
1387+
marginRight: 6,
1388+
cursor: seeNewChangesDisabled ? "wait" : "pointer",
1389+
}}
1390+
/>
1391+
See new changes
1392+
</SuccessButton>
1393+
</div>
1394+
)}
13381395
</div>
13391396
)
13401397
} else {
@@ -1362,7 +1419,11 @@ export const ChatRowContent = ({
13621419
{title}
13631420
</div>
13641421
)}
1365-
<div ref={contentRef} onMouseUp={handleMouseUp} style={{ position: "relative", paddingTop: 10 }}>
1422+
<WithCopyButton
1423+
ref={contentRef}
1424+
onMouseUp={handleMouseUp}
1425+
textToCopy={question}
1426+
style={{ paddingTop: 10 }}>
13661427
<Markdown markdown={question} />
13671428
<OptionsButtons
13681429
options={options}
@@ -1379,7 +1440,7 @@ export const ChatRowContent = ({
13791440
}}
13801441
/>
13811442
)}
1382-
</div>
1443+
</WithCopyButton>
13831444
</>
13841445
)
13851446
case "new_task":
@@ -1428,7 +1489,7 @@ export const ChatRowContent = ({
14281489
response = message.text
14291490
}
14301491
return (
1431-
<div ref={contentRef} onMouseUp={handleMouseUp} style={{ position: "relative" }}>
1492+
<WithCopyButton ref={contentRef} onMouseUp={handleMouseUp} textToCopy={response}>
14321493
<Markdown markdown={response} />
14331494
<OptionsButtons
14341495
options={options}
@@ -1445,7 +1506,7 @@ export const ChatRowContent = ({
14451506
}}
14461507
/>
14471508
)}
1448-
</div>
1509+
</WithCopyButton>
14491510
)
14501511
}
14511512
default:

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

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,10 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
400400
</HeroTooltip>
401401
</div>
402402
{!shouldShowPromptCacheInfo() && (
403-
<DeleteButton taskSize={formatSize(currentTaskItem?.size)} taskId={currentTaskItem?.id} />
403+
<div className="flex items-center flex-wrap">
404+
<CopyButton taskText={task.text} />
405+
<DeleteButton taskSize={formatSize(currentTaskItem?.size)} taskId={currentTaskItem?.id} />
406+
</div>
404407
)}
405408
</div>
406409
{shouldShowPromptCacheInfo() && (
@@ -452,7 +455,10 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
452455
</HeroTooltip>
453456
)}
454457
</div>
455-
<DeleteButton taskSize={formatSize(currentTaskItem?.size)} taskId={currentTaskItem?.id} />
458+
<div className="flex items-center flex-wrap">
459+
<CopyButton taskText={task.text} />
460+
<DeleteButton taskSize={formatSize(currentTaskItem?.size)} taskId={currentTaskItem?.id} />
461+
</div>
456462
</div>
457463
)}
458464
<div className="flex flex-col">
@@ -619,6 +625,36 @@ export const highlightText = (text?: string, withShadow = true) => {
619625
return [text]
620626
}
621627

628+
const CopyButton: React.FC<{
629+
taskText?: string
630+
}> = ({ taskText }) => {
631+
const [copied, setCopied] = useState(false)
632+
633+
const handleCopy = () => {
634+
if (!taskText) return
635+
636+
navigator.clipboard.writeText(taskText).then(() => {
637+
setCopied(true)
638+
setTimeout(() => setCopied(false), 1500)
639+
})
640+
}
641+
642+
return (
643+
<HeroTooltip content="Copy Task">
644+
<VSCodeButton
645+
appearance="icon"
646+
onClick={handleCopy}
647+
style={{ padding: "0px 0px" }}
648+
className="p-0"
649+
aria-label="Copy Task">
650+
<div className="flex items-center gap-[3px] text-[8px] font-bold opacity-60">
651+
<i className={`codicon codicon-${copied ? "check" : "copy"}`} />
652+
</div>
653+
</VSCodeButton>
654+
</HeroTooltip>
655+
)
656+
}
657+
622658
const DeleteButton: React.FC<{
623659
taskSize: string
624660
taskId?: string

0 commit comments

Comments
 (0)