diff --git a/src/renderer/src/App.test.tsx b/src/renderer/src/App.test.tsx index 3f971ab..457c065 100644 --- a/src/renderer/src/App.test.tsx +++ b/src/renderer/src/App.test.tsx @@ -1539,6 +1539,83 @@ describe('App', () => { ).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() + + expect( + await screen.findByText('加载后会继续完成布局的回复') + ).toBeInTheDocument() + const chat = container.querySelector('.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 () => { vi.mocked(api.conversations.list).mockResolvedValueOnce([ { diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 486cea8..1fddd6a 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -817,6 +817,32 @@ function ChatHistoryPane({ visibleMessageCount ]) + useLayoutEffect(() => { + if (!active || typeof ResizeObserver !== 'function') { + return + } + const scrollContainer = scrollRef.current + const messageList = + scrollContainer?.querySelector('.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(() => { const previous = prependScrollPositionRef.current if (!previous) {