Skip to content

Commit 1a2d06d

Browse files
dbpolitozerone0x
authored andcommitted
feat(deskop): Add Copy to Messages (anomalyco#7658)
1 parent dd0e5d0 commit 1a2d06d

File tree

4 files changed

+66
-1
lines changed

4 files changed

+66
-1
lines changed

packages/ui/src/components/message-part.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,25 @@
7676
}
7777

7878
[data-slot="user-message-text"] {
79+
position: relative;
7980
white-space: pre-wrap;
8081
word-break: break-word;
8182
overflow: hidden;
8283
background: var(--surface-base);
8384
padding: 8px 12px;
8485
border-radius: 4px;
86+
87+
[data-slot="user-message-copy-wrapper"] {
88+
position: absolute;
89+
top: 7px;
90+
right: 7px;
91+
opacity: 0;
92+
transition: opacity 0.15s ease;
93+
}
94+
95+
&:hover [data-slot="user-message-copy-wrapper"] {
96+
opacity: 1;
97+
}
8598
}
8699

87100
.text-text-strong {

packages/ui/src/components/message-part.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import { Markdown } from "./markdown"
3838
import { ImagePreview } from "./image-preview"
3939
import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/util/path"
4040
import { checksum } from "@opencode-ai/util/encode"
41+
import { Tooltip } from "./tooltip"
42+
import { IconButton } from "./icon-button"
4143
import { createAutoScroll } from "../hooks"
4244

4345
interface Diagnostic {
@@ -278,6 +280,7 @@ export function AssistantMessageDisplay(props: { message: AssistantMessage; part
278280

279281
export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) {
280282
const dialog = useDialog()
283+
const [copied, setCopied] = createSignal(false)
281284

282285
const textPart = createMemo(
283286
() => props.parts?.find((p) => p.type === "text" && !(p as TextPart).synthetic) as TextPart | undefined,
@@ -307,6 +310,14 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
307310
dialog.show(() => <ImagePreview src={url} alt={alt} />)
308311
}
309312

313+
const handleCopy = async () => {
314+
const content = text()
315+
if (!content) return
316+
await navigator.clipboard.writeText(content)
317+
setCopied(true)
318+
setTimeout(() => setCopied(false), 2000)
319+
}
320+
310321
return (
311322
<div data-component="user-message">
312323
<Show when={attachments().length > 0}>
@@ -341,6 +352,11 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
341352
<Show when={text()}>
342353
<div data-slot="user-message-text">
343354
<HighlightedText text={text()} references={inlineFiles()} agents={agents()} />
355+
<div data-slot="user-message-copy-wrapper">
356+
<Tooltip value={copied() ? "Copied!" : "Copy"} placement="top" gutter={8}>
357+
<IconButton icon={copied() ? "check" : "copy"} variant="secondary" onClick={handleCopy} />
358+
</Tooltip>
359+
</div>
344360
</div>
345361
</Show>
346362
</div>

packages/ui/src/components/session-turn.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,22 @@
225225
}
226226
}
227227

228+
[data-slot="session-turn-summary-section"] {
229+
position: relative;
230+
231+
[data-slot="session-turn-summary-copy"] {
232+
position: absolute;
233+
top: 0;
234+
right: 0;
235+
opacity: 0;
236+
transition: opacity 0.15s ease;
237+
}
238+
239+
&:hover [data-slot="session-turn-summary-copy"] {
240+
opacity: 1;
241+
}
242+
}
243+
228244
[data-slot="session-turn-accordion"] {
229245
width: 100%;
230246
}

packages/ui/src/components/session-turn.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { useDiffComponent } from "../context/diff"
1111
import { getDirectory, getFilename } from "@opencode-ai/util/path"
1212

1313
import { Binary } from "@opencode-ai/util/binary"
14-
import { createEffect, createMemo, For, Match, on, onCleanup, ParentProps, Show, Switch } from "solid-js"
14+
import { createEffect, createMemo, createSignal, For, Match, on, onCleanup, ParentProps, Show, Switch } from "solid-js"
1515
import { createResizeObserver } from "@solid-primitives/resize-observer"
1616
import { DiffChanges } from "./diff-changes"
1717
import { Typewriter } from "./typewriter"
@@ -21,6 +21,8 @@ import { Accordion } from "./accordion"
2121
import { StickyAccordionHeader } from "./sticky-accordion-header"
2222
import { FileIcon } from "./file-icon"
2323
import { Icon } from "./icon"
24+
import { IconButton } from "./icon-button"
25+
import { Tooltip } from "./tooltip"
2426
import { Card } from "./card"
2527
import { Dynamic } from "solid-js/web"
2628
import { Button } from "./button"
@@ -328,6 +330,15 @@ export function SessionTurn(
328330
const hasDiffs = createMemo(() => message()?.summary?.diffs?.length)
329331
const hideResponsePart = createMemo(() => !working() && !!responsePartId())
330332

333+
const [responseCopied, setResponseCopied] = createSignal(false)
334+
const handleCopyResponse = async () => {
335+
const content = response()
336+
if (!content) return
337+
await navigator.clipboard.writeText(content)
338+
setResponseCopied(true)
339+
setTimeout(() => setResponseCopied(false), 2000)
340+
}
341+
331342
function duration() {
332343
const msg = message()
333344
if (!msg) return ""
@@ -556,6 +567,15 @@ export function SessionTurn(
556567
{/* Response */}
557568
<Show when={!working() && (response() || hasDiffs())}>
558569
<div data-slot="session-turn-summary-section">
570+
<div data-slot="session-turn-summary-copy">
571+
<Tooltip value={responseCopied() ? "Copied!" : "Copy"} placement="top" gutter={8}>
572+
<IconButton
573+
icon={responseCopied() ? "check" : "copy"}
574+
variant="secondary"
575+
onClick={handleCopyResponse}
576+
/>
577+
</Tooltip>
578+
</div>
559579
<div data-slot="session-turn-summary-header">
560580
<h2 data-slot="session-turn-summary-title">Response</h2>
561581
<Markdown

0 commit comments

Comments
 (0)