diff options
| author | Adam <[email protected]> | 2026-02-04 10:00:55 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-02-04 10:01:00 -0600 |
| commit | 61d3f788b847593a865d1aa8a9a112911f55d117 (patch) | |
| tree | f083b28d3545a06ed9e9a2cb722e2d66796672ce | |
| parent | a3b281b2f3414b82518909d5e31e4fbbd3f7bf3b (diff) | |
| download | opencode-61d3f788b847593a865d1aa8a9a112911f55d117.tar.gz opencode-61d3f788b847593a865d1aa8a9a112911f55d117.zip | |
fix(app): don't show scroll-to-bottom unecessarily
| -rw-r--r-- | packages/app/src/pages/session.tsx | 67 |
1 files changed, 64 insertions, 3 deletions
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 7ff4bebb4..f74eadc87 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -279,6 +279,10 @@ export default function Page() { pendingMessage: undefined as string | undefined, scrollGesture: 0, autoCreated: false, + scroll: { + overflow: false, + bottom: true, + }, }) createEffect( @@ -795,6 +799,7 @@ export default function Page() { let inputRef!: HTMLDivElement let promptDock: HTMLDivElement | undefined let scroller: HTMLDivElement | undefined + let content: HTMLDivElement | undefined const scrollGestureWindowMs = 250 @@ -1618,10 +1623,40 @@ export default function Page() { 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 = () => { setStore("messageId", undefined) autoScroll.forceScrollToBottom() clearMessageHash() + + const el = scroller + if (el) scheduleScrollState(el) } // 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) => { scroller = el autoScroll.scrollRef(el) + if (el) scheduleScrollState(el) } + createResizeObserver( + () => content, + () => { + const el = scroller + if (el) scheduleScrollState(el) + }, + ) + const turnInit = 20 const turnBatch = 20 let turnHandle: number | undefined @@ -1759,6 +1803,8 @@ export default function Page() { 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) if (!hash) { autoScroll.forceScrollToBottom() + + const el = scroller + if (el) scheduleScrollState(el) return } @@ -1864,6 +1913,9 @@ export default function Page() { } autoScroll.forceScrollToBottom() + + const el = scroller + if (el) scheduleScrollState(el) } const closestMessage = (node: Element | null): HTMLElement | null => { @@ -2029,6 +2081,7 @@ export default function Page() { cancelTurnBackfill() document.removeEventListener("keydown", handleKeyDown) if (scrollSpyFrame !== undefined) cancelAnimationFrame(scrollSpyFrame) + if (scrollStateFrame !== undefined) cancelAnimationFrame(scrollStateFrame) }) return ( @@ -2133,8 +2186,9 @@ export default function Page() { <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" classList={{ - "opacity-100 translate-y-0 scale-100": autoScroll.userScrolled(), - "opacity-0 translate-y-2 scale-95 pointer-events-none": !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": + !ui.scroll.overflow || ui.scroll.bottom, }} > <button @@ -2232,6 +2286,7 @@ export default function Page() { markScrollGesture(e.currentTarget) }} onScroll={(e) => { + scheduleScrollState(e.currentTarget) if (!hasScrollGesture()) return autoScroll.handleScroll() markScrollGesture(e.currentTarget) @@ -2359,7 +2414,13 @@ export default function Page() { </Show> <div - ref={autoScroll.contentRef} + ref={(el) => { + content = el + autoScroll.contentRef(el) + + const root = scroller + if (root) scheduleScrollState(root) + }} 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]" classList={{ |
