From 88605a46172472505411b4b5eb1443a251f2629b Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Thu, 5 Feb 2026 23:20:13 +0000 Subject: [PATCH] feat(ui): add copy option for selected text --- .../ui/src/components/message-section.tsx | 27 ++++++++++++++++++- .../ui/src/lib/i18n/messages/en/messaging.ts | 3 +++ .../ui/src/lib/i18n/messages/es/messaging.ts | 3 +++ .../ui/src/lib/i18n/messages/fr/messaging.ts | 3 +++ .../ui/src/lib/i18n/messages/ja/messaging.ts | 3 +++ .../ui/src/lib/i18n/messages/ru/messaging.ts | 3 +++ .../lib/i18n/messages/zh-Hans/messaging.ts | 3 +++ 7 files changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/message-section.tsx b/packages/ui/src/components/message-section.tsx index 283e4b24..19f611fd 100644 --- a/packages/ui/src/components/message-section.tsx +++ b/packages/ui/src/components/message-section.tsx @@ -7,6 +7,8 @@ import { getSessionInfo } from "../stores/sessions" import { messageStoreBus } from "../stores/message-v2/bus" import { useScrollCache } from "../lib/hooks/use-scroll-cache" import { useI18n } from "../lib/i18n" +import { copyToClipboard } from "../lib/clipboard" +import { showToastNotification } from "../lib/notifications" import type { InstanceMessageStore } from "../stores/message-v2/instance-store" const SCROLL_SCOPE = "session" @@ -375,7 +377,9 @@ export default function MessageSection(props: MessageSectionProps) { const anchorRect = rects.length > 0 ? rects[0] : range.getBoundingClientRect() const shellRect = shell.getBoundingClientRect() const relativeTop = Math.max(anchorRect.top - shellRect.top - 40, 8) - const maxLeft = Math.max(shell.clientWidth - 180, 8) + // Keep the popover within the stream shell. The quote popover currently + // renders 3 actions; keep enough horizontal room for the pill. + const maxLeft = Math.max(shell.clientWidth - 260, 8) const relativeLeft = Math.min(Math.max(anchorRect.left - shellRect.left, 8), maxLeft) setQuoteSelection({ text: limited, top: relativeTop, left: relativeLeft }) } @@ -394,6 +398,24 @@ export default function MessageSection(props: MessageSectionProps) { selection?.removeAllRanges() } } + + async function handleCopySelectionRequest() { + const info = quoteSelection() + if (!info) return + + const success = await copyToClipboard(info.text) + showToastNotification({ + message: success ? t("messageSection.quote.copied") : t("messageSection.quote.copyFailed"), + variant: success ? "success" : "error", + duration: success ? 2000 : 6000, + }) + + clearQuoteSelection() + if (typeof window !== "undefined") { + const selection = window.getSelection() + selection?.removeAllRanges() + } + } function handleContentRendered() { if (props.loading) { @@ -835,6 +857,9 @@ export default function MessageSection(props: MessageSectionProps) { + )} diff --git a/packages/ui/src/lib/i18n/messages/en/messaging.ts b/packages/ui/src/lib/i18n/messages/en/messaging.ts index b2c8568a..61783e6f 100644 --- a/packages/ui/src/lib/i18n/messages/en/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/en/messaging.ts @@ -20,6 +20,9 @@ export const messagingMessages = { "messageSection.scroll.toLatestAriaLabel": "Scroll to latest message", "messageSection.quote.addAsQuote": "Add as quote", "messageSection.quote.addAsCode": "Add as code", + "messageSection.quote.copy": "Copy", + "messageSection.quote.copied": "Copied!", + "messageSection.quote.copyFailed": "Copy failed", "messageTimeline.ariaLabel": "Message timeline", "messageTimeline.segment.user.label": "You", diff --git a/packages/ui/src/lib/i18n/messages/es/messaging.ts b/packages/ui/src/lib/i18n/messages/es/messaging.ts index 1c1da653..dd10f351 100644 --- a/packages/ui/src/lib/i18n/messages/es/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/es/messaging.ts @@ -20,6 +20,9 @@ export const messagingMessages = { "messageSection.scroll.toLatestAriaLabel": "Desplazarse al último mensaje", "messageSection.quote.addAsQuote": "Añadir como cita", "messageSection.quote.addAsCode": "Añadir como código", + "messageSection.quote.copy": "Copiar", + "messageSection.quote.copied": "¡Copiado!", + "messageSection.quote.copyFailed": "No se pudo copiar", "messageTimeline.ariaLabel": "Línea de tiempo de mensajes", "messageTimeline.segment.user.label": "Tú", diff --git a/packages/ui/src/lib/i18n/messages/fr/messaging.ts b/packages/ui/src/lib/i18n/messages/fr/messaging.ts index ef8fecf1..6bb886ae 100644 --- a/packages/ui/src/lib/i18n/messages/fr/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/fr/messaging.ts @@ -20,6 +20,9 @@ export const messagingMessages = { "messageSection.scroll.toLatestAriaLabel": "Aller au dernier message", "messageSection.quote.addAsQuote": "Ajouter en citation", "messageSection.quote.addAsCode": "Ajouter en code", + "messageSection.quote.copy": "Copier", + "messageSection.quote.copied": "Copié !", + "messageSection.quote.copyFailed": "Impossible de copier", "messageTimeline.ariaLabel": "Chronologie des messages", "messageTimeline.segment.user.label": "Vous", diff --git a/packages/ui/src/lib/i18n/messages/ja/messaging.ts b/packages/ui/src/lib/i18n/messages/ja/messaging.ts index 25823eb9..456d9f4b 100644 --- a/packages/ui/src/lib/i18n/messages/ja/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/ja/messaging.ts @@ -20,6 +20,9 @@ export const messagingMessages = { "messageSection.scroll.toLatestAriaLabel": "最新のメッセージへスクロール", "messageSection.quote.addAsQuote": "引用として追加", "messageSection.quote.addAsCode": "コードとして追加", + "messageSection.quote.copy": "コピー", + "messageSection.quote.copied": "コピーしました", + "messageSection.quote.copyFailed": "コピーできませんでした", "messageTimeline.ariaLabel": "メッセージタイムライン", "messageTimeline.segment.user.label": "あなた", diff --git a/packages/ui/src/lib/i18n/messages/ru/messaging.ts b/packages/ui/src/lib/i18n/messages/ru/messaging.ts index 3fe9e15d..4021c18b 100644 --- a/packages/ui/src/lib/i18n/messages/ru/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/ru/messaging.ts @@ -20,6 +20,9 @@ export const messagingMessages = { "messageSection.scroll.toLatestAriaLabel": "Прокрутить к последнему сообщению", "messageSection.quote.addAsQuote": "Добавить как цитату", "messageSection.quote.addAsCode": "Добавить как код", + "messageSection.quote.copy": "Копировать", + "messageSection.quote.copied": "Скопировано!", + "messageSection.quote.copyFailed": "Не удалось скопировать", "messageTimeline.ariaLabel": "Таймлайн сообщений", "messageTimeline.segment.user.label": "Вы", diff --git a/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts b/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts index 93f7a15d..e8a252af 100644 --- a/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts +++ b/packages/ui/src/lib/i18n/messages/zh-Hans/messaging.ts @@ -20,6 +20,9 @@ export const messagingMessages = { "messageSection.scroll.toLatestAriaLabel": "滚动到最新消息", "messageSection.quote.addAsQuote": "作为引用添加", "messageSection.quote.addAsCode": "作为代码添加", + "messageSection.quote.copy": "复制", + "messageSection.quote.copied": "已复制!", + "messageSection.quote.copyFailed": "无法复制", "messageTimeline.ariaLabel": "消息时间线", "messageTimeline.segment.user.label": "你",