fix: stabilize chat history prepend scroll preservation on mobile and desktop

- Mobile: defeat iOS momentum scroll when compensating history prepend
  (overflow toggle + short rAF watchdog); disable history virtualization
  and post-paint background prepends; preload Markdown renderer and use
  plain-text Suspense fallback to avoid first-frame geometry shifts
- Desktop: stop double-compensating prepends on the virtualized list -
  virtua shift owns the adjustment; remove sticky-anchor heuristics that
  misfired as failed restores
- Sync: skip no-op store writes when messages/parts are unchanged
This commit is contained in:
Bohdan Triapitsyn
2026-07-03 01:34:04 +03:00
parent 3bd785a10a
commit d71aec54db
7 changed files with 286 additions and 40 deletions
+12 -8
View File
@@ -399,7 +399,12 @@ export function useSync() {
complete: merged.complete,
loading: false,
})
store.setState({ message: materialized.message, part: materialized.part })
if (materialized.messagesChanged || materialized.partsChanged) {
store.setState({
...(materialized.messagesChanged ? { message: materialized.message } : {}),
...(materialized.partsChanged ? { part: materialized.part } : {}),
})
}
setSessionPrefetch({
directory,
sessionID,
@@ -498,13 +503,12 @@ export function useSync() {
shouldLoadMessages ? loadMessages(sessionID, { isStale }) : Promise.resolve(),
])
// Progressive mount: after the initial page resolves, if the session
// isn't stale and the server indicated more messages, dispatch a
// second fetch to prepend older history. The user sees the first page
// immediately; the rest arrive shortly after. This gives the scroll
// container headroom above the viewport so the "load older on
// scroll-up" trigger fires before the user hits the absolute top.
if (!isStale()) {
// Progressive mount on desktop: after the initial page resolves, if the
// session isn't stale and the server indicated more messages, dispatch a
// second fetch to prepend older history. Mobile avoids this background
// prepend because adding rows after first paint on a narrow viewport can
// visibly shift the timeline; user scroll still loads older history.
if (!isStale() && !isMobileSurfaceRuntime()) {
const currentMeta = getMetaFor(sessionID)
if (currentMeta.cursor && !currentMeta.complete) {
loadMessages(sessionID, { before: currentMeta.cursor, mode: "prepend", isStale })