Files
tf_code/packages/app/src/pages/session/use-session-hash-scroll.ts

175 lines
5.1 KiB
TypeScript

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,
}
}