From 12c6c0925f9c2999e5ed38cdc6c620a81cc2c152 Mon Sep 17 00:00:00 2001 From: LukeParkerDev <10430890+Hona@users.noreply.github.com> Date: Wed, 13 May 2026 10:16:03 +1000 Subject: [PATCH] fix(app): guard virtualizer scroll root --- .../src/pages/session/message-timeline.tsx | 110 +++++++++++------- 1 file changed, 71 insertions(+), 39 deletions(-) diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index a458f9581..df6ee730c 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -3,7 +3,7 @@ import { createStore, produce } from "solid-js/store" import { Dynamic } from "solid-js/web" import { useNavigate } from "@solidjs/router" import { useMutation } from "@tanstack/solid-query" -import { VList, type VListHandle } from "virtua/solid" +import { Virtualizer, type VirtualizerHandle } from "virtua/solid" import { Accordion } from "@opencode-ai/ui/accordion" import { Button } from "@opencode-ai/ui/button" import { Card } from "@opencode-ai/ui/card" @@ -452,7 +452,7 @@ export function MessageTimeline(props: { const { params, sessionKey } = useSessionKey() const platform = usePlatform() - let virtualizer: VListHandle | undefined + let virtualizer: VirtualizerHandle | undefined const sessionID = createMemo(() => params.id) const sessionMessages = createMemo(() => { const id = sessionID() @@ -713,10 +713,11 @@ export function MessageTimeline(props: { let more: HTMLButtonElement | undefined let head: HTMLDivElement | undefined - let listHost: HTMLDivElement | undefined let listRoot: HTMLDivElement | undefined let listCleanup = () => {} let listFrame: number | undefined + let contentFrame: number | undefined + const [scrollRoot, setScrollRoot] = createSignal() const updateTitleMetrics = () => { if (!head || head.clientWidth <= 0) return @@ -728,15 +729,44 @@ export function MessageTimeline(props: { updateTitleMetrics, ) - const bindListRoot = () => { - const root = listHost?.firstElementChild - if (!(root instanceof HTMLDivElement)) return + const bindContentRoot = (root: HTMLDivElement) => { + const child = root.firstElementChild + props.setContentRef(child instanceof HTMLDivElement ? child : root) + } + + const scheduleContentRoot = (root: HTMLDivElement) => { + if (contentFrame !== undefined) cancelAnimationFrame(contentFrame) + contentFrame = requestAnimationFrame(() => { + contentFrame = undefined + if (listRoot !== root) return + bindContentRoot(root) + }) + } + + const connectListRoot = (root: HTMLDivElement) => { + if (listRoot !== root) return + if (!root.isConnected || !root.ownerDocument.defaultView) { + listFrame = requestAnimationFrame(() => { + listFrame = undefined + connectListRoot(root) + }) + return + } + + props.setScrollRef(root) + setScrollRoot(root) + scheduleContentRoot(root) + } + + const bindListRoot = (root: HTMLDivElement) => { if (root === listRoot) return + if (listFrame !== undefined) cancelAnimationFrame(listFrame) + if (contentFrame !== undefined) cancelAnimationFrame(contentFrame) listCleanup() listRoot = root - props.setScrollRef(root) - props.setContentRef(root.firstElementChild instanceof HTMLDivElement ? root.firstElementChild : root) + setScrollRoot(undefined) + connectListRoot(root) const onWheel = (event: WheelEvent) => { const delta = normalizeWheelDelta({ @@ -788,18 +818,11 @@ export function MessageTimeline(props: { } } - const bindListHost = (el: HTMLDivElement) => { - listHost = el - if (listFrame !== undefined) cancelAnimationFrame(listFrame) - listFrame = requestAnimationFrame(() => { - listFrame = undefined - bindListRoot() - }) - } - onCleanup(() => { if (listFrame !== undefined) cancelAnimationFrame(listFrame) + if (contentFrame !== undefined) cancelAnimationFrame(contentFrame) listCleanup() + setScrollRoot(undefined) props.setScrollRef(undefined) }) @@ -1595,28 +1618,37 @@ export function MessageTimeline(props: { -
- { - virtualizer = handle - }} - class="relative min-w-0 w-full h-full" - onScroll={() => { - const root = listRoot - if (!root) return - props.onScheduleScrollState(root) - props.onHistoryScroll() - if (!props.hasScrollGesture()) return - props.onUserScroll() - props.onAutoScrollHandleScroll() - props.onMarkScrollGesture(root) - }} - > - {(key) => } - +
+ + {(root) => ( + { + virtualizer = handle + scheduleContentRoot(root()) + }} + onScroll={() => { + const root = listRoot + if (!root) return + props.onScheduleScrollState(root) + props.onHistoryScroll() + if (!props.hasScrollGesture()) return + props.onUserScroll() + props.onAutoScrollHandleScroll() + props.onMarkScrollGesture(root) + }} + > + {(key) => } + + )} +