import { TimelineRow } from "@opencode/session-ui/timeline/projection" import { onCleanup } from "solid-js" import { createStore } from "solid-js/store" import { render } from "solid-js/web" import { LanguageProvider } from "../src/runtime/i18n/language" import { createTimelineFollow } from "../src/session/timeline/follow" import { createTimelineVirtualizer } from "../src/session/timeline/virtualizer" export function mountTimelineVirtualizer(input: { count: number rowHeight: number immediate?: boolean viewportHeight?: number }) { const host = document.createElement("main") host.dataset.testid = "timeline-virtualizer-fixture" host.dataset.scrolls = "0" host.dataset.viewportResizes = "0" host.style.cssText = "position:fixed;top:24px;right:24px;width:400px;z-index:1000" document.body.appendChild(host) function Fixture() { const [state, setState] = createStore({ ready: false, grown: false }) const follow = createTimelineFollow(() => "cold-reveal-fixture") const rows = Array.from( { length: input.count }, (_, index) => new TimelineRow.UserMessage({ userMessageID: `message-${index}` }), ) const rowByKey = new Map(rows.map((row) => [TimelineRow.key(row), row])) const indexes = new Map(rows.map((row, index) => [row.userMessageID, index])) let viewport!: HTMLDivElement let content!: HTMLDivElement let container!: HTMLDivElement let scrollToEnd = () => {} const timeline = createTimelineVirtualizer({ sessionKey: () => "cold-reveal-fixture", projection: { rows: () => rows, rowByKey: () => rowByKey, activeMessageID: () => undefined, messageByID: () => new Map(), messageRowIndex: () => indexes, messageLastRowIndex: () => indexes, }, showHeader: () => false, follow, scroll: () => ({ overflow: false, jump: false }), setScrollRef: (element) => { if (!element) return viewport = element resize.observe(element, { box: "border-box" }) }, onContentResize: () => { if (content) return content = host.querySelector("[data-timeline-virtual-content]")! reveal.observe(content, { attributes: true, attributeFilter: ["style"] }) }, onScheduleScrollState: (element) => { host.dataset.scrolls = String(Number(host.dataset.scrolls) + 1) host.dataset.lastScrollTop = String(element.scrollTop) }, onResumeScroll: () => {}, onUserScroll: () => {}, onHistoryScroll: () => {}, canRenderImmediately: () => input.immediate ?? false, setScrollToEnd: (fn) => { scrollToEnd = fn }, }) const resize = new ResizeObserver((entries) => { host.dataset.observedHeight = String(entries[0].borderBoxSize[0].blockSize) host.dataset.viewportResizes = String(Number(host.dataset.viewportResizes) + 1) }) const reveal = new MutationObserver(() => { if (content.style.visibility === "hidden" || host.dataset.firstReveal) return // Capture the first reveal, not a later frame after geometry has recovered. const mounted = [...content.querySelectorAll("[data-timeline-key]")] host.dataset.firstReveal = JSON.stringify({ rows: mounted.map((element) => Number(element.firstElementChild!.getAttribute("data-index"))), pendingMarkdown: content.querySelectorAll('[data-component="markdown"]:not([data-markdown-ready])').length, viewportHeight: viewport.clientHeight, scrollTop: viewport.scrollTop, clipped: mounted.flatMap((element) => element.firstElementChild!.getBoundingClientRect().height > element.offsetHeight + 1 ? [element.dataset.timelineKey] : [], ), }) }) onCleanup(() => { resize.disconnect() reveal.disconnect() }) return (
{/* A tool inside a group of several, which names them all on the group, as a reveal anchors it. */}
false} renderRow={(row) => (
{row().userMessageID}
)} />
) } render( () => ( ), host, ) }