fix: keep opened conversations pinned to bottom

This commit is contained in:
mesalogo
2026-08-19 09:31:40 +08:00
parent ccaab25d11
commit c41d7e8b96
2 changed files with 103 additions and 0 deletions
+77
View File
@@ -1539,6 +1539,83 @@ describe('App', () => {
).not.toBeInTheDocument() ).not.toBeInTheDocument()
}) })
it('keeps a newly opened persisted conversation at the bottom while its content settles', async () => {
let resizeCallback: ResizeObserverCallback | undefined
const disconnect = vi.fn()
vi.stubGlobal(
'ResizeObserver',
class {
constructor(callback: ResizeObserverCallback) {
resizeCallback = callback
}
observe(): void {}
unobserve(): void {}
disconnect(): void {
disconnect()
}
}
)
vi.mocked(api.conversations.list).mockResolvedValueOnce([
{
id: '00000000-0000-4000-8000-000000000417',
projectId,
title: '已有会话',
updatedAt: 1_775_000_000_000,
messages: [
{
id: '00000000-0000-4000-8000-000000000418',
role: 'user',
content: '最后一个用户问题',
createdAt: 1_775_000_000_000,
state: 'complete'
},
{
id: '00000000-0000-4000-8000-000000000419',
role: 'assistant',
content: '加载后会继续完成布局的回复',
createdAt: 1_775_000_000_001,
state: 'complete'
}
]
}
])
const { container } = render(<App />)
expect(
await screen.findByText('加载后会继续完成布局的回复')
).toBeInTheDocument()
const chat = container.querySelector<HTMLElement>('.chat')
if (!chat || !resizeCallback) {
throw new Error('Missing chat resize observer')
}
let scrollHeight = 1_200
Object.defineProperties(chat, {
clientHeight: { configurable: true, value: 400 },
scrollHeight: {
configurable: true,
get: () => scrollHeight
},
scrollTop: { configurable: true, writable: true, value: 800 }
})
const scrollTo = vi.fn()
chat.scrollTo = scrollTo
scrollHeight = 1_800
act(() => resizeCallback?.([], {} as ResizeObserver))
expect(scrollTo).toHaveBeenLastCalledWith({
top: 1_800,
behavior: 'auto'
})
chat.scrollTop = 100
fireEvent.scroll(chat)
scrollTo.mockClear()
scrollHeight = 2_200
act(() => resizeCallback?.([], {} as ResizeObserver))
expect(scrollTo).not.toHaveBeenCalled()
})
it('renders the latest 80 messages and preserves scroll when revealing earlier messages', async () => { it('renders the latest 80 messages and preserves scroll when revealing earlier messages', async () => {
vi.mocked(api.conversations.list).mockResolvedValueOnce([ vi.mocked(api.conversations.list).mockResolvedValueOnce([
{ {
+26
View File
@@ -817,6 +817,32 @@ function ChatHistoryPane({
visibleMessageCount visibleMessageCount
]) ])
useLayoutEffect(() => {
if (!active || typeof ResizeObserver !== 'function') {
return
}
const scrollContainer = scrollRef.current
const messageList =
scrollContainer?.querySelector<HTMLElement>('.message-list')
if (!scrollContainer || !messageList) {
return
}
const observer = new ResizeObserver(() => {
if (
restorePendingRef.current ||
!pinnedToBottomRef.current
) {
return
}
scrollContainer.scrollTo({
top: scrollContainer.scrollHeight,
behavior: 'auto'
})
})
observer.observe(messageList)
return () => observer.disconnect()
}, [active])
useLayoutEffect(() => { useLayoutEffect(() => {
const previous = prependScrollPositionRef.current const previous = prependScrollPositionRef.current
if (!previous) { if (!previous) {