From 5871af11d40b15bbccd944a266beba7c2a15cbee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Fri, 15 May 2026 20:41:28 +0800 Subject: [PATCH] Always reveal newly sent chat prompts Sending from history must move the transcript to the new turn even if session state has not yet advanced from idle. The previous guard tied the jump to chatState, so a user_text tail that landed before thinking/streaming would be skipped permanently because the tail id no longer changed. Constraint: The same MessageList path serves desktop and H5 chat. Rejected: Wait for chatState to become thinking before scrolling | that misses updates where the message id is already recorded. Confidence: high Scope-risk: narrow Directive: New user_text tail messages are intentional navigation events and must override historical scroll position. Tested: cd desktop && bun run test -- MessageList.test.tsx Tested: cd desktop && bun run lint Tested: cd desktop && bun run build Tested: git diff --check --- .../src/components/chat/MessageList.test.tsx | 92 +++++++++++++++++++ desktop/src/components/chat/MessageList.tsx | 4 +- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/desktop/src/components/chat/MessageList.test.tsx b/desktop/src/components/chat/MessageList.test.tsx index a5d8f151..25a92cf7 100644 --- a/desktop/src/components/chat/MessageList.test.tsx +++ b/desktop/src/components/chat/MessageList.test.tsx @@ -1342,6 +1342,98 @@ describe('MessageList nested tool calls', () => { expect(screen.queryByRole('button', { name: 'Latest' })).toBeNull() }) + it('jumps to the latest message when a sent prompt lands before chat state changes', async () => { + const scrollIntoView = vi.fn() + Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', { + configurable: true, + value: scrollIntoView, + }) + + useChatStore.setState({ + sessions: { + [ACTIVE_TAB]: makeSessionState({ + messages: [ + { + id: 'user-1', + type: 'user_text', + content: '历史消息', + timestamp: 1, + }, + { + id: 'assistant-1', + type: 'assistant_text', + content: '历史回复', + timestamp: 2, + }, + ], + }), + }, + }) + + const { container } = render() + const scroller = container.querySelector('.overflow-y-auto') as HTMLDivElement + let scrollTop = 120 + Object.defineProperty(scroller, 'scrollHeight', { configurable: true, value: 1000 }) + Object.defineProperty(scroller, 'clientHeight', { configurable: true, value: 400 }) + Object.defineProperty(scroller, 'scrollTop', { + configurable: true, + get: () => scrollTop, + set: (value) => { + scrollTop = value + }, + }) + + scrollIntoView.mockClear() + await waitForProgrammaticScrollReset() + fireEvent.scroll(scroller) + expect(screen.getByRole('button', { name: 'Latest' })).toBeTruthy() + + act(() => { + useChatStore.setState((state) => ({ + sessions: { + ...state.sessions, + [ACTIVE_TAB]: { + ...state.sessions[ACTIVE_TAB]!, + chatState: 'idle', + messages: [ + ...state.sessions[ACTIVE_TAB]!.messages, + { + id: 'user-2', + type: 'user_text', + content: '刚发送的问题', + timestamp: 3, + }, + ], + }, + }, + })) + }) + + await waitFor(() => { + expect(screen.getByText('刚发送的问题')).toBeTruthy() + }) + expect(scrollIntoView).not.toHaveBeenCalled() + expect(scrollTop).toBe(600) + expect(screen.queryByRole('button', { name: 'Latest' })).toBeNull() + + act(() => { + useChatStore.setState((state) => ({ + sessions: { + ...state.sessions, + [ACTIVE_TAB]: { + ...state.sessions[ACTIVE_TAB]!, + chatState: 'thinking', + }, + }, + })) + }) + + await waitFor(() => { + expect(screen.getByText('刚发送的问题')).toBeTruthy() + }) + expect(scrollTop).toBe(600) + }) + it('keeps user actions anchored to the right bubble and assistant actions to the left bubble', () => { useChatStore.setState({ sessions: { diff --git a/desktop/src/components/chat/MessageList.tsx b/desktop/src/components/chat/MessageList.tsx index 5919a0c5..5bb99da3 100644 --- a/desktop/src/components/chat/MessageList.tsx +++ b/desktop/src/components/chat/MessageList.tsx @@ -646,10 +646,10 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = { lastTailMessageIdBySessionRef.current.set(resolvedSessionId, tailMessageId) if (previousTailMessageId === undefined || previousTailMessageId === tailMessageId) return - if (tailMessageType === 'user_text' && chatState !== 'idle') { + if (tailMessageType === 'user_text') { scrollToBottom('auto') } - }, [chatState, resolvedSessionId, scrollToBottom, tailMessageId, tailMessageType]) + }, [resolvedSessionId, scrollToBottom, tailMessageId, tailMessageType]) useEffect(() => { if (!shouldAutoScrollRef.current) {