feat(deskop): Add Copy to Messages (#7658)
This commit is contained in:
@@ -76,12 +76,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-slot="user-message-text"] {
|
[data-slot="user-message-text"] {
|
||||||
|
position: relative;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--surface-base);
|
background: var(--surface-base);
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 4px;
|
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 {
|
.text-text-strong {
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ import { Markdown } from "./markdown"
|
|||||||
import { ImagePreview } from "./image-preview"
|
import { ImagePreview } from "./image-preview"
|
||||||
import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/util/path"
|
import { getDirectory as _getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||||
import { checksum } from "@opencode-ai/util/encode"
|
import { checksum } from "@opencode-ai/util/encode"
|
||||||
|
import { Tooltip } from "./tooltip"
|
||||||
|
import { IconButton } from "./icon-button"
|
||||||
import { createAutoScroll } from "../hooks"
|
import { createAutoScroll } from "../hooks"
|
||||||
|
|
||||||
interface Diagnostic {
|
interface Diagnostic {
|
||||||
@@ -278,6 +280,7 @@ export function AssistantMessageDisplay(props: { message: AssistantMessage; part
|
|||||||
|
|
||||||
export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) {
|
export function UserMessageDisplay(props: { message: UserMessage; parts: PartType[] }) {
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
|
const [copied, setCopied] = createSignal(false)
|
||||||
|
|
||||||
const textPart = createMemo(
|
const textPart = createMemo(
|
||||||
() => props.parts?.find((p) => p.type === "text" && !(p as TextPart).synthetic) as TextPart | undefined,
|
() => 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} />)
|
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 (
|
return (
|
||||||
<div data-component="user-message">
|
<div data-component="user-message">
|
||||||
<Show when={attachments().length > 0}>
|
<Show when={attachments().length > 0}>
|
||||||
@@ -341,6 +352,11 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
|
|||||||
<Show when={text()}>
|
<Show when={text()}>
|
||||||
<div data-slot="user-message-text">
|
<div data-slot="user-message-text">
|
||||||
<HighlightedText text={text()} references={inlineFiles()} agents={agents()} />
|
<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>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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"] {
|
[data-slot="session-turn-accordion"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { useDiffComponent } from "../context/diff"
|
|||||||
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
import { getDirectory, getFilename } from "@opencode-ai/util/path"
|
||||||
|
|
||||||
import { Binary } from "@opencode-ai/util/binary"
|
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 { createResizeObserver } from "@solid-primitives/resize-observer"
|
||||||
import { DiffChanges } from "./diff-changes"
|
import { DiffChanges } from "./diff-changes"
|
||||||
import { Typewriter } from "./typewriter"
|
import { Typewriter } from "./typewriter"
|
||||||
@@ -21,6 +21,8 @@ import { Accordion } from "./accordion"
|
|||||||
import { StickyAccordionHeader } from "./sticky-accordion-header"
|
import { StickyAccordionHeader } from "./sticky-accordion-header"
|
||||||
import { FileIcon } from "./file-icon"
|
import { FileIcon } from "./file-icon"
|
||||||
import { Icon } from "./icon"
|
import { Icon } from "./icon"
|
||||||
|
import { IconButton } from "./icon-button"
|
||||||
|
import { Tooltip } from "./tooltip"
|
||||||
import { Card } from "./card"
|
import { Card } from "./card"
|
||||||
import { Dynamic } from "solid-js/web"
|
import { Dynamic } from "solid-js/web"
|
||||||
import { Button } from "./button"
|
import { Button } from "./button"
|
||||||
@@ -328,6 +330,15 @@ export function SessionTurn(
|
|||||||
const hasDiffs = createMemo(() => message()?.summary?.diffs?.length)
|
const hasDiffs = createMemo(() => message()?.summary?.diffs?.length)
|
||||||
const hideResponsePart = createMemo(() => !working() && !!responsePartId())
|
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() {
|
function duration() {
|
||||||
const msg = message()
|
const msg = message()
|
||||||
if (!msg) return ""
|
if (!msg) return ""
|
||||||
@@ -556,6 +567,15 @@ export function SessionTurn(
|
|||||||
{/* Response */}
|
{/* Response */}
|
||||||
<Show when={!working() && (response() || hasDiffs())}>
|
<Show when={!working() && (response() || hasDiffs())}>
|
||||||
<div data-slot="session-turn-summary-section">
|
<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">
|
<div data-slot="session-turn-summary-header">
|
||||||
<h2 data-slot="session-turn-summary-title">Response</h2>
|
<h2 data-slot="session-turn-summary-title">Response</h2>
|
||||||
<Markdown
|
<Markdown
|
||||||
|
|||||||
Reference in New Issue
Block a user