Prevent cached session remeasurements and remove logs
This commit is contained in:
@@ -26,6 +26,7 @@ interface MessageBlockListProps {
|
||||
onFork?: (messageId?: string) => void
|
||||
onContentRendered?: () => void
|
||||
setBottomSentinel: (element: HTMLDivElement | null) => void
|
||||
suspendMeasurements?: () => boolean
|
||||
}
|
||||
|
||||
export default function MessageBlockList(props: MessageBlockListProps) {
|
||||
@@ -41,6 +42,7 @@ export default function MessageBlockList(props: MessageBlockListProps) {
|
||||
threshold={VIRTUAL_ITEM_MARGIN_PX}
|
||||
placeholderClass="message-stream-placeholder"
|
||||
virtualizationEnabled={() => !props.loading}
|
||||
suspendMeasurements={props.suspendMeasurements}
|
||||
>
|
||||
|
||||
<MessageBlock
|
||||
|
||||
@@ -30,12 +30,11 @@ export interface MessageSectionProps {
|
||||
onRevert?: (messageId: string) => void
|
||||
onFork?: (messageId?: string) => void
|
||||
registerScrollToBottom?: (fn: () => void) => void
|
||||
requestScrollToBottom?: () => void
|
||||
isActive: boolean
|
||||
showSidebarToggle?: boolean
|
||||
onSidebarToggle?: () => void
|
||||
forceCompactStatusLayout?: boolean
|
||||
onQuoteSelection?: (text: string, mode: "quote" | "code") => void
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
export default function MessageSection(props: MessageSectionProps) {
|
||||
@@ -140,8 +139,8 @@ export default function MessageSection(props: MessageSectionProps) {
|
||||
const bottomSentinel = () => bottomSentinelSignal()
|
||||
const setBottomSentinel = (element: HTMLDivElement | null) => {
|
||||
setBottomSentinelSignal(element)
|
||||
resolvePendingActiveScroll()
|
||||
}
|
||||
const [scrollToBottomRequest, setScrollToBottomRequest] = createSignal(false)
|
||||
const [autoScroll, setAutoScroll] = createSignal(true)
|
||||
const [showScrollTopButton, setShowScrollTopButton] = createSignal(false)
|
||||
const [showScrollBottomButton, setShowScrollBottomButton] = createSignal(false)
|
||||
@@ -160,6 +159,9 @@ export default function MessageSection(props: MessageSectionProps) {
|
||||
let detachScrollIntentListeners: (() => void) | undefined
|
||||
let hasRestoredScroll = false
|
||||
let suppressAutoScrollOnce = false
|
||||
let pendingActiveScroll = false
|
||||
let scrollToBottomFrame: number | null = null
|
||||
let scrollToBottomDelayedFrame: number | null = null
|
||||
|
||||
function markUserScrollIntent() {
|
||||
const now = typeof performance !== "undefined" ? performance.now() : Date.now()
|
||||
@@ -197,14 +199,13 @@ export default function MessageSection(props: MessageSectionProps) {
|
||||
|
||||
function setContainerRef(element: HTMLDivElement | null) {
|
||||
containerRef = element || undefined
|
||||
if (import.meta.env?.DEV) {
|
||||
console.debug("[MessageSection] setContainerRef", props.sessionId, Boolean(containerRef))
|
||||
}
|
||||
setScrollElement(containerRef)
|
||||
attachScrollIntentListeners(containerRef)
|
||||
if (!containerRef) {
|
||||
clearQuoteSelection()
|
||||
return
|
||||
}
|
||||
resolvePendingActiveScroll()
|
||||
}
|
||||
|
||||
function setShellElement(element: HTMLDivElement | null) {
|
||||
@@ -219,9 +220,6 @@ export default function MessageSection(props: MessageSectionProps) {
|
||||
const hasItems = messageIds().length > 0
|
||||
const bottomVisible = bottomSentinelVisible()
|
||||
const topVisible = topSentinelVisible()
|
||||
if (import.meta.env?.DEV) {
|
||||
console.debug("[MessageSection] sentinel visibility", props.sessionId, { bottomVisible, topVisible })
|
||||
}
|
||||
setShowScrollBottomButton(hasItems && !bottomVisible)
|
||||
setShowScrollTopButton(hasItems && !topVisible)
|
||||
}
|
||||
@@ -238,13 +236,6 @@ export default function MessageSection(props: MessageSectionProps) {
|
||||
function scrollToBottom(immediate = false) {
|
||||
if (!containerRef) return
|
||||
const sentinel = bottomSentinel()
|
||||
if (import.meta.env?.DEV) {
|
||||
console.debug("[MessageSection] scrollToBottom", props.sessionId, {
|
||||
immediate,
|
||||
hasSentinel: Boolean(sentinel),
|
||||
bottomVisible: bottomSentinelVisible(),
|
||||
})
|
||||
}
|
||||
const behavior = immediate ? "auto" : "smooth"
|
||||
if (!immediate) {
|
||||
suppressAutoScrollOnce = true
|
||||
@@ -253,13 +244,43 @@ export default function MessageSection(props: MessageSectionProps) {
|
||||
setAutoScroll(true)
|
||||
scheduleScrollPersist()
|
||||
}
|
||||
|
||||
function clearScrollToBottomFrames() {
|
||||
if (scrollToBottomFrame !== null) {
|
||||
cancelAnimationFrame(scrollToBottomFrame)
|
||||
scrollToBottomFrame = null
|
||||
}
|
||||
if (scrollToBottomDelayedFrame !== null) {
|
||||
cancelAnimationFrame(scrollToBottomDelayedFrame)
|
||||
scrollToBottomDelayedFrame = null
|
||||
}
|
||||
}
|
||||
|
||||
function requestScrollToBottom(immediate = true) {
|
||||
if (!containerRef || !bottomSentinel()) {
|
||||
pendingActiveScroll = true
|
||||
return
|
||||
}
|
||||
pendingActiveScroll = false
|
||||
clearScrollToBottomFrames()
|
||||
scrollToBottomFrame = requestAnimationFrame(() => {
|
||||
scrollToBottomFrame = null
|
||||
scrollToBottomDelayedFrame = requestAnimationFrame(() => {
|
||||
scrollToBottomDelayedFrame = null
|
||||
scrollToBottom(immediate)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function resolvePendingActiveScroll() {
|
||||
if (!pendingActiveScroll) return
|
||||
if (!props.isActive) return
|
||||
requestScrollToBottom(true)
|
||||
}
|
||||
|
||||
function scrollToTop(immediate = false) {
|
||||
if (!containerRef) return
|
||||
const behavior = immediate ? "auto" : "smooth"
|
||||
if (import.meta.env?.DEV) {
|
||||
console.debug("[MessageSection] scrollToTop", props.sessionId, { immediate })
|
||||
}
|
||||
setAutoScroll(false)
|
||||
topSentinel()?.scrollIntoView({ block: "start", inline: "nearest", behavior })
|
||||
scheduleScrollPersist()
|
||||
@@ -378,19 +399,17 @@ export default function MessageSection(props: MessageSectionProps) {
|
||||
|
||||
createEffect(() => {
|
||||
if (props.registerScrollToBottom) {
|
||||
props.registerScrollToBottom(() => scrollToBottom(true))
|
||||
props.registerScrollToBottom(() => requestScrollToBottom(true))
|
||||
}
|
||||
})
|
||||
|
||||
let lastActiveState = false
|
||||
createEffect(() => {
|
||||
const active = props.isActive
|
||||
const container = containerRef
|
||||
if (!container) return
|
||||
if (active) {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => scrollToBottom(true)))
|
||||
} else {
|
||||
requestAnimationFrame(() => container.scrollTo({ top: 0, behavior: "auto" }))
|
||||
const active = Boolean(props.isActive)
|
||||
if (active && !lastActiveState) {
|
||||
requestScrollToBottom(true)
|
||||
}
|
||||
lastActiveState = active
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
@@ -554,6 +573,7 @@ export default function MessageSection(props: MessageSectionProps) {
|
||||
if (pendingAnchorScroll !== null) {
|
||||
cancelAnimationFrame(pendingAnchorScroll)
|
||||
}
|
||||
clearScrollToBottomFrames()
|
||||
if (detachScrollIntentListeners) {
|
||||
detachScrollIntentListeners()
|
||||
}
|
||||
@@ -626,6 +646,7 @@ export default function MessageSection(props: MessageSectionProps) {
|
||||
onFork={props.onFork}
|
||||
onContentRendered={handleContentRendered}
|
||||
setBottomSentinel={setBottomSentinel}
|
||||
suspendMeasurements={() => props.isActive === false}
|
||||
/>
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Show, createMemo, createEffect, createSignal, type Component } from "solid-js"
|
||||
import { Show, createMemo, createEffect, type Component } from "solid-js"
|
||||
import type { Session } from "../../types/session"
|
||||
import type { Attachment } from "../../types/attachment"
|
||||
import type { ClientPart } from "../../types/message"
|
||||
@@ -26,25 +26,29 @@ interface SessionViewProps {
|
||||
showSidebarToggle?: boolean
|
||||
onSidebarToggle?: () => void
|
||||
forceCompactStatusLayout?: boolean
|
||||
isActive: boolean
|
||||
isActive?: boolean
|
||||
}
|
||||
|
||||
export const SessionView: Component<SessionViewProps> = (props) => {
|
||||
const session = () => props.activeSessions.get(props.sessionId)
|
||||
const messagesLoading = createMemo(() => isSessionMessagesLoading(props.instanceId, props.sessionId))
|
||||
const messageStore = createMemo(() => messageStoreBus.getOrCreate(props.instanceId))
|
||||
const [scrollToBottomHandle, setScrollToBottomHandle] = createSignal<(() => void) | null>(null)
|
||||
createEffect(() => {
|
||||
if (!props.isActive) return
|
||||
const handler = scrollToBottomHandle()
|
||||
if (!handler) return
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => handler()))
|
||||
})
|
||||
const sessionBusy = createMemo(() => {
|
||||
const currentSession = session()
|
||||
if (!currentSession) return false
|
||||
return getSessionBusyStatus(props.instanceId, currentSession.id)
|
||||
})
|
||||
let scrollToBottomHandle: (() => void) | undefined
|
||||
function scheduleScrollToBottom() {
|
||||
if (!scrollToBottomHandle) return
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => scrollToBottomHandle?.())
|
||||
})
|
||||
}
|
||||
createEffect(() => {
|
||||
if (!props.isActive) return
|
||||
scheduleScrollToBottom()
|
||||
})
|
||||
let quoteHandler: ((text: string, mode: "quote" | "code") => void) | null = null
|
||||
|
||||
createEffect(() => {
|
||||
@@ -70,10 +74,10 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
||||
}
|
||||
|
||||
async function handleSendMessage(prompt: string, attachments: Attachment[]) {
|
||||
const handler = scrollToBottomHandle()
|
||||
if (handler) {
|
||||
handler()
|
||||
if (scrollToBottomHandle && import.meta.env?.DEV) {
|
||||
console.debug("[SessionView] handleSendMessage scroll", props.sessionId)
|
||||
}
|
||||
scheduleScrollToBottom()
|
||||
await sendMessage(props.instanceId, props.sessionId, prompt, attachments)
|
||||
}
|
||||
|
||||
@@ -201,12 +205,13 @@ export const SessionView: Component<SessionViewProps> = (props) => {
|
||||
onRevert={handleRevert}
|
||||
onFork={handleFork}
|
||||
isActive={props.isActive}
|
||||
registerScrollToBottom={(fn) => {
|
||||
setScrollToBottomHandle(() => fn)
|
||||
if (props.isActive) {
|
||||
requestAnimationFrame(() => requestAnimationFrame(() => fn()))
|
||||
}
|
||||
}}
|
||||
registerScrollToBottom={(fn) => {
|
||||
scrollToBottomHandle = fn
|
||||
if (props.isActive) {
|
||||
scheduleScrollToBottom()
|
||||
}
|
||||
}}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ interface VirtualItemProps {
|
||||
placeholderClass?: string
|
||||
virtualizationEnabled?: Accessor<boolean>
|
||||
forceVisible?: Accessor<boolean>
|
||||
suspendMeasurements?: Accessor<boolean>
|
||||
onMeasured?: () => void
|
||||
id?: string
|
||||
}
|
||||
@@ -138,10 +139,12 @@ export default function VirtualItem(props: VirtualItemProps) {
|
||||
if (!virtualizationEnabled()) return false
|
||||
return !isIntersecting()
|
||||
})
|
||||
const measurementsSuspended = () => Boolean(props.suspendMeasurements?.())
|
||||
|
||||
let wrapperRef: HTMLDivElement | undefined
|
||||
|
||||
|
||||
let contentRef: HTMLDivElement | undefined
|
||||
|
||||
let resizeObserver: ResizeObserver | undefined
|
||||
let intersectionCleanup: (() => void) | undefined
|
||||
|
||||
@@ -176,23 +179,27 @@ export default function VirtualItem(props: VirtualItemProps) {
|
||||
}
|
||||
|
||||
function updateMeasuredHeight() {
|
||||
if (!contentRef) return
|
||||
if (!contentRef || measurementsSuspended()) return
|
||||
const next = contentRef.offsetHeight
|
||||
if (next === measuredHeight()) return
|
||||
persistMeasurement(next)
|
||||
}
|
||||
|
||||
|
||||
function setupResizeObserver() {
|
||||
if (!contentRef) return
|
||||
if (!contentRef || measurementsSuspended()) return
|
||||
cleanupResizeObserver()
|
||||
if (typeof ResizeObserver === "undefined") {
|
||||
updateMeasuredHeight()
|
||||
return
|
||||
}
|
||||
resizeObserver = new ResizeObserver(() => updateMeasuredHeight())
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
if (measurementsSuspended()) return
|
||||
updateMeasuredHeight()
|
||||
})
|
||||
resizeObserver.observe(contentRef)
|
||||
}
|
||||
|
||||
|
||||
function refreshIntersectionObserver(targetRoot: Element | Document | null) {
|
||||
cleanupIntersectionObserver()
|
||||
if (!wrapperRef) {
|
||||
@@ -219,7 +226,7 @@ export default function VirtualItem(props: VirtualItemProps) {
|
||||
contentRef = element ?? undefined
|
||||
if (contentRef) {
|
||||
queueMicrotask(() => {
|
||||
if (shouldHideContent()) return
|
||||
if (shouldHideContent() || measurementsSuspended()) return
|
||||
updateMeasuredHeight()
|
||||
setupResizeObserver()
|
||||
})
|
||||
@@ -227,10 +234,10 @@ export default function VirtualItem(props: VirtualItemProps) {
|
||||
cleanupResizeObserver()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
createEffect(() => {
|
||||
if (shouldHideContent()) {
|
||||
if (shouldHideContent() || measurementsSuspended()) {
|
||||
cleanupResizeObserver()
|
||||
} else if (contentRef) {
|
||||
queueMicrotask(() => {
|
||||
@@ -239,6 +246,7 @@ export default function VirtualItem(props: VirtualItemProps) {
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
createEffect(() => {
|
||||
const key = props.cacheKey
|
||||
|
||||
Reference in New Issue
Block a user