fix(chat): clear conversation marker at latest

This commit is contained in:
程序员阿江(Relakkes)
2026-08-02 18:34:41 +08:00
parent a26813036e
commit c28c3ad7fb
2 changed files with 38 additions and 21 deletions
@@ -658,9 +658,9 @@ describe('MessageList nested tool calls', () => {
const { container } = render(<MessageList />)
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 () => {
+20 -18
View File
@@ -1643,7 +1643,7 @@ export function MessageList({ sessionId, compact = false, mobileLayout = false }
const [branchingMessageId, setBranchingMessageId] = useState<string | null>(null)
const [rewindingTurnId, setRewindingTurnId] = useState<string | null>(null)
const [turnUndoConfirmTargetId, setTurnUndoConfirmTargetId] = useState<string | null>(null)
const [showJumpToLatest, setShowJumpToLatest] = useState(false)
const [isAwayFromLatest, setIsAwayFromLatest] = useState(false)
const [virtualViewport, setVirtualViewport] = useState<VirtualViewport>({
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 && (
<Button
variant="secondary"
size="md"