Ensure autoscroll reacts to UI toggles
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user