diff --git a/desktop/src/components/chat/MessageActionBar.tsx b/desktop/src/components/chat/MessageActionBar.tsx
index 47647272..a0d09102 100644
--- a/desktop/src/components/chat/MessageActionBar.tsx
+++ b/desktop/src/components/chat/MessageActionBar.tsx
@@ -52,6 +52,7 @@ export function MessageActionBar({
label={copyLabel}
displayLabel={}
displayCopiedLabel={}
+ onPointerUp={(event) => event.currentTarget.blur()}
className="inline-flex h-7 w-7 items-center justify-center rounded-full border border-transparent bg-transparent text-[var(--color-text-tertiary)] transition-colors hover:border-[var(--color-brand)]/30 hover:bg-[var(--color-surface-container-low)] hover:text-[var(--color-text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35"
/>
) : null}
@@ -62,6 +63,7 @@ export function MessageActionBar({
disabled={branchAction.loading}
aria-label={branchAction.label}
title={branchAction.label}
+ onPointerUp={(event) => event.currentTarget.blur()}
className="inline-flex h-7 w-7 items-center justify-center rounded-full border border-transparent bg-transparent text-[var(--color-text-tertiary)] transition-colors hover:border-[var(--color-brand)]/30 hover:bg-[var(--color-surface-container-low)] hover:text-[var(--color-text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35 disabled:cursor-wait disabled:opacity-60"
>
diff --git a/desktop/src/components/chat/MessageList.test.tsx b/desktop/src/components/chat/MessageList.test.tsx
index a67f1404..8f225fc6 100644
--- a/desktop/src/components/chat/MessageList.test.tsx
+++ b/desktop/src/components/chat/MessageList.test.tsx
@@ -1580,6 +1580,33 @@ describe('MessageList nested tool calls', () => {
)
})
+ it('releases pointer focus from message actions after clicking copy', () => {
+ useChatStore.setState({
+ sessions: {
+ [ACTIVE_TAB]: makeSessionState({
+ messages: [
+ {
+ id: 'assistant-1',
+ type: 'assistant_text',
+ content: '离开 hover 后操作条应该恢复隐藏。',
+ timestamp: 1,
+ },
+ ],
+ }),
+ },
+ })
+
+ render()
+
+ const copyButton = screen.getByRole('button', { name: 'Copy reply' })
+ copyButton.focus()
+ expect(document.activeElement).toBe(copyButton)
+
+ fireEvent.pointerUp(copyButton)
+
+ expect(document.activeElement).not.toBe(copyButton)
+ })
+
it('adds selected user message text to the composer context', async () => {
useChatStore.setState({
sessions: {
diff --git a/desktop/src/components/shared/CopyButton.tsx b/desktop/src/components/shared/CopyButton.tsx
index a5ae0972..1bd33588 100644
--- a/desktop/src/components/shared/CopyButton.tsx
+++ b/desktop/src/components/shared/CopyButton.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useState, type ReactNode } from 'react'
+import { useEffect, useState, type PointerEventHandler, type ReactNode } from 'react'
import { copyTextToClipboard } from '../chat/clipboard'
type Props = {
@@ -8,6 +8,7 @@ type Props = {
displayLabel?: ReactNode
displayCopiedLabel?: ReactNode
className?: string
+ onPointerUp?: PointerEventHandler
}
export function CopyButton({
@@ -17,6 +18,7 @@ export function CopyButton({
displayLabel,
displayCopiedLabel,
className = '',
+ onPointerUp,
}: Props) {
const [copied, setCopied] = useState(false)
@@ -48,6 +50,7 @@ export function CopyButton({