Preserve session scroll when returning

This commit is contained in:
Shantur Rathore
2025-12-09 21:29:48 +00:00
parent 67a12d6126
commit c323667729
2 changed files with 40 additions and 9 deletions

View File

@@ -125,6 +125,7 @@ export default function MessageSection(props: MessageSectionProps) {
const [activeMessageId, setActiveMessageId] = createSignal<string | null>(null)
const changeToken = createMemo(() => String(sessionRevision()))
const isActive = createMemo(() => props.isActive !== false)
const scrollCache = useScrollCache({
@@ -236,11 +237,12 @@ export default function MessageSection(props: MessageSectionProps) {
})
}
function scrollToBottom(immediate = false) {
function scrollToBottom(immediate = false, options?: { suppressAutoAnchor?: boolean }) {
if (!containerRef) return
const sentinel = bottomSentinel()
const behavior = immediate ? "auto" : "smooth"
if (!immediate) {
const suppressAutoAnchor = options?.suppressAutoAnchor ?? !immediate
if (suppressAutoAnchor) {
suppressAutoScrollOnce = true
}
sentinel?.scrollIntoView({ block: "end", inline: "nearest", behavior })
@@ -260,6 +262,10 @@ export default function MessageSection(props: MessageSectionProps) {
}
function requestScrollToBottom(immediate = true) {
if (!isActive()) {
pendingActiveScroll = true
return
}
if (!containerRef || !bottomSentinel()) {
pendingActiveScroll = true
return
@@ -277,7 +283,7 @@ export default function MessageSection(props: MessageSectionProps) {
function resolvePendingActiveScroll() {
if (!pendingActiveScroll) return
if (!props.isActive) return
if (!isActive()) return
requestScrollToBottom(true)
}
@@ -292,8 +298,15 @@ export default function MessageSection(props: MessageSectionProps) {
function scheduleAnchorScroll(immediate = false) {
if (!autoScroll()) return
if (!isActive()) {
pendingActiveScroll = true
return
}
const sentinel = bottomSentinel()
if (!sentinel) return
if (!sentinel) {
pendingActiveScroll = true
return
}
if (pendingAnchorScroll !== null) {
cancelAnimationFrame(pendingAnchorScroll)
pendingAnchorScroll = null
@@ -415,9 +428,14 @@ export default function MessageSection(props: MessageSectionProps) {
let lastActiveState = false
createEffect(() => {
const active = Boolean(props.isActive)
if (active && !lastActiveState) {
requestScrollToBottom(true)
const active = isActive()
if (active) {
resolvePendingActiveScroll()
if (!lastActiveState && autoScroll()) {
requestScrollToBottom(true)
}
} else if (autoScroll()) {
pendingActiveScroll = true
}
lastActiveState = active
})
@@ -670,7 +688,7 @@ export default function MessageSection(props: MessageSectionProps) {
onFork={props.onFork}
onContentRendered={handleContentRendered}
setBottomSentinel={setBottomSentinel}
suspendMeasurements={() => props.isActive === false}
suspendMeasurements={() => !isActive()}
onInitialRenderComplete={handleInitialRenderComplete}
/>
@@ -688,7 +706,7 @@ export default function MessageSection(props: MessageSectionProps) {
<button
type="button"
class="message-scroll-button"
onClick={() => scrollToBottom()}
onClick={() => scrollToBottom(false, { suppressAutoAnchor: false })}
aria-label="Scroll to latest message"
>
<span class="message-scroll-icon" aria-hidden="true"></span>

View File

@@ -167,6 +167,18 @@ export default function VirtualItem(props: VirtualItemProps) {
return
}
const normalized = nextHeight
const previous = sizeCache.get(props.cacheKey) ?? measuredHeight()
const shouldKeepPrevious = previous > 0 && (normalized === 0 || (normalized > 0 && normalized < previous))
if (shouldKeepPrevious) {
if (!hasReportedMeasurement) {
hasReportedMeasurement = true
props.onMeasured?.()
}
setHasMeasured(true)
sizeCache.set(props.cacheKey, previous)
setMeasuredHeight(previous)
return
}
if (normalized > 0) {
sizeCache.set(props.cacheKey, normalized)
setHasMeasured(true)
@@ -262,6 +274,7 @@ export default function VirtualItem(props: VirtualItemProps) {
})
createEffect(() => {
measurementsSuspended()
const root = props.scrollContainer ? props.scrollContainer() : null
refreshIntersectionObserver(root ?? null)
})