fix: prevent outer scrolling in horizontal multi-model messages (#13964)

### What this PR does

Before this PR:
In horizontal multi-model layout, long assistant responses could create
two vertical scroll areas.
When the mouse wheel was used on the card edge, header area, or other
non-content areas, the outer card wrapper could scroll and move the
whole content area out of view, leaving blank space.

After this PR:
Only the inner message content area scrolls vertically in horizontal
multi-model layout.
Scrolling on card edges or other non-content areas no longer scrolls the
outer card wrapper away.
The horizontal group container still scrolls horizontally as before.

Fixes #

### Why we need it and why it was done in this way

This is a focused UI bug fix for an accidental nested vertical scrolling
setup in horizontal multi-model messages.

The fix keeps the existing inner content scrolling behavior and height
limit, but removes the meaningless outer vertical scrolling from the
card wrapper. This keeps the change minimal and reduces regression risk.

The following tradeoffs were made:
A minimal CSS fix was chosen instead of introducing wheel-event
interception logic.

The following alternatives were considered:
Intercepting wheel events at the card level was considered, but rejected
because it would increase complexity and broaden the behavior change
beyond this bug.

Links to places where the discussion took place:
N/A

### Breaking changes

None.

### Special notes for your reviewer

A regression test was added for the horizontal multi-model layout scroll
responsibilities.

Manual verification was also performed:
- Long responses still scroll correctly inside the content area
- Scrolling on the card edge no longer moves the whole card content out
of view
- Horizontal scrolling of the group still works
- No behavioral changes were made to fold, vertical, or grid layouts

Before:

![before](https://github.com/user-attachments/assets/166b6166-9205-4097-b563-60ce7e62e78f)

After:

![after](https://github.com/user-attachments/assets/7e8cab19-cf22-4f98-9d17-a7c2e1a7f2ea)

### Checklist

- [x] PR: The PR description is expressive enough and will help future
contributors
- [x] Code: Write code that humans can understand and keep it simple
- [x] Refactor: I have left the code cleaner than I found it
- [x] Upgrade: Impact of this change on upgrade flows was considered and
not required
- [x] Documentation: A user-guide update was considered and is not
required
- [x] Self-review: I have reviewed my own code before requesting review
from others

### Release note

```release-note
Fixed a UI bug in horizontal multi-model message layout where scrolling on the card edge could scroll the outer message card instead of only the inner content area.
```

Signed-off-by: sjsjysfj <gc160514190803@163.com>
This commit is contained in:
付Jumping
2026-04-23 10:16:14 +08:00
committed by GitHub
parent c0b3c880e3
commit 09da6a3364
2 changed files with 134 additions and 1 deletions

View File

@@ -361,7 +361,7 @@ interface MessageWrapperProps {
const MessageWrapper = styled.div<MessageWrapperProps>`
&.horizontal {
padding: 1px;
overflow-y: auto;
overflow-y: visible;
.message {
height: 100%;
border: 0.5px solid var(--color-border);

View File

@@ -0,0 +1,133 @@
import type { Topic } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
import { render, screen } from '@testing-library/react'
import type { ReactNode } from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
editMessage: vi.fn(),
scrollIntoView: vi.fn(),
setTimeoutTimer: vi.fn(),
useChatContext: vi.fn().mockReturnValue({ isMultiSelectMode: false }),
useSettings: vi.fn().mockReturnValue({
multiModelMessageStyle: 'horizontal',
gridColumns: 2,
gridPopoverTrigger: 'click'
}),
EventEmitter: {
on: vi.fn(),
off: vi.fn()
},
MessageEditingProvider: vi.fn(({ children }: { children: ReactNode }) => <>{children}</>),
MessageGroupMenuBar: vi.fn(() => <div className="group-menu-bar">menu</div>),
MessageItem: vi.fn(({ message }: { message: Message }) => (
<div className="message">
<div className="message-content-container" data-testid={`content-${message.id}`}>
Long message content
</div>
<div className="MessageFooter">footer</div>
</div>
))
}))
vi.mock('@renderer/context/MessageEditingContext', () => ({
MessageEditingProvider: mocks.MessageEditingProvider
}))
vi.mock('@renderer/utils', () => ({
classNames: (items: Array<Record<string, boolean> | string | undefined>) =>
items
.flatMap((item) => {
if (!item) return []
if (typeof item === 'string') return [item]
return Object.entries(item)
.filter(([, value]) => value)
.map(([key]) => key)
})
.join(' ')
}))
vi.mock('uuid', () => ({
default: () => 'test-uuid',
v4: () => 'test-uuid'
}))
vi.mock('@renderer/hooks/useChatContext', () => ({
useChatContext: () => mocks.useChatContext()
}))
vi.mock('@renderer/hooks/useAssistant', () => ({
useAssistant: () => ({
assistant: null
})
}))
vi.mock('@renderer/hooks/useMessageOperations', () => ({
useMessageOperations: () => ({
editMessage: mocks.editMessage
})
}))
vi.mock('@renderer/hooks/useSettings', () => ({
useSettings: () => mocks.useSettings()
}))
vi.mock('@renderer/hooks/useTimer', () => ({
useTimer: () => ({
setTimeoutTimer: mocks.setTimeoutTimer
})
}))
vi.mock('@renderer/services/EventService', () => ({
EVENT_NAMES: {
LOCATE_MESSAGE: 'locate-message'
},
EventEmitter: mocks.EventEmitter
}))
vi.mock('@renderer/utils/dom', () => ({
scrollIntoView: mocks.scrollIntoView
}))
vi.mock('../Message', () => ({
default: mocks.MessageItem
}))
vi.mock('../MessageGroupMenuBar', () => ({
default: mocks.MessageGroupMenuBar
}))
const { default: MessageGroup } = await import('../MessageGroup')
const createMessage = (id: string, index: number) =>
({
id,
askId: 'ask-1',
role: 'assistant',
blocks: [],
multiModelMessageStyle: 'horizontal',
index
}) as unknown as Message & { index: number }
describe('MessageGroup', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('uses only the inner content container for vertical scrolling in horizontal layout', () => {
const messages = [createMessage('msg-1', 0), createMessage('msg-2', 1)]
const topic = { id: 'topic-1' } as Topic
render(<MessageGroup messages={messages} topic={topic} />)
const outerWrapper = document.getElementById('message-msg-1')
expect(outerWrapper).not.toBeNull()
expect(getComputedStyle(outerWrapper!).overflowY).toBe('visible')
const contentContainer = screen.getByTestId('content-msg-1')
expect(getComputedStyle(contentContainer).overflowY).toBe('auto')
const horizontalGroup = outerWrapper!.parentElement as HTMLElement
expect(getComputedStyle(horizontalGroup).overflowX).toBe('auto')
})
})