fix(app): don't show scroll-to-bottom unecessarily

This commit is contained in:
Adam
2026-02-04 10:00:55 -06:00
parent a3b281b2f3
commit 61d3f788b8

View File

@@ -279,6 +279,10 @@ export default function Page() {
pendingMessage: undefined as string | undefined, pendingMessage: undefined as string | undefined,
scrollGesture: 0, scrollGesture: 0,
autoCreated: false, autoCreated: false,
scroll: {
overflow: false,
bottom: true,
},
}) })
createEffect( createEffect(
@@ -795,6 +799,7 @@ export default function Page() {
let inputRef!: HTMLDivElement let inputRef!: HTMLDivElement
let promptDock: HTMLDivElement | undefined let promptDock: HTMLDivElement | undefined
let scroller: HTMLDivElement | undefined let scroller: HTMLDivElement | undefined
let content: HTMLDivElement | undefined
const scrollGestureWindowMs = 250 const scrollGestureWindowMs = 250
@@ -1618,10 +1623,40 @@ export default function Page() {
window.history.replaceState(null, "", window.location.href.replace(/#.*$/, "")) window.history.replaceState(null, "", window.location.href.replace(/#.*$/, ""))
} }
let scrollStateFrame: number | undefined
let scrollStateTarget: HTMLDivElement | undefined
const updateScrollState = (el: HTMLDivElement) => {
const max = el.scrollHeight - el.clientHeight
const overflow = max > 1
const bottom = !overflow || el.scrollTop >= max - 2
if (ui.scroll.overflow === overflow && ui.scroll.bottom === bottom) return
setUi("scroll", { overflow, bottom })
}
const scheduleScrollState = (el: HTMLDivElement) => {
scrollStateTarget = el
if (scrollStateFrame !== undefined) return
scrollStateFrame = requestAnimationFrame(() => {
scrollStateFrame = undefined
const target = scrollStateTarget
scrollStateTarget = undefined
if (!target) return
updateScrollState(target)
})
}
const resumeScroll = () => { const resumeScroll = () => {
setStore("messageId", undefined) setStore("messageId", undefined)
autoScroll.forceScrollToBottom() autoScroll.forceScrollToBottom()
clearMessageHash() clearMessageHash()
const el = scroller
if (el) scheduleScrollState(el)
} }
// When the user returns to the bottom, treat the active message as "latest". // When the user returns to the bottom, treat the active message as "latest".
@@ -1657,8 +1692,17 @@ export default function Page() {
const setScrollRef = (el: HTMLDivElement | undefined) => { const setScrollRef = (el: HTMLDivElement | undefined) => {
scroller = el scroller = el
autoScroll.scrollRef(el) autoScroll.scrollRef(el)
if (el) scheduleScrollState(el)
} }
createResizeObserver(
() => content,
() => {
const el = scroller
if (el) scheduleScrollState(el)
},
)
const turnInit = 20 const turnInit = 20
const turnBatch = 20 const turnBatch = 20
let turnHandle: number | undefined let turnHandle: number | undefined
@@ -1759,6 +1803,8 @@ export default function Page() {
el.scrollTo({ top: el.scrollHeight, behavior: "auto" }) el.scrollTo({ top: el.scrollHeight, behavior: "auto" })
}) })
} }
if (el) scheduleScrollState(el)
}, },
) )
@@ -1839,6 +1885,9 @@ export default function Page() {
const hash = window.location.hash.slice(1) const hash = window.location.hash.slice(1)
if (!hash) { if (!hash) {
autoScroll.forceScrollToBottom() autoScroll.forceScrollToBottom()
const el = scroller
if (el) scheduleScrollState(el)
return return
} }
@@ -1864,6 +1913,9 @@ export default function Page() {
} }
autoScroll.forceScrollToBottom() autoScroll.forceScrollToBottom()
const el = scroller
if (el) scheduleScrollState(el)
} }
const closestMessage = (node: Element | null): HTMLElement | null => { const closestMessage = (node: Element | null): HTMLElement | null => {
@@ -2029,6 +2081,7 @@ export default function Page() {
cancelTurnBackfill() cancelTurnBackfill()
document.removeEventListener("keydown", handleKeyDown) document.removeEventListener("keydown", handleKeyDown)
if (scrollSpyFrame !== undefined) cancelAnimationFrame(scrollSpyFrame) if (scrollSpyFrame !== undefined) cancelAnimationFrame(scrollSpyFrame)
if (scrollStateFrame !== undefined) cancelAnimationFrame(scrollStateFrame)
}) })
return ( return (
@@ -2133,8 +2186,9 @@ export default function Page() {
<div <div
class="absolute left-1/2 -translate-x-1/2 bottom-[calc(var(--prompt-height,8rem)+32px)] z-[60] pointer-events-none transition-all duration-200 ease-out" class="absolute left-1/2 -translate-x-1/2 bottom-[calc(var(--prompt-height,8rem)+32px)] z-[60] pointer-events-none transition-all duration-200 ease-out"
classList={{ classList={{
"opacity-100 translate-y-0 scale-100": autoScroll.userScrolled(), "opacity-100 translate-y-0 scale-100": ui.scroll.overflow && !ui.scroll.bottom,
"opacity-0 translate-y-2 scale-95 pointer-events-none": !autoScroll.userScrolled(), "opacity-0 translate-y-2 scale-95 pointer-events-none":
!ui.scroll.overflow || ui.scroll.bottom,
}} }}
> >
<button <button
@@ -2232,6 +2286,7 @@ export default function Page() {
markScrollGesture(e.currentTarget) markScrollGesture(e.currentTarget)
}} }}
onScroll={(e) => { onScroll={(e) => {
scheduleScrollState(e.currentTarget)
if (!hasScrollGesture()) return if (!hasScrollGesture()) return
autoScroll.handleScroll() autoScroll.handleScroll()
markScrollGesture(e.currentTarget) markScrollGesture(e.currentTarget)
@@ -2359,7 +2414,13 @@ export default function Page() {
</Show> </Show>
<div <div
ref={autoScroll.contentRef} ref={(el) => {
content = el
autoScroll.contentRef(el)
const root = scroller
if (root) scheduleScrollState(root)
}}
role="log" role="log"
class="flex flex-col gap-12 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]" class="flex flex-col gap-12 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]"
classList={{ classList={{