diff --git a/packages/ui/src/components/message-part.css b/packages/ui/src/components/message-part.css index fba36dc278d..3cdd93cb9fb 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 d59f5cfa3e3..644690ed2f0 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(() => ) } + const handleCopy = async () => { + const content = text() + if (!content) return + await navigator.clipboard.writeText(content) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + return (
0}> @@ -341,6 +352,11 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
+
+ + + +
diff --git a/packages/ui/src/components/session-turn.css b/packages/ui/src/components/session-turn.css index 9b7aa736437..581935b3ed5 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 f69d414be58..075da218bb1 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 */}
+
+ + + +

Response