Prevent long chat URLs from breaking message layout

Long uninterrupted URLs can otherwise force user message bubbles beyond the chat column. The fix keeps the bubble constrained to its parent and allows safe breaks inside continuous URL text, with the same guard applied to markdown paragraphs and links.

Constraint: Issue #378 reports desktop chat overflow when a conversation contains a long URL
Rejected: Add horizontal scrolling to user messages | this preserves the overflow instead of fixing normal chat readability
Confidence: high
Scope-risk: narrow
Tested: cd desktop && bunx vitest run src/components/chat/UserMessage.test.tsx src/components/chat/MessageList.test.tsx
Tested: cd desktop && bun run lint
Tested: cd desktop && bun run build
Tested: bun run check:desktop
Tested: browser layout fixture PASS shell=619 bubbleClient=619 bubbleScroll=619
Not-tested: full root quality gate
Related: https://github.com/NanmiCoder/cc-haha/issues/378
This commit is contained in:
程序员阿江(Relakkes) 2026-05-12 13:15:54 +08:00
parent fbce0225f2
commit b438347d2d
3 changed files with 30 additions and 5 deletions

View File

@ -0,0 +1,21 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it } from 'vitest'
import { UserMessage } from './UserMessage'
describe('UserMessage', () => {
it('keeps long URLs inside the message bubble', () => {
const longUrl = `https://cn.bing.com/search?q=${'encoded'.repeat(60)}`
const { container } = render(<UserMessage content={longUrl} />)
const shell = container.querySelector('[data-message-shell="user"]')
const bubble = screen.getByText(longUrl)
expect(shell?.className).toContain('min-w-0')
expect(bubble.className).toContain('min-w-0')
expect(bubble.className).toContain('max-w-full')
expect(bubble.className).toContain('whitespace-pre-wrap')
expect(bubble.style.overflowWrap).toBe('anywhere')
expect(bubble.style.wordBreak).toBe('break-word')
})
})

View File

@ -22,8 +22,12 @@ export function UserMessage({ content, attachments }: Props) {
{hasText && (
<div
className="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' }}
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>

View File

@ -105,13 +105,13 @@ function parseMarkdown(content: string): { html: string; codeBlocks: CodeBlock[]
return { html, codeBlocks }
}
const BASE_PROSE_CLASSES = `markdown-prose prose prose-sm max-w-none text-[var(--color-text-primary)]
const BASE_PROSE_CLASSES = `markdown-prose prose prose-sm min-w-0 max-w-none break-words [overflow-wrap:anywhere] text-[var(--color-text-primary)]
prose-headings:text-[var(--color-text-primary)] prose-headings:font-semibold
prose-p:my-2 prose-p:leading-relaxed
prose-p:break-words
prose-p:break-words prose-p:[overflow-wrap:anywhere]
prose-code:text-[13px] prose-code:text-[var(--color-code-fg)] prose-code:font-[var(--font-mono)] prose-code:bg-[var(--color-code-bg)] prose-code:border prose-code:border-[var(--color-border)] prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-md prose-code:before:hidden prose-code:after:hidden
prose-pre:!bg-transparent prose-pre:!p-0 prose-pre:!shadow-none
prose-a:text-[var(--color-text-accent)] prose-a:no-underline hover:prose-a:underline
prose-a:text-[var(--color-text-accent)] prose-a:no-underline prose-a:[overflow-wrap:anywhere] hover:prose-a:underline
prose-strong:text-[var(--color-text-primary)]
prose-ul:my-2 prose-ol:my-2
prose-li:my-0.5