fix(desktop): remove hidden chat action hover strip #642

The previous fix moved the hover group onto the message shell, but the action row still occupied layout space while invisible. That invisible row kept expanding the shell hit area, so moving the pointer into the empty metadata strip could still reveal copy and timestamp controls.

Collapse the action row to zero height by default and expand it only while the message shell is hovered or focused. Keep message content spacing separate from the action row so hidden controls do not create a hover target.

Constraint: Copy/fork controls must remain reachable after the bubble reveals them.
Rejected: Use display:none for the action row | would remove the keyboard focus path entirely.
Confidence: high
Scope-risk: narrow
Directive: Keep hidden message metadata rows out of normal hover hit-testing; do not reintroduce invisible layout space around chat bubbles.
Tested: cd desktop && bun run test src/components/chat/MessageList.test.tsx --run -t "keeps user actions anchored"
Tested: cd desktop && bun run test src/components/chat/MessageList.test.tsx --run
Tested: cd desktop && bun run test src/components/chat/UserMessage.test.tsx --run
Tested: bun run check:desktop
Tested: git diff --check
Not-tested: Live Tauri native hover smoke
Related: #642
This commit is contained in:
程序员阿江(Relakkes) 2026-05-31 21:43:09 +08:00
parent 9b3e58b60b
commit 0ab624bff4
4 changed files with 23 additions and 20 deletions

View File

@ -68,7 +68,7 @@ export const AssistantMessage = memo(function AssistantMessage({ content, isStre
<div
data-message-shell="assistant"
data-layout={documentLayout ? 'document' : 'bubble'}
className={`group flex min-w-0 flex-col items-start gap-2 ${
className={`group flex min-w-0 flex-col items-start ${
documentLayout
? 'w-full max-w-full'
: 'max-w-[88%] sm:max-w-[80%] lg:max-w-[72%]'

View File

@ -41,7 +41,7 @@ export function MessageActionBar({
<div
data-message-actions
data-align={align}
className={`invisible flex w-full opacity-0 transition-opacity duration-150 group-hover:visible group-hover:opacity-100 group-focus-within:visible group-focus-within:opacity-100 ${
className={`pointer-events-none mt-0 flex h-0 w-full overflow-hidden opacity-0 transition-[height,margin-top,opacity] duration-150 group-hover:pointer-events-auto group-hover:mt-2 group-hover:h-7 group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:mt-2 group-focus-within:h-7 group-focus-within:opacity-100 ${
align === 'end' ? 'justify-end' : 'justify-start'
}`}
>

View File

@ -2743,8 +2743,9 @@ describe('MessageList nested tool calls', () => {
expect(assistantShell?.className).not.toContain('ml-10')
expect(userActions?.getAttribute('data-align')).toBe('end')
expect(assistantActions?.getAttribute('data-align')).toBe('start')
expect(userActions?.className).toContain('invisible')
expect(userActions?.className).toContain('group-hover:visible')
expect(userActions?.className).toContain('h-0')
expect(userActions?.className).toContain('group-hover:h-7')
expect(userActions?.className).not.toContain('invisible')
expect(within(userActions as HTMLElement).getByText('5m ago')).toBeTruthy()
expect(within(assistantActions as HTMLElement).getByText('2h ago')).toBeTruthy()
})

View File

@ -17,24 +17,26 @@ export const UserMessage = memo(function UserMessage({ content, attachments, bra
<div className="mb-5 flex justify-end">
<div
data-message-shell="user"
className="group flex min-w-0 max-w-[82%] flex-col items-end gap-2 sm:max-w-[78%] lg:max-w-[72%]"
className="group flex min-w-0 max-w-[82%] flex-col items-end sm:max-w-[78%] lg:max-w-[72%]"
>
{attachments && attachments.length > 0 && (
<AttachmentGallery attachments={attachments} variant="message" />
)}
<div className="flex max-w-full flex-col items-end gap-2">
{attachments && attachments.length > 0 && (
<AttachmentGallery attachments={attachments} variant="message" />
)}
{hasText && (
<div
className="min-w-0 max-w-full bg-[var(--color-surface-user-msg)] px-4 py-3 text-sm leading-relaxed text-[var(--color-text-primary)] whitespace-pre-wrap break-words"
style={{
borderRadius: '18px 4px 18px 18px',
overflowWrap: 'anywhere',
wordBreak: 'break-word',
}}
>
{content}
</div>
)}
{hasText && (
<div
className="min-w-0 max-w-full bg-[var(--color-surface-user-msg)] px-4 py-3 text-sm leading-relaxed text-[var(--color-text-primary)] whitespace-pre-wrap break-words"
style={{
borderRadius: '18px 4px 18px 18px',
overflowWrap: 'anywhere',
wordBreak: 'break-word',
}}
>
{content}
</div>
)}
</div>
{hasText && (
<MessageActionBar