@@ -5,7 +5,7 @@ import { getRepo, initializeAssistantMode } from "@/api/repos";
55import { MessageThread } from "@/components/message/MessageThread" ;
66import { PromptInput , type PromptInputHandle } from "@/components/message/PromptInput" ;
77import { FloatingTTSButton } from '@/components/message/FloatingTTSButton'
8- import { X , CornerUpLeft } from "lucide-react" ;
8+ import { X , CornerUpLeft , ArrowDown } from "lucide-react" ;
99import { ModelSelectDialog } from "@/components/model/ModelSelectDialog" ;
1010import { Header } from "@/components/ui/header" ;
1111import { SessionList } from "@/components/session/SessionList" ;
@@ -27,6 +27,7 @@ import { useSettingsDialog } from "@/hooks/useSettingsDialog";
2727import { useAutoScroll } from "@/hooks/useAutoScroll" ;
2828import { useMobile } from "@/hooks/useMobile" ;
2929import { useVisualViewport } from "@/hooks/useVisualViewport" ;
30+ import { useTallScrollContent } from "@/hooks/useTallScrollContent" ;
3031import { useTTS } from "@/hooks/useTTS" ;
3132import { getAssistantText , getLatestPlayableAssistantMessage , useAutoPlayLastResponse } from "@/hooks/useAutoPlayLastResponse" ;
3233import { useEffect , useRef , useCallback , useMemo } from "react" ;
@@ -86,12 +87,14 @@ export function SessionDetail() {
8687 const [ minimizedQuestion , setMinimizedQuestion ] = useState < QuestionRequest | null > ( null ) ;
8788 const [ isHeaderVisible , setIsHeaderVisible ] = useState ( true ) ;
8889 const lastHeaderScrollTopRef = useRef ( 0 ) ;
90+ const lastHeaderScrollHeightRef = useRef ( 0 ) ;
8991
9092 const isMobile = useMobile ( ) ;
9193 const { keyboardHeight } = useVisualViewport ( ) ;
9294 const inputBottomOffset = isMobile ? keyboardHeight : 0 ;
9395 const promptOverlayRef = useRef < HTMLDivElement > ( null ) ;
9496 const [ promptOverlayHeight , setPromptOverlayHeight ] = useState ( 112 ) ;
97+ const isContentTall = useTallScrollContent ( messageContainerRef , 1.5 ) ;
9598
9699 useEffect ( ( ) => {
97100 const el = promptOverlayRef . current ;
@@ -216,27 +219,41 @@ export function SessionDetail() {
216219 if ( ! container ) return
217220
218221 lastHeaderScrollTopRef . current = container . scrollTop
222+ lastHeaderScrollHeightRef . current = container . scrollHeight
219223 setIsHeaderVisible ( true )
220224
225+ const SCROLL_DELTA_THRESHOLD = 8
226+
221227 const handleScroll = ( ) => {
222228 if ( ! isMobile ) {
223229 setIsHeaderVisible ( true )
224230 return
225231 }
226232 const currentScrollTop = container . scrollTop
227233 const previousScrollTop = lastHeaderScrollTopRef . current
228- const maxScrollTop = container . scrollHeight - container . clientHeight
229- const isAtBottom = maxScrollTop - currentScrollTop < 24
230- const isScrollingDown = currentScrollTop > previousScrollTop + 4
231- const isScrollingUp = currentScrollTop < previousScrollTop - 50
234+ const currentScrollHeight = container . scrollHeight
235+ const previousScrollHeight = lastHeaderScrollHeightRef . current
236+
237+ lastHeaderScrollTopRef . current = currentScrollTop
238+ lastHeaderScrollHeightRef . current = currentScrollHeight
239+
240+ if ( currentScrollHeight !== previousScrollHeight ) return
241+
242+ const delta = currentScrollTop - previousScrollTop
243+ if ( Math . abs ( delta ) < SCROLL_DELTA_THRESHOLD ) return
232244
233- if ( isAtBottom || isScrollingDown ) {
245+ const isAtTop = currentScrollTop < 24
246+
247+ if ( isAtTop ) {
234248 setIsHeaderVisible ( true )
235- } else if ( isScrollingUp ) {
236- setIsHeaderVisible ( false )
249+ return
237250 }
238251
239- lastHeaderScrollTopRef . current = currentScrollTop
252+ if ( delta > 0 ) {
253+ setIsHeaderVisible ( true )
254+ } else {
255+ setIsHeaderVisible ( false )
256+ }
240257 }
241258
242259 container . addEventListener ( 'scroll' , handleScroll , { passive : true } )
@@ -538,7 +555,7 @@ export function SessionDetail() {
538555 style = { { bottom : inputBottomOffset } }
539556 >
540557 < div className = "relative w-[94%] md:max-w-4xl" >
541- < div className = "absolute -top-5 right-0 md:right-4 z-50 flex flex-col items-end gap-2" >
558+ < div className = "absolute -top-9 right-0 z-50 flex flex-col items-end gap-2" >
542559 { ttsEnabled && ! hasPromptContent && ! isSessionActive && latestPlayableAssistant && (
543560 < FloatingTTSButton
544561 messageId = { latestPlayableAssistant . message . info . id }
@@ -561,6 +578,18 @@ export function SessionDetail() {
561578 </ button >
562579 ) }
563580 </ div >
581+ { isMobile && showScrollButton && isContentTall && (
582+ < div className = "absolute -top-9 left-0 z-50 flex flex-col items-start gap-2" >
583+ < button
584+ onClick = { scrollToBottom }
585+ className = "flex items-center justify-center px-3 py-1.5 rounded-lg bg-zinc-950/80 hover:bg-zinc-900/90 text-blue-300 hover:text-blue-200 border border-blue-400/20 shadow-md backdrop-blur-md transition-all duration-200 active:scale-95 hover:scale-105 ring-1 ring-blue-400/15"
586+ aria-label = "Scroll to bottom"
587+ title = "Scroll to bottom"
588+ >
589+ < ArrowDown className = "w-5 h-5" />
590+ </ button >
591+ </ div >
592+ ) }
564593 { leaderActive && (
565594 < div className = "absolute -top-12 left-1/2 -translate-x-1/2 z-50 px-4 py-2 rounded-xl bg-primary/90 text-primary-foreground border border-primary shadow-lg backdrop-blur-md animate-pulse" >
566595 < span className = "text-sm font-medium" > Waiting for shortcut key...</ span >
0 commit comments