summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-04 10:00:55 -0600
committerAdam <[email protected]>2026-02-04 10:01:00 -0600
commit61d3f788b847593a865d1aa8a9a112911f55d117 (patch)
treef083b28d3545a06ed9e9a2cb722e2d66796672ce
parenta3b281b2f3414b82518909d5e31e4fbbd3f7bf3b (diff)
downloadopencode-61d3f788b847593a865d1aa8a9a112911f55d117.tar.gz
opencode-61d3f788b847593a865d1aa8a9a112911f55d117.zip
fix(app): don't show scroll-to-bottom unecessarily
-rw-r--r--packages/app/src/pages/session.tsx67
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={{