Skip to content

Commit b749fa9

Browse files
committed
fix(app): scroll jitter/loop
1 parent 8a51cbd commit b749fa9

7 files changed

Lines changed: 144 additions & 488 deletions

File tree

packages/app/src/pages/session.tsx

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ import { createOpenReviewFile, createSizing } from "@/pages/session/helpers"
3737
import { MessageTimeline } from "@/pages/session/message-timeline"
3838
import { type DiffStyle, SessionReviewTab, type SessionReviewTabProps } from "@/pages/session/review-tab"
3939
import { resetSessionModel, syncSessionModel } from "@/pages/session/session-model-helpers"
40-
import { createScrollSpy } from "@/pages/session/scroll-spy"
4140
import { SessionMobileTabs } from "@/pages/session/session-mobile-tabs"
4241
import { SessionSidePanel } from "@/pages/session/session-side-panel"
4342
import { TerminalPanel } from "@/pages/session/terminal-panel"
@@ -486,20 +485,49 @@ export default function Page() {
486485
return "main"
487486
})
488487

489-
const activeMessage = createMemo(() => {
490-
if (!store.messageId) return lastUserMessage()
491-
const found = visibleUserMessages()?.find((m) => m.id === store.messageId)
492-
return found ?? lastUserMessage()
493-
})
494488
const setActiveMessage = (message: UserMessage | undefined) => {
489+
messageMark = scrollMark
495490
setStore("messageId", message?.id)
496491
}
497492

493+
const anchor = (id: string) => `message-${id}`
494+
495+
const cursor = () => {
496+
const root = scroller
497+
if (!root) return store.messageId
498+
499+
const box = root.getBoundingClientRect()
500+
const line = box.top + 100
501+
const list = [...root.querySelectorAll<HTMLElement>("[data-message-id]")]
502+
.map((el) => {
503+
const id = el.dataset.messageId
504+
if (!id) return
505+
506+
const rect = el.getBoundingClientRect()
507+
return { id, top: rect.top, bottom: rect.bottom }
508+
})
509+
.filter((item): item is { id: string; top: number; bottom: number } => !!item)
510+
511+
const shown = list.filter((item) => item.bottom > box.top && item.top < box.bottom)
512+
const hit = shown.find((item) => item.top <= line && item.bottom >= line)
513+
if (hit) return hit.id
514+
515+
const near = [...shown].sort((a, b) => {
516+
const da = Math.abs(a.top - line)
517+
const db = Math.abs(b.top - line)
518+
if (da !== db) return da - db
519+
return a.top - b.top
520+
})[0]
521+
if (near) return near.id
522+
523+
return list.filter((item) => item.top <= line).at(-1)?.id ?? list[0]?.id ?? store.messageId
524+
}
525+
498526
function navigateMessageByOffset(offset: number) {
499527
const msgs = visibleUserMessages()
500528
if (msgs.length === 0) return
501529

502-
const current = store.messageId
530+
const current = store.messageId && messageMark === scrollMark ? store.messageId : cursor()
503531
const base = current ? msgs.findIndex((m) => m.id === current) : msgs.length
504532
const currentIndex = base === -1 ? msgs.length : base
505533
const targetIndex = currentIndex + offset
@@ -572,6 +600,8 @@ export default function Page() {
572600
let dockHeight = 0
573601
let scroller: HTMLDivElement | undefined
574602
let content: HTMLDivElement | undefined
603+
let scrollMark = 0
604+
let messageMark = 0
575605

576606
const scrollGestureWindowMs = 250
577607

@@ -616,6 +646,7 @@ export default function Page() {
616646
() => {
617647
setStore("messageId", undefined)
618648
setStore("changes", "session")
649+
setUi("pendingMessage", undefined)
619650
},
620651
{ defer: true },
621652
),
@@ -1110,12 +1141,6 @@ export default function Page() {
11101141

11111142
let scrollStateFrame: number | undefined
11121143
let scrollStateTarget: HTMLDivElement | undefined
1113-
const scrollSpy = createScrollSpy({
1114-
onActive: (id) => {
1115-
if (id === store.messageId) return
1116-
setStore("messageId", id)
1117-
},
1118-
})
11191144

11201145
const updateScrollState = (el: HTMLDivElement) => {
11211146
const max = el.scrollHeight - el.clientHeight
@@ -1163,31 +1188,21 @@ export default function Page() {
11631188
),
11641189
)
11651190

1166-
createEffect(
1167-
on(
1168-
sessionKey,
1169-
() => {
1170-
scrollSpy.clear()
1171-
},
1172-
{ defer: true },
1173-
),
1174-
)
1175-
1176-
const anchor = (id: string) => `message-${id}`
1177-
11781191
const setScrollRef = (el: HTMLDivElement | undefined) => {
11791192
scroller = el
11801193
autoScroll.scrollRef(el)
1181-
scrollSpy.setContainer(el)
11821194
if (el) scheduleScrollState(el)
11831195
}
11841196

1197+
const markUserScroll = () => {
1198+
scrollMark += 1
1199+
}
1200+
11851201
createResizeObserver(
11861202
() => content,
11871203
() => {
11881204
const el = scroller
11891205
if (el) scheduleScrollState(el)
1190-
scrollSpy.markDirty()
11911206
},
11921207
)
11931208

@@ -1220,7 +1235,6 @@ export default function Page() {
12201235
if (stick) autoScroll.forceScrollToBottom()
12211236

12221237
if (el) scheduleScrollState(el)
1223-
scrollSpy.markDirty()
12241238
},
12251239
)
12261240

@@ -1248,7 +1262,6 @@ export default function Page() {
12481262

12491263
onCleanup(() => {
12501264
document.removeEventListener("keydown", handleKeyDown)
1251-
scrollSpy.destroy()
12521265
if (reviewFrame !== undefined) cancelAnimationFrame(reviewFrame)
12531266
if (scrollStateFrame !== undefined) cancelAnimationFrame(scrollStateFrame)
12541267
})
@@ -1280,7 +1293,7 @@ export default function Page() {
12801293
<div class="flex-1 min-h-0 overflow-hidden">
12811294
<Switch>
12821295
<Match when={params.id}>
1283-
<Show when={activeMessage()}>
1296+
<Show when={lastUserMessage()}>
12841297
<MessageTimeline
12851298
mobileChanges={mobileChanges()}
12861299
mobileFallback={reviewContent({
@@ -1300,8 +1313,7 @@ export default function Page() {
13001313
onAutoScrollHandleScroll={autoScroll.handleScroll}
13011314
onMarkScrollGesture={markScrollGesture}
13021315
hasScrollGesture={hasScrollGesture}
1303-
isDesktop={isDesktop()}
1304-
onScrollSpyScroll={scrollSpy.onScroll}
1316+
onUserScroll={markUserScroll}
13051317
onTurnBackfillScroll={historyWindow.onScrollerScroll}
13061318
onAutoScrollInteraction={autoScroll.handleInteraction}
13071319
centered={centered()}
@@ -1320,8 +1332,6 @@ export default function Page() {
13201332
}}
13211333
renderedUserMessages={historyWindow.renderedUserMessages()}
13221334
anchor={anchor}
1323-
onRegisterMessage={scrollSpy.register}
1324-
onUnregisterMessage={scrollSpy.unregister}
13251335
/>
13261336
</Show>
13271337
</Match>

packages/app/src/pages/session/message-timeline.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -193,8 +193,7 @@ export function MessageTimeline(props: {
193193
onAutoScrollHandleScroll: () => void
194194
onMarkScrollGesture: (target?: EventTarget | null) => void
195195
hasScrollGesture: () => boolean
196-
isDesktop: boolean
197-
onScrollSpyScroll: () => void
196+
onUserScroll: () => void
198197
onTurnBackfillScroll: () => void
199198
onAutoScrollInteraction: (event: MouseEvent) => void
200199
centered: boolean
@@ -205,8 +204,6 @@ export function MessageTimeline(props: {
205204
onLoadEarlier: () => void
206205
renderedUserMessages: UserMessage[]
207206
anchor: (id: string) => string
208-
onRegisterMessage: (el: HTMLDivElement, id: string) => void
209-
onUnregisterMessage: (id: string) => void
210207
}) {
211208
let touchGesture: number | undefined
212209

@@ -574,9 +571,9 @@ export function MessageTimeline(props: {
574571
props.onScheduleScrollState(e.currentTarget)
575572
props.onTurnBackfillScroll()
576573
if (!props.hasScrollGesture()) return
574+
props.onUserScroll()
577575
props.onAutoScrollHandleScroll()
578576
props.onMarkScrollGesture(e.currentTarget)
579-
if (props.isDesktop) props.onScrollSpyScroll()
580577
}}
581578
onClick={props.onAutoScrollInteraction}
582579
class="relative min-w-0 w-full h-full"
@@ -763,10 +760,6 @@ export function MessageTimeline(props: {
763760
<div
764761
id={props.anchor(messageID)}
765762
data-message-id={messageID}
766-
ref={(el) => {
767-
props.onRegisterMessage(el, messageID)
768-
onCleanup(() => props.onUnregisterMessage(messageID))
769-
}}
770763
classList={{
771764
"min-w-0 w-full max-w-full": true,
772765
"md:max-w-200 2xl:max-w-[1000px]": props.centered,

packages/app/src/pages/session/scroll-spy.test.ts

Lines changed: 0 additions & 127 deletions
This file was deleted.

0 commit comments

Comments
 (0)