summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authoradamelmore <[email protected]>2026-01-27 17:48:21 -0600
committeradamelmore <[email protected]>2026-01-27 17:48:58 -0600
commitb4a9e1b1906af28ad58aa76afa43c194b2581ffd (patch)
tree7f93774d19eaf4bddd189e963f146402810b32e4
parent15ffd3cba1d3bd7d4d84c6911623a9c1d19e6647 (diff)
downloadopencode-b4a9e1b1906af28ad58aa76afa43c194b2581ffd.tar.gz
opencode-b4a9e1b1906af28ad58aa76afa43c194b2581ffd.zip
fix(app): auto-scroll
-rw-r--r--packages/app/src/pages/session.tsx125
1 files changed, 103 insertions, 22 deletions
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index eda15a581..a845d3a65 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -478,6 +478,12 @@ export default function Page() {
const targetIndex = currentIndex === -1 ? (offset > 0 ? 0 : msgs.length - 1) : currentIndex + offset
if (targetIndex < 0 || targetIndex >= msgs.length) return
+ if (targetIndex === msgs.length - 1) {
+ resumeScroll()
+ return
+ }
+
+ autoScroll.pause()
scrollToMessage(msgs[targetIndex], "auto")
}
@@ -524,14 +530,7 @@ export default function Page() {
const scrollGestureWindowMs = 250
- const scrollIgnoreWindowMs = 250
- let scrollIgnore = 0
-
- const markScrollIgnore = () => {
- scrollIgnore = Date.now()
- }
-
- const hasScrollIgnore = () => Date.now() - scrollIgnore < scrollIgnoreWindowMs
+ let touchGesture: number | undefined
const markScrollGesture = (target?: EventTarget | null) => {
const root = scroller
@@ -1274,9 +1273,15 @@ export default function Page() {
overflowAnchor: "dynamic",
})
+ const clearMessageHash = () => {
+ if (!window.location.hash) return
+ window.history.replaceState(null, "", window.location.href.replace(/#.*$/, ""))
+ }
+
const resumeScroll = () => {
setStore("messageId", undefined)
autoScroll.forceScrollToBottom()
+ clearMessageHash()
}
// When the user returns to the bottom, treat the active message as "latest".
@@ -1286,6 +1291,7 @@ export default function Page() {
(scrolled) => {
if (scrolled) return
setStore("messageId", undefined)
+ clearMessageHash()
},
{ defer: true },
),
@@ -1361,7 +1367,6 @@ export default function Page() {
requestAnimationFrame(() => {
const delta = el.scrollHeight - beforeHeight
if (!delta) return
- markScrollIgnore()
el.scrollTop = beforeTop + delta
})
@@ -1399,7 +1404,6 @@ export default function Page() {
if (stick && el) {
requestAnimationFrame(() => {
- markScrollIgnore()
el.scrollTo({ top: el.scrollHeight, behavior: "auto" })
})
}
@@ -1494,6 +1498,7 @@ export default function Page() {
const match = hash.match(/^message-(.+)$/)
if (match) {
+ autoScroll.pause()
const msg = visibleUserMessages().find((m) => m.id === match[1])
if (msg) {
scrollToMessage(msg, behavior)
@@ -1507,6 +1512,7 @@ export default function Page() {
const target = document.getElementById(hash)
if (target) {
+ autoScroll.pause()
scrollToElement(target, behavior)
return
}
@@ -1603,6 +1609,7 @@ export default function Page() {
const msg = visibleUserMessages().find((m) => m.id === targetId)
if (!msg) return
if (ui.pendingMessage === targetId) setUi("pendingMessage", undefined)
+ autoScroll.pause()
requestAnimationFrame(() => scrollToMessage(msg, "auto"))
})
@@ -1783,28 +1790,102 @@ export default function Page() {
>
<button
class="pointer-events-auto size-8 flex items-center justify-center rounded-full bg-background-base border border-border-base shadow-sm text-text-base hover:bg-background-stronger transition-colors"
- onClick={() => {
- setStore("messageId", undefined)
- autoScroll.forceScrollToBottom()
- window.history.replaceState(null, "", window.location.href.replace(/#.*$/, ""))
- }}
+ onClick={resumeScroll}
>
<Icon name="arrow-down-to-line" />
</button>
</div>
<div
ref={setScrollRef}
- onWheel={(e) => markScrollGesture(e.target)}
- onTouchMove={(e) => markScrollGesture(e.target)}
+ onWheel={(e) => {
+ const root = e.currentTarget
+ const target = e.target instanceof Element ? e.target : undefined
+ const nested = target?.closest("[data-scrollable]")
+ if (!nested || nested === root) {
+ markScrollGesture(root)
+ return
+ }
+
+ if (!(nested instanceof HTMLElement)) {
+ markScrollGesture(root)
+ return
+ }
+
+ const max = nested.scrollHeight - nested.clientHeight
+ if (max <= 1) {
+ markScrollGesture(root)
+ return
+ }
+
+ const delta =
+ e.deltaMode === 1
+ ? e.deltaY * 40
+ : e.deltaMode === 2
+ ? e.deltaY * root.clientHeight
+ : e.deltaY
+ if (!delta) return
+
+ if (delta < 0) {
+ if (nested.scrollTop + delta <= 0) markScrollGesture(root)
+ return
+ }
+
+ const remaining = max - nested.scrollTop
+ if (delta > remaining) markScrollGesture(root)
+ }}
+ onTouchStart={(e) => {
+ touchGesture = e.touches[0]?.clientY
+ }}
+ onTouchMove={(e) => {
+ const next = e.touches[0]?.clientY
+ const prev = touchGesture
+ touchGesture = next
+ if (next === undefined || prev === undefined) return
+
+ const delta = prev - next
+ if (!delta) return
+
+ const root = e.currentTarget
+ const target = e.target instanceof Element ? e.target : undefined
+ const nested = target?.closest("[data-scrollable]")
+ if (!nested || nested === root) {
+ markScrollGesture(root)
+ return
+ }
+
+ if (!(nested instanceof HTMLElement)) {
+ markScrollGesture(root)
+ return
+ }
+
+ const max = nested.scrollHeight - nested.clientHeight
+ if (max <= 1) {
+ markScrollGesture(root)
+ return
+ }
+
+ if (delta < 0) {
+ if (nested.scrollTop + delta <= 0) markScrollGesture(root)
+ return
+ }
+
+ const remaining = max - nested.scrollTop
+ if (delta > remaining) markScrollGesture(root)
+ }}
+ onTouchEnd={() => {
+ touchGesture = undefined
+ }}
+ onTouchCancel={() => {
+ touchGesture = undefined
+ }}
onPointerDown={(e) => {
if (e.target !== e.currentTarget) return
- markScrollGesture(e.target)
+ markScrollGesture(e.currentTarget)
}}
onScroll={(e) => {
- const gesture = hasScrollGesture()
- if (!hasScrollIgnore() || gesture) autoScroll.handleScroll()
- if (!gesture) return
- markScrollGesture(e.target)
+ if (!hasScrollGesture()) return
+ autoScroll.handleScroll()
+ markScrollGesture(e.currentTarget)
if (isDesktop()) scheduleScrollSpy(e.currentTarget)
}}
onClick={autoScroll.handleInteraction}