Ensure autoscroll reacts to UI toggles

This commit is contained in:
Shantur Rathore
2025-11-27 19:20:55 +00:00
parent cc45c16d73
commit 042a45db0d
3 changed files with 51 additions and 15 deletions

View File

@@ -222,7 +222,16 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
} }
}) })
const preferenceSignature = createMemo(() => {
const pref = preferences()
const showThinking = pref.showThinkingBlocks ? 1 : 0
const thinkingExpansion = pref.thinkingBlocksExpansion ?? "expanded"
const showUsage = (pref.showUsageMetrics ?? true) ? 1 : 0
return `${showThinking}|${thinkingExpansion}|${showUsage}`
})
const connectionStatus = () => sseManager.getStatus(props.instanceId) const connectionStatus = () => sseManager.getStatus(props.instanceId)
const handleCommandPaletteClick = () => { const handleCommandPaletteClick = () => {
showCommandPalette(props.instanceId) showCommandPalette(props.instanceId)
} }
@@ -391,7 +400,7 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
orderedParts.forEach((part, partIndex) => { orderedParts.forEach((part, partIndex) => {
if (part.type === "tool") { if (part.type === "tool") {
flushContent() flushContent()
const partVersion = typeof part.version === "number" ? part.version : 0 const partRevision = typeof part.revision === "number" ? part.revision : 0
const messageVersion = record.revision const messageVersion = record.revision
const key = `${record.id}:${part.id ?? partIndex}` const key = `${record.id}:${part.id ?? partIndex}`
let toolItem = sessionCache.toolItems.get(key) let toolItem = sessionCache.toolItems.get(key)
@@ -403,7 +412,7 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
messageInfo, messageInfo,
messageId: record.id, messageId: record.id,
messageVersion, messageVersion,
partVersion, partRevision,
} }
sessionCache.toolItems.set(key, toolItem) sessionCache.toolItems.set(key, toolItem)
} else { } else {
@@ -412,7 +421,7 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
toolItem.messageInfo = messageInfo toolItem.messageInfo = messageInfo
toolItem.messageId = record.id toolItem.messageId = record.id
toolItem.messageVersion = messageVersion toolItem.messageVersion = messageVersion
toolItem.partVersion = partVersion toolItem.partRevision = partRevision
} }
items.push(toolItem) items.push(toolItem)
usedToolKeys.add(key) usedToolKeys.add(key)
@@ -510,12 +519,12 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
if (!lastItem) { if (!lastItem) {
tailSignature = `msg:${lastBlock.record.id}:${lastBlock.record.revision}` tailSignature = `msg:${lastBlock.record.id}:${lastBlock.record.revision}`
} else if (lastItem.type === "tool") { } else if (lastItem.type === "tool") {
tailSignature = `tool:${lastItem.key}:${lastItem.partVersion}` tailSignature = `tool:${lastItem.key}:${lastItem.partRevision}`
} else if (lastItem.type === "content") { } else if (lastItem.type === "content") {
tailSignature = `content:${lastItem.key}:${lastBlock.record.revision}` tailSignature = `content:${lastItem.key}:${lastBlock.record.revision}`
} else { } else {
const version = typeof lastItem.part.version === "number" ? lastItem.part.version : 0 const revision = typeof lastItem.part.revision === "number" ? lastItem.part.revision : lastBlock.record.revision
tailSignature = `step:${lastItem.key}:${version}` tailSignature = `step:${lastItem.key}:${revision}`
} }
return `${revisionValue}:${tailSignature}` return `${revisionValue}:${tailSignature}`
}) })
@@ -531,6 +540,7 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
const [showScrollBottomButton, setShowScrollBottomButton] = createSignal(false) const [showScrollBottomButton, setShowScrollBottomButton] = createSignal(false)
let containerRef: HTMLDivElement | undefined let containerRef: HTMLDivElement | undefined
let lastKnownScrollTop = 0 let lastKnownScrollTop = 0
let lastMeasuredScrollHeight = 0
let pendingScrollFrame: number | null = null let pendingScrollFrame: number | null = null
let userScrollIntentUntil = 0 let userScrollIntentUntil = 0
let detachScrollIntentListeners: (() => void) | undefined let detachScrollIntentListeners: (() => void) | undefined
@@ -572,6 +582,7 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
function setContainerRef(element: HTMLDivElement | null) { function setContainerRef(element: HTMLDivElement | null) {
containerRef = element || undefined containerRef = element || undefined
lastKnownScrollTop = containerRef?.scrollTop ?? 0 lastKnownScrollTop = containerRef?.scrollTop ?? 0
lastMeasuredScrollHeight = containerRef?.scrollHeight ?? 0
attachScrollIntentListeners(containerRef) attachScrollIntentListeners(containerRef)
} }
@@ -597,12 +608,18 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
if (!containerRef) return if (!containerRef) return
containerRef.scrollTo({ top: containerRef.scrollHeight, behavior }) containerRef.scrollTo({ top: containerRef.scrollHeight, behavior })
setAutoScroll(true) setAutoScroll(true)
lastMeasuredScrollHeight = containerRef.scrollHeight
lastKnownScrollTop = containerRef.scrollTop lastKnownScrollTop = containerRef.scrollTop
updateScrollIndicators(containerRef) updateScrollIndicators(containerRef)
scheduleScrollPersist() scheduleScrollPersist()
}) })
} }
function scrollToBottomAndClamp(immediate = false) {
scrollToBottom(immediate)
requestAnimationFrame(() => clampScrollAfterShrink())
}
function scrollToTop(immediate = false) { function scrollToTop(immediate = false) {
if (!containerRef) return if (!containerRef) return
const behavior = immediate ? "auto" : "smooth" const behavior = immediate ? "auto" : "smooth"
@@ -610,6 +627,7 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
if (!containerRef) return if (!containerRef) return
containerRef.scrollTo({ top: 0, behavior }) containerRef.scrollTo({ top: 0, behavior })
lastMeasuredScrollHeight = containerRef.scrollHeight
lastKnownScrollTop = containerRef.scrollTop lastKnownScrollTop = containerRef.scrollTop
updateScrollIndicators(containerRef) updateScrollIndicators(containerRef)
scheduleScrollPersist() scheduleScrollPersist()
@@ -626,6 +644,18 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
}) })
} }
function clampScrollAfterShrink() {
if (!containerRef || !autoScroll()) return
const currentHeight = containerRef.scrollHeight
const clientHeight = containerRef.clientHeight
if (currentHeight < lastMeasuredScrollHeight) {
const maxScrollTop = Math.max(currentHeight - clientHeight, 0)
containerRef.scrollTo({ top: maxScrollTop, behavior: "auto" })
lastKnownScrollTop = containerRef.scrollTop
}
lastMeasuredScrollHeight = currentHeight
}
function handleScroll(event: Event) { function handleScroll(event: Event) {
@@ -642,6 +672,7 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
const movingUp = currentTop < previousTop - SCROLL_DIRECTION_THRESHOLD const movingUp = currentTop < previousTop - SCROLL_DIRECTION_THRESHOLD
const movingDown = currentTop > previousTop + SCROLL_DIRECTION_THRESHOLD const movingDown = currentTop > previousTop + SCROLL_DIRECTION_THRESHOLD
lastKnownScrollTop = currentTop lastKnownScrollTop = currentTop
lastMeasuredScrollHeight = containerRef.scrollHeight
const atBottom = isNearBottom(containerRef) const atBottom = isNearBottom(containerRef)
if (isUserScroll) { if (isUserScroll) {
if (movingUp && !atBottom && autoScroll()) { if (movingUp && !atBottom && autoScroll()) {
@@ -667,6 +698,7 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
const atBottom = isNearBottom(target) const atBottom = isNearBottom(target)
setAutoScroll(atBottom) setAutoScroll(atBottom)
} }
lastMeasuredScrollHeight = target.scrollHeight
updateScrollIndicators(target) updateScrollIndicators(target)
}, },
}) })
@@ -675,17 +707,24 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
let previousToken: string | undefined let previousToken: string | undefined
createEffect(() => { createEffect(() => {
const token = changeToken() const token = changeToken()
if (!token || token === previousToken) { if (!token || token === previousToken) {
return return
} }
previousToken = token previousToken = token
if (autoScroll()) { if (autoScroll()) {
scrollToBottom(true) scrollToBottomAndClamp(true)
} }
}) })
createEffect(() => {
preferenceSignature()
if (!autoScroll()) {
return
}
scrollToBottomAndClamp(true)
})
createEffect(() => { createEffect(() => {
if (messageRecords().length === 0) { if (messageRecords().length === 0) {
setShowScrollTopButton(false) setShowScrollTopButton(false)
@@ -694,6 +733,7 @@ export default function MessageStreamV2(props: MessageStreamV2Props) {
} }
}) })
onCleanup(() => { onCleanup(() => {
if (pendingScrollFrame !== null) { if (pendingScrollFrame !== null) {
cancelAnimationFrame(pendingScrollFrame) cancelAnimationFrame(pendingScrollFrame)

View File

@@ -404,13 +404,10 @@ export function createInstanceMessageStore(instanceId: string): InstanceMessageS
parts.forEach((part, index) => { parts.forEach((part, index) => {
const id = ensurePartId(messageId, part, index) const id = ensurePartId(messageId, part, index)
const cloned = clonePart(part) const cloned = clonePart(part)
if (typeof cloned.version !== "number") {
cloned.version = 0
}
map[id] = { map[id] = {
id, id,
data: cloned, data: cloned,
revision: cloned.version, revision: 0,
} }
ids.push(id) ids.push(id)
}) })

View File

@@ -36,7 +36,6 @@ export type ClientPart = SDKPart & {
sessionID?: string sessionID?: string
messageID?: string messageID?: string
synthetic?: boolean synthetic?: boolean
version?: number
renderCache?: RenderCache renderCache?: RenderCache
pendingPermission?: PendingPermissionState pendingPermission?: PendingPermissionState
} }