diff --git a/ui/desktop/src/ChatWindow.tsx b/ui/desktop/src/ChatWindow.tsx index f50c5e780..3e6706326 100644 --- a/ui/desktop/src/ChatWindow.tsx +++ b/ui/desktop/src/ChatWindow.tsx @@ -8,7 +8,7 @@ import Input from './components/Input'; import LoadingGoose from './components/LoadingGoose'; import MoreMenu from './components/MoreMenu'; import { Card } from './components/ui/card'; -import { ScrollArea } from './components/ui/scroll-area'; +import { ScrollArea, ScrollAreaHandle } from './components/ui/scroll-area'; import UserMessage from './components/UserMessage'; import WingToWing, { Working } from './components/WingToWing'; import { askAi } from './utils/askAI'; @@ -22,7 +22,6 @@ import { useRecentModels } from './components/settings/models/RecentModels'; import { createSelectedModel } from './components/settings/models/utils'; import { getDefaultModel } from './components/settings/models/hardcoded_stuff'; import Splash from './components/Splash'; -import { loadAndAddStoredExtensions } from './extensions'; declare global { interface Window { @@ -53,13 +52,10 @@ export interface Chat { }>; } -type ScrollBehavior = 'auto' | 'smooth' | 'instant'; - export function ChatContent({ chats, setChats, selectedChatId, - setSelectedChatId, initialQuery, setProgressMessage, setWorking, @@ -77,8 +73,8 @@ export function ChatContent({ const [hasMessages, setHasMessages] = useState(false); const [lastInteractionTime, setLastInteractionTime] = useState(Date.now()); const [showGame, setShowGame] = useState(false); - const messagesEndRef = useRef(null); const [working, setWorkingLocal] = useState(Working.Idle); + const scrollRef = useRef(null); useEffect(() => { setWorking(working); @@ -94,7 +90,6 @@ export function ChatContent({ onToolCall: ({ toolCall }) => { updateWorking(Working.Working); setProgressMessage(`Executing tool: ${toolCall.toolName}`); - requestAnimationFrame(() => scrollToBottom('instant')); }, onResponse: (response) => { if (!response.ok) { @@ -115,8 +110,6 @@ export function ChatContent({ const fetchResponses = await askAi(message.content); setMessageMetadata((prev) => ({ ...prev, [message.id]: fetchResponses })); - requestAnimationFrame(() => scrollToBottom('smooth')); - const timeSinceLastInteraction = Date.now() - lastInteractionTime; window.electron.logInfo('last interaction:' + lastInteractionTime); if (timeSinceLastInteraction > 60000) { @@ -150,23 +143,6 @@ export function ChatContent({ } }, [messages]); - const scrollToBottom = (behavior: ScrollBehavior = 'smooth') => { - if (messagesEndRef.current) { - messagesEndRef.current.scrollIntoView({ - behavior, - block: 'end', - inline: 'nearest', - }); - } - }; - - // Single effect to handle all scrolling - useEffect(() => { - if (isLoading || messages.length > 0 || working === Working.Working) { - scrollToBottom(isLoading || working === Working.Working ? 'instant' : 'smooth'); - } - }, [messages, isLoading, working]); - // Handle submit const handleSubmit = (e: React.FormEvent) => { window.electron.startPowerSaveBlocker(); @@ -178,7 +154,9 @@ export function ChatContent({ role: 'user', content: content, }); - scrollToBottom('instant'); + if (scrollRef.current?.scrollToBottom) { + scrollRef.current.scrollToBottom(); + } } }; @@ -241,7 +219,7 @@ export function ChatContent({ {messages.length === 0 ? ( ) : ( - + {messages.map((message) => (
{message.role === 'user' ? ( @@ -288,7 +266,6 @@ export function ChatContent({
)}
-
)} diff --git a/ui/desktop/src/components/LoadingGoose.tsx b/ui/desktop/src/components/LoadingGoose.tsx index fe0e169d2..d9679d334 100644 --- a/ui/desktop/src/components/LoadingGoose.tsx +++ b/ui/desktop/src/components/LoadingGoose.tsx @@ -4,7 +4,7 @@ import GooseLogo from './GooseLogo'; const LoadingGoose = () => { return (
-
+
goose is working on it..
diff --git a/ui/desktop/src/components/ui/scroll-area.tsx b/ui/desktop/src/components/ui/scroll-area.tsx index 244df655d..607b263b6 100644 --- a/ui/desktop/src/components/ui/scroll-area.tsx +++ b/ui/desktop/src/components/ui/scroll-area.tsx @@ -3,22 +3,86 @@ import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; import { cn } from '../../utils'; -const ScrollArea = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - {children} - - - - -)); +export interface ScrollAreaHandle { + scrollToBottom: () => void; +} + +interface ScrollAreaProps extends React.ComponentPropsWithoutRef { + autoScroll?: boolean; +} + +const ScrollArea = React.forwardRef( + ({ className, children, autoScroll = false, ...props }, ref) => { + const rootRef = React.useRef>(null); + const viewportRef = React.useRef(null); + const viewportEndRef = React.useRef(null); + const [isFollowing, setIsFollowing] = React.useState(true); + + const scrollToBottom = React.useCallback(() => { + if (viewportEndRef.current) { + viewportEndRef.current.scrollIntoView({ + behavior: 'smooth', + block: 'end', + inline: 'nearest', + }); + setIsFollowing(true); + } + }, []); + + // Expose the scrollToBottom method to parent components + React.useImperativeHandle( + ref, + () => ({ + scrollToBottom, + }), + [scrollToBottom] + ); + + // Handle scroll events to update isFollowing state + const handleScroll = React.useCallback(() => { + if (!viewportRef.current) return; + + const viewport = viewportRef.current; + const { scrollHeight, scrollTop, clientHeight } = viewport; + + const scrollBottom = scrollTop + clientHeight; + const newIsFollowing = scrollHeight === scrollBottom; + + // react will internally optimize this to not re-store the same values + setIsFollowing(newIsFollowing); + }, []); + + React.useEffect(() => { + if (!autoScroll || !isFollowing) return; + + scrollToBottom(); + }, [children, autoScroll, isFollowing, scrollToBottom]); + + // Add scroll event listener + React.useEffect(() => { + const viewport = viewportRef.current; + if (!viewport) return; + + viewport.addEventListener('scroll', handleScroll); + return () => viewport.removeEventListener('scroll', handleScroll); + }, [handleScroll]); + + return ( + + + {children} + {autoScroll &&
} + + + + + ); + } +); ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; const ScrollBar = React.forwardRef<