diff options
| author | Daniel Polito <[email protected]> | 2026-01-10 19:01:23 -0300 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-01-10 16:01:23 -0600 |
| commit | 50ed4c6b5de8aa0e5d0739f933f7afd2ecaa95f4 (patch) | |
| tree | e7a6e63d030e0d14eece9603e3017ba2e6bb1d9b /packages | |
| parent | f882cca98a2b245b695880499dfc4101d4ed924c (diff) | |
| download | opencode-50ed4c6b5de8aa0e5d0739f933f7afd2ecaa95f4.tar.gz opencode-50ed4c6b5de8aa0e5d0739f933f7afd2ecaa95f4.zip | |
feat(deskop): Add Copy to Messages (#7658)
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/ui/src/components/message-part.css | 13 | ||||
| -rw-r--r-- | packages/ui/src/components/message-part.tsx | 16 | ||||
| -rw-r--r-- | packages/ui/src/components/session-turn.css | 16 | ||||
| -rw-r--r-- | packages/ui/src/components/session-turn.tsx | 22 |
4 files changed, 66 insertions, 1 deletions
diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index fba36dc27..3cdd93cb9 100644 --- a/packages/ui/src/components/message-part.css +++ b/packages/ui/src/components/message-part.css @@ -76,12 +76,25 @@ } [data-slot="user-message-text"] { + position: relative; white-space: pre-wrap; word-break: break-word; overflow: hidden; background: var(--surface-base); padding: 8px 12px; border-radius: 4px; + + [data-slot="user-message-copy-wrapper"] { + position: absolute; + top: 7px; + right: 7px; + opacity: 0; + transition: opacity 0.15s ease; + } + + &:hover [data-slot="user-message-copy-wrapper"] { + opacity: 1; + } } .text-text-strong { diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx index d59f5cfa3..644690ed2 100644 --- a/packages/ui/src/components/message-part.tsx +++ b/packages/ui/src/components/message-part.tsx @@ -38,6 +38,8 @@ import { Markdown } from "./markdown" import { ImagePreview } from "./image-preview" import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/util/path" import { checksum } from "@opencode-ai/util/encode" +import { Tooltip } from "./tooltip" +import { IconButton } from "./icon-button" import { createAutoScroll } from "../hooks" interface Diagnostic { @@ -278,6 +280,7 @@ export function AssistantMessageDisplay(props: { message: AssistantMessage; part export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) { const dialog = useDialog() + const [copied, setCopied] = createSignal(false) const textPart = createMemo( () => 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 dialog.show(() => <ImagePreview src={url} alt={alt} />) } + const handleCopy = async () => { + const content = text() + if (!content) return + await navigator.clipboard.writeText(content) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + return ( <div data-component="user-message"> <Show when={attachments().length > 0}> @@ -341,6 +352,11 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp <Show when={text()}> <div data-slot="user-message-text"> <HighlightedText text={text()} references={inlineFiles()} agents={agents()} /> + <div data-slot="user-message-copy-wrapper"> + <Tooltip value={copied() ? "Copied!" : "Copy"} placement="top" gutter={8}> + <IconButton icon={copied() ? "check" : "copy"} variant="secondary" onClick={handleCopy} /> + </Tooltip> + </div> </div> </Show> </div> diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index 9b7aa7364..581935b3e 100644 --- a/packages/ui/src/components/session-turn.css +++ b/packages/ui/src/components/session-turn.css @@ -225,6 +225,22 @@ } } + [data-slot="session-turn-summary-section"] { + position: relative; + + [data-slot="session-turn-summary-copy"] { + position: absolute; + top: 0; + right: 0; + opacity: 0; + transition: opacity 0.15s ease; + } + + &:hover [data-slot="session-turn-summary-copy"] { + opacity: 1; + } + } + [data-slot="session-turn-accordion"] { width: 100%; } diff --git a/packages/ui/src/components/session-turn.tsx b/packages/ui/src/components/session-turn.tsx index f69d414be..075da218b 100644 --- a/packages/ui/src/components/session-turn.tsx +++ b/packages/ui/src/components/session-turn.tsx @@ -11,7 +11,7 @@ import { useDiffComponent } from "../context/diff" import { getDirectory, getFilename } from "@opencode-ai/util/path" import { Binary } from "@opencode-ai/util/binary" -import { createEffect, createMemo, For, Match, on, onCleanup, ParentProps, Show, Switch } from "solid-js" +import { createEffect, createMemo, createSignal, For, Match, on, onCleanup, ParentProps, Show, Switch } from "solid-js" import { createResizeObserver } from "@solid-primitives/resize-observer" import { DiffChanges } from "./diff-changes" import { Typewriter } from "./typewriter" @@ -21,6 +21,8 @@ import { Accordion } from "./accordion" import { StickyAccordionHeader } from "./sticky-accordion-header" import { FileIcon } from "./file-icon" import { Icon } from "./icon" +import { IconButton } from "./icon-button" +import { Tooltip } from "./tooltip" import { Card } from "./card" import { Dynamic } from "solid-js/web" import { Button } from "./button" @@ -328,6 +330,15 @@ export function SessionTurn( const hasDiffs = createMemo(() => message()?.summary?.diffs?.length) const hideResponsePart = createMemo(() => !working() && !!responsePartId()) + const [responseCopied, setResponseCopied] = createSignal(false) + const handleCopyResponse = async () => { + const content = response() + if (!content) return + await navigator.clipboard.writeText(content) + setResponseCopied(true) + setTimeout(() => setResponseCopied(false), 2000) + } + function duration() { const msg = message() if (!msg) return "" @@ -556,6 +567,15 @@ export function SessionTurn( {/* Response */} <Show when={!working() && (response() || hasDiffs())}> <div data-slot="session-turn-summary-section"> + <div data-slot="session-turn-summary-copy"> + <Tooltip value={responseCopied() ? "Copied!" : "Copy"} placement="top" gutter={8}> + <IconButton + icon={responseCopied() ? "check" : "copy"} + variant="secondary" + onClick={handleCopyResponse} + /> + </Tooltip> + </div> <div data-slot="session-turn-summary-header"> <h2 data-slot="session-turn-summary-title">Response</h2> <Markdown |
