import { createEffect, on, onCleanup } from "solid-js" import { UserMessage } from "@opencode-ai/sdk/v2" export const messageIdFromHash = (hash: string) => { const value = hash.startsWith("#") ? hash.slice(1) : hash const match = value.match(/^message-(.+)$/) if (!match) return return match[1] } export const useSessionHashScroll = (input: { sessionKey: () => string sessionID: () => string | undefined messagesReady: () => boolean visibleUserMessages: () => UserMessage[] turnStart: () => number currentMessageId: () => string | undefined pendingMessage: () => string | undefined setPendingMessage: (value: string | undefined) => void setActiveMessage: (message: UserMessage | undefined) => void setTurnStart: (value: number) => void scheduleTurnBackfill: () => void autoScroll: { pause: () => void; forceScrollToBottom: () => void } scroller: () => HTMLDivElement | undefined anchor: (id: string) => string scheduleScrollState: (el: HTMLDivElement) => void consumePendingMessage: (key: string) => string | undefined }) => { const clearMessageHash = () => { if (!window.location.hash) return window.history.replaceState(null, "", window.location.href.replace(/#.*$/, "")) } const updateHash = (id: string) => { window.history.replaceState(null, "", `#${input.anchor(id)}`) } const scrollToElement = (el: HTMLElement, behavior: ScrollBehavior) => { const root = input.scroller() if (!root) return false const a = el.getBoundingClientRect() const b = root.getBoundingClientRect() const top = a.top - b.top + root.scrollTop root.scrollTo({ top, behavior }) return true } const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => { input.setActiveMessage(message) const msgs = input.visibleUserMessages() const index = msgs.findIndex((m) => m.id === message.id) if (index !== -1 && index < input.turnStart()) { input.setTurnStart(index) input.scheduleTurnBackfill() requestAnimationFrame(() => { const el = document.getElementById(input.anchor(message.id)) if (!el) { requestAnimationFrame(() => { const next = document.getElementById(input.anchor(message.id)) if (!next) return scrollToElement(next, behavior) }) return } scrollToElement(el, behavior) }) updateHash(message.id) return } const el = document.getElementById(input.anchor(message.id)) if (!el) { updateHash(message.id) requestAnimationFrame(() => { const next = document.getElementById(input.anchor(message.id)) if (!next) return if (!scrollToElement(next, behavior)) return }) return } if (scrollToElement(el, behavior)) { updateHash(message.id) return } requestAnimationFrame(() => { const next = document.getElementById(input.anchor(message.id)) if (!next) return if (!scrollToElement(next, behavior)) return }) updateHash(message.id) } const applyHash = (behavior: ScrollBehavior) => { const hash = window.location.hash.slice(1) if (!hash) { input.autoScroll.forceScrollToBottom() const el = input.scroller() if (el) input.scheduleScrollState(el) return } const messageId = messageIdFromHash(hash) if (messageId) { input.autoScroll.pause() const msg = input.visibleUserMessages().find((m) => m.id === messageId) if (msg) { scrollToMessage(msg, behavior) return } return } const target = document.getElementById(hash) if (target) { input.autoScroll.pause() scrollToElement(target, behavior) return } input.autoScroll.forceScrollToBottom() const el = input.scroller() if (el) input.scheduleScrollState(el) } createEffect( on(input.sessionKey, (key) => { if (!input.sessionID()) return const messageID = input.consumePendingMessage(key) if (!messageID) return input.setPendingMessage(messageID) }), ) createEffect(() => { if (!input.sessionID() || !input.messagesReady()) return requestAnimationFrame(() => applyHash("auto")) }) createEffect(() => { if (!input.sessionID() || !input.messagesReady()) return input.visibleUserMessages().length input.turnStart() const targetId = input.pendingMessage() ?? messageIdFromHash(window.location.hash) if (!targetId) return if (input.currentMessageId() === targetId) return const msg = input.visibleUserMessages().find((m) => m.id === targetId) if (!msg) return if (input.pendingMessage() === targetId) input.setPendingMessage(undefined) input.autoScroll.pause() requestAnimationFrame(() => scrollToMessage(msg, "auto")) }) createEffect(() => { if (!input.sessionID() || !input.messagesReady()) return const handler = () => requestAnimationFrame(() => applyHash("auto")) window.addEventListener("hashchange", handler) onCleanup(() => window.removeEventListener("hashchange", handler)) }) return { clearMessageHash, scrollToMessage, applyHash, } }