From c28c3ad7fb152ccf72a8914f0f2f725142c54516 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: Sun, 2 Aug 2026 18:34:41 +0800 Subject: [PATCH] fix(chat): clear conversation marker at latest --- .../src/components/chat/MessageList.test.tsx | 21 ++++++++-- desktop/src/components/chat/MessageList.tsx | 38 ++++++++++--------- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/desktop/src/components/chat/MessageList.test.tsx b/desktop/src/components/chat/MessageList.test.tsx index 612bb315..4c804555 100644 --- a/desktop/src/components/chat/MessageList.test.tsx +++ b/desktop/src/components/chat/MessageList.test.tsx @@ -658,9 +658,9 @@ describe('MessageList nested tool calls', () => { const { container } = render() const scroller = container.querySelector('.chat-scroll-area') as HTMLElement - let scrollTop = 0 + let scrollTop = 2 Object.defineProperty(scroller, 'clientHeight', { configurable: true, value: 200 }) - Object.defineProperty(scroller, 'scrollHeight', { configurable: true, value: 450 }) + Object.defineProperty(scroller, 'scrollHeight', { configurable: true, value: 600 }) Object.defineProperty(scroller, 'scrollTop', { configurable: true, get: () => scrollTop, @@ -673,9 +673,15 @@ describe('MessageList nested tool calls', () => { scrollTop = 250 fireEvent.scroll(scroller) expect(screen.getByRole('button', { name: /Turn 2 of 4: Second prompt/ }).getAttribute('aria-current')).toBe('location') + + scrollTop = 400 + fireEvent.scroll(scroller) + expect(screen.getAllByRole('button', { name: /Turn \d of 4/ }).every((marker) => ( + marker.getAttribute('aria-current') === null + ))).toBe(true) }) - it('keeps a clicked turn active until the user scrolls again', () => { + it('keeps a clicked turn active until user scrolling resumes, then clears it at latest', () => { useChatStore.setState({ sessions: { [ACTIVE_TAB]: makeSessionState({ @@ -714,6 +720,15 @@ describe('MessageList nested tool calls', () => { scrollTop = 0 fireEvent.scroll(scroller) expect(screen.getByRole('button', { name: /Turn 1 of 4: First prompt/ }).getAttribute('aria-current')).toBe('location') + + fireEvent.click(thirdTurn) + expect(thirdTurn.getAttribute('aria-current')).toBe('location') + fireEvent.wheel(scroller, { deltaY: 100 }) + scrollTop = 700 + fireEvent.scroll(scroller) + expect(screen.getAllByRole('button', { name: /Turn \d of 4/ }).every((marker) => ( + marker.getAttribute('aria-current') === null + ))).toBe(true) }) it('mounts and highlights a far virtualized message selected from the navigator', async () => { diff --git a/desktop/src/components/chat/MessageList.tsx b/desktop/src/components/chat/MessageList.tsx index 1d789c2e..2ea9be4d 100644 --- a/desktop/src/components/chat/MessageList.tsx +++ b/desktop/src/components/chat/MessageList.tsx @@ -1643,7 +1643,7 @@ export function MessageList({ sessionId, compact = false, mobileLayout = false } const [branchingMessageId, setBranchingMessageId] = useState(null) const [rewindingTurnId, setRewindingTurnId] = useState(null) const [turnUndoConfirmTargetId, setTurnUndoConfirmTargetId] = useState(null) - const [showJumpToLatest, setShowJumpToLatest] = useState(false) + const [isAwayFromLatest, setIsAwayFromLatest] = useState(false) const [virtualViewport, setVirtualViewport] = useState({ scrollTop: SCROLL_BOTTOM_SENTINEL, viewportHeight: VIRTUAL_DEFAULT_VIEWPORT_HEIGHT, @@ -1741,7 +1741,7 @@ export function MessageList({ sessionId, compact = false, mobileLayout = false } wasAtBottom: true, }) } - setShowJumpToLatest(false) + setIsAwayFromLatest(false) // Reset flag after the scroll event(s) from scrollIntoView have fired requestAnimationFrame(() => { const latestContainer = scrollContainerRef.current @@ -1827,7 +1827,7 @@ export function MessageList({ sessionId, compact = false, mobileLayout = false } if (isPermissionLayoutShift) return shouldAutoScrollRef.current = isAtBottom - setShowJumpToLatest(!isAtBottom) + setIsAwayFromLatest(!isAtBottom) if (resolvedSessionId) { rememberSessionScroll(resolvedSessionId, container) @@ -1842,7 +1842,7 @@ export function MessageList({ sessionId, compact = false, mobileLayout = false } markUserScrollIntent() if (event.deltaY < 0) { shouldAutoScrollRef.current = false - setShowJumpToLatest(true) + setIsAwayFromLatest(true) } }, [markUserScrollIntent]) @@ -1862,7 +1862,7 @@ export function MessageList({ sessionId, compact = false, mobileLayout = false } markUserScrollIntent() if (isUpwardScrollKey) { shouldAutoScrollRef.current = false - setShowJumpToLatest(true) + setIsAwayFromLatest(true) } }, [markUserScrollIntent]) @@ -1895,7 +1895,7 @@ export function MessageList({ sessionId, compact = false, mobileLayout = false } scrollTop: snapshot.scrollTop, viewportHeight: container.clientHeight || current.viewportHeight || VIRTUAL_DEFAULT_VIEWPORT_HEIGHT, })) - setShowJumpToLatest(true) + setIsAwayFromLatest(true) } else if (container) { // Switch to a session we were at the bottom of (or first visit): write // the bottom sentinel without going through scrollToBottom's read path, @@ -1909,7 +1909,7 @@ export function MessageList({ sessionId, compact = false, mobileLayout = false } scrollTop: SCROLL_BOTTOM_SENTINEL, viewportHeight: container.clientHeight || current.viewportHeight || VIRTUAL_DEFAULT_VIEWPORT_HEIGHT, })) - setShowJumpToLatest(false) + setIsAwayFromLatest(false) if (resolvedSessionId) { sessionScrollSnapshots.set(resolvedSessionId, { scrollTop: container.scrollTop, @@ -1942,7 +1942,7 @@ export function MessageList({ sessionId, compact = false, mobileLayout = false } useEffect(() => { if (!shouldAutoScrollRef.current) { - setShowJumpToLatest(true) + setIsAwayFromLatest(true) return } @@ -2077,13 +2077,15 @@ export function MessageList({ sessionId, compact = false, mobileLayout = false } [measuredItemsVersion, renderItemKeys, renderItemMetrics, renderItems, virtualViewport], ) const activeConversationNavigationItemId = useMemo( - () => getActiveConversationNavigationItemId( - conversationNavigationItems, - virtualTranscriptWindow.offsets, - virtualViewport.scrollTop, - virtualViewport.viewportHeight, - ), - [conversationNavigationItems, virtualTranscriptWindow.offsets, virtualViewport], + () => isAwayFromLatest + ? getActiveConversationNavigationItemId( + conversationNavigationItems, + virtualTranscriptWindow.offsets, + virtualViewport.scrollTop, + virtualViewport.viewportHeight, + ) + : null, + [conversationNavigationItems, isAwayFromLatest, virtualTranscriptWindow.offsets, virtualViewport], ) const visibleConversationNavigationItemId = programmaticNavigationItemId && conversationNavigationItems.some((item) => item.id === programmaticNavigationItemId) @@ -2349,7 +2351,7 @@ export function MessageList({ sessionId, compact = false, mobileLayout = false } const isNearby = Math.abs(container.scrollTop - targetScrollTop) <= viewportHeight * 1.25 shouldAutoScrollRef.current = false - setShowJumpToLatest(true) + setIsAwayFromLatest(true) ignoreProgrammaticScrollUntilRef.current = performance.now() + 250 ignoreProgrammaticScrollTopRef.current = targetScrollTop @@ -2400,7 +2402,7 @@ export function MessageList({ sessionId, compact = false, mobileLayout = false } setActiveConversationFindMatch(match) shouldAutoScrollRef.current = false - setShowJumpToLatest(true) + setIsAwayFromLatest(true) ignoreProgrammaticScrollUntilRef.current = performance.now() + 250 ignoreProgrammaticScrollTopRef.current = targetScrollTop setScrollTopWithoutLayoutRead(container, targetScrollTop) @@ -2695,7 +2697,7 @@ export function MessageList({ sessionId, compact = false, mobileLayout = false } /> ) : null} - {showJumpToLatest && ( + {isAwayFromLatest && (