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