refactor(sync): replace message reloads with incremental tail sync

This commit is contained in:
weishu
2026-07-28 12:20:53 +08:00
parent 2235b924a7
commit faf70c64dd
28 changed files with 2927 additions and 2077 deletions
@@ -5,10 +5,13 @@ import { I18nProvider } from '@/lib/i18n-context'
import {
ConversationOutlinePanel,
captureScrollAnchor,
getHistoryCoverageRetryDelay,
getScrollIntent,
loadOlderUntilViewportCovered,
locateOutlineTargetMessage,
prependMissingUserSnapshot,
restoreScrollAnchor,
shouldLoadOlderForViewport,
shouldCancelInitialScrollSettling,
} from '@/components/AssistantChat/HappyThread'
import type { ConversationOutlineItem } from '@/chat/outline'
@@ -230,6 +233,89 @@ describe('scroll anchor helpers', () => {
})
})
describe('viewport-driven history coverage', () => {
it('recognizes an underfilled viewport and a top sentinel inside the preload margin', () => {
expect(shouldLoadOlderForViewport({
scrollHeight: 300,
clientHeight: 500,
viewportTop: 100,
sentinelTop: 100,
sentinelBottom: 101
})).toBe(true)
expect(shouldLoadOlderForViewport({
scrollHeight: 1_000,
clientHeight: 500,
viewportTop: 100,
sentinelTop: -200,
sentinelBottom: -199
})).toBe(false)
})
it('loads raw pages until rendered DOM grows by roughly one viewport', async () => {
let scrollHeight = 100
const growthByPage = [0, 250, 300]
const loadOlderPage = vi.fn(async () => {
scrollHeight += growthByPage.shift() ?? 0
return true
})
const loaded = await loadOlderUntilViewportCovered({
hasMoreMessages: () => true,
needsCoverage: () => true,
getScrollHeight: () => scrollHeight,
getClientHeight: () => 500,
loadOlderPage,
waitForRender: async () => {}
})
expect(loaded).toBe(3)
expect(loadOlderPage).toHaveBeenCalledTimes(3)
expect(scrollHeight).toBe(650)
})
it('stops when history is exhausted even if raw pages produced no DOM growth', async () => {
let remainingPages = 2
const loadOlderPage = vi.fn(async () => {
remainingPages -= 1
return true
})
const loaded = await loadOlderUntilViewportCovered({
hasMoreMessages: () => remainingPages > 0,
needsCoverage: () => true,
getScrollHeight: () => 100,
getClientHeight: () => 500,
loadOlderPage,
waitForRender: async () => {}
})
expect(loaded).toBe(2)
expect(loadOlderPage).toHaveBeenCalledTimes(2)
})
it('caps one gesture when pages keep producing no rendered output', async () => {
const loadOlderPage = vi.fn(async () => true)
const loaded = await loadOlderUntilViewportCovered({
hasMoreMessages: () => true,
needsCoverage: () => true,
getScrollHeight: () => 100,
getClientHeight: () => 500,
loadOlderPage,
waitForRender: async () => {},
maxPages: 4
})
expect(loaded).toBe(4)
expect(loadOlderPage).toHaveBeenCalledTimes(4)
})
it('defers an intersection signal until the initial scroll-settling deadline', () => {
expect(getHistoryCoverageRetryDelay(2_800, 1_000)).toBe(1_816)
expect(getHistoryCoverageRetryDelay(900, 1_000)).toBe(16)
})
})
describe('outline target loading', () => {
it('loads older messages through the scroll-preserving wrapper until the target appears', async () => {
const loadOlderPreservingScroll = vi.fn<() => Promise<boolean>>()