fix: dismiss chat selection bubble on scroll (#672)

The chat add-to-session popover is anchored to viewport coordinates, so it can float over unrelated messages after the transcript scrolls. The shared selection-popover dismissal hook now treats captured scroll as a dismissal signal, matching the existing outside-click behavior and clearing the stale text selection.\n\nConstraint: Desktop selection popovers are owned by message/workspace child components, not by the transcript scroll container.\nRejected: Thread scroll state through every selectable message | would re-render message rows during scroll for a global dismissal concern.\nConfidence: high\nScope-risk: narrow\nDirective: Keep selection popover dismissal centralized unless a future menu needs scroll persistence explicitly.\nTested: bun run test -- src/components/chat/MessageList.test.tsx -t "dismisses the selected-message action when the message list scrolls"\nTested: bun run test -- src/components/chat/MessageList.test.tsx\nTested: bun run check:desktop
This commit is contained in:
程序员阿江(Relakkes) 2026-06-01 16:48:15 +08:00
parent 80b2e98f8d
commit e9e4f3e1e5
2 changed files with 45 additions and 3 deletions

View File

@ -1715,6 +1715,36 @@ describe('MessageList nested tool calls', () => {
expect(window.getSelection()?.toString()).toBe('')
})
it('dismisses the selected-message action when the message list scrolls', async () => {
useChatStore.setState({
sessions: {
[ACTIVE_TAB]: makeSessionState({
messages: [{
id: 'assistant-1',
type: 'assistant_text',
content: 'Scrolling should clear this selected reply.',
timestamp: 1,
}],
}),
},
})
const { container } = render(<MessageList />)
const assistantText = screen.getByText(/Scrolling should clear/)
await selectMessageText(assistantText, 'selected reply')
expect(screen.getByRole('button', { name: 'Add to chat' })).toBeTruthy()
const scroller = container.querySelector('.overflow-y-auto') as HTMLDivElement
await act(async () => {
fireEvent.scroll(scroller)
await Promise.resolve()
})
expect(screen.queryByRole('button', { name: 'Add to chat' })).toBeNull()
expect(window.getSelection()?.toString()).toBe('')
})
it('keeps only the latest selected-message action when selecting across messages', async () => {
useChatStore.setState({
sessions: {

View File

@ -76,6 +76,11 @@ export function useSelectionPopoverDismiss({
useEffect(() => {
if (!active) return
const dismiss = () => {
onDismiss()
clearWindowSelection()
}
const handlePointerDown = (event: PointerEvent) => {
const popover = popoverRef.current
const target = event.target
@ -83,11 +88,18 @@ export function useSelectionPopoverDismiss({
return
}
onDismiss()
clearWindowSelection()
dismiss()
}
const handleScroll = () => {
dismiss()
}
document.addEventListener('pointerdown', handlePointerDown, true)
return () => document.removeEventListener('pointerdown', handlePointerDown, true)
document.addEventListener('scroll', handleScroll, true)
return () => {
document.removeEventListener('pointerdown', handlePointerDown, true)
document.removeEventListener('scroll', handleScroll, true)
}
}, [active, onDismiss, popoverRef])
}