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": "你",