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) {