diff --git a/components/frontend/src/components/session/MessagesTab.tsx b/components/frontend/src/components/session/MessagesTab.tsx index c17494324..26f7d0ba1 100755 --- a/components/frontend/src/components/session/MessagesTab.tsx +++ b/components/frontend/src/components/session/MessagesTab.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useState, useRef, useEffect, useMemo, useLayoutEffect, useCallback } from "react"; -import { MessageSquare } from "lucide-react"; +import { MessageSquare, ChevronUp } from "lucide-react"; import { StreamMessage } from "@/components/ui/stream-message"; import { LoadingDots } from "@/components/ui/message"; import { Button } from "@/components/ui/button"; @@ -15,6 +15,9 @@ import type { QueuedMessageItem } from "@/hooks/use-session-queue"; /** Maximum number of messages rendered at once. Older messages are loaded on demand. */ const MAX_VISIBLE_MESSAGES = 100; +/** Scroll distance (px) from the top before the scroll-to-top button appears. */ +const SCROLL_TO_TOP_THRESHOLD = 300; + /** Derive a stable React key for any message variant. */ function getMessageKey(m: MessageObject | ToolUseMessages | HierarchicalToolMessage, idx: number): string { if ('id' in m && m.id) return m.id; @@ -66,6 +69,7 @@ const MessagesTab: React.FC = ({ session, streamMessages, chat const messagesContainerRef = useRef(null); const [isAtBottom, setIsAtBottom] = useState(true); + const [showScrollToTop, setShowScrollToTop] = useState(false); // How many messages (counting from the end) are currently rendered. const [loadedMessageCount, setLoadedMessageCount] = useState(MAX_VISIBLE_MESSAGES); // Refs for scroll-position preservation when loading earlier messages. @@ -100,9 +104,11 @@ const MessagesTab: React.FC = ({ session, streamMessages, chat }; const handleScroll = () => { + const container = messagesContainerRef.current; const bottom = checkIfAtBottom(); - // Avoid a re-render when the value hasn't changed. setIsAtBottom((prev) => (prev === bottom ? prev : bottom)); + const scrolledPastThreshold = !!container && container.scrollTop > SCROLL_TO_TOP_THRESHOLD; + setShowScrollToTop((prev) => (prev === scrolledPastThreshold ? prev : scrolledPastThreshold)); }; const scrollToBottom = () => { @@ -112,6 +118,13 @@ const MessagesTab: React.FC = ({ session, streamMessages, chat } }; + const scrollToTop = useCallback(() => { + const container = messagesContainerRef.current; + if (container) { + container.scrollTo({ top: 0, behavior: "smooth" }); + } + }, []); + // Load earlier messages and preserve the user's visual scroll position. const loadEarlierMessages = useCallback(() => { const container = messagesContainerRef.current; @@ -193,10 +206,11 @@ const MessagesTab: React.FC = ({ session, streamMessages, chat return (
+
{showWelcomeExperience && welcomeExperienceComponent} @@ -273,6 +287,19 @@ const MessagesTab: React.FC = ({ session, streamMessages, chat )}
+ {showScrollToTop && ( + + )} +
+