From df012aa93defde1df552c6245a5d479958d8ae06 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Mon, 23 Mar 2026 11:43:30 -0700 Subject: [PATCH 1/4] feat(web): replace manual auto-scroll with useStickToBottom in chat thread Replaces the custom scroll tracking implementation (manual scroll listeners, scrollIntoView calls, isAutoScrollEnabled state) with the useStickToBottom library already used in detailsCard. Fixes layout regressions for sticky answer headers and chat box visibility. Co-Authored-By: Claude Sonnet 4.6 --- .../chat/components/chatThread/chatThread.tsx | 147 +++++++----------- 1 file changed, 60 insertions(+), 87 deletions(-) diff --git a/packages/web/src/features/chat/components/chatThread/chatThread.tsx b/packages/web/src/features/chat/components/chatThread/chatThread.tsx index c69f54ade..dfa68a2e0 100644 --- a/packages/web/src/features/chat/components/chatThread/chatThread.tsx +++ b/packages/web/src/features/chat/components/chatThread/chatThread.tsx @@ -2,7 +2,6 @@ import { useToast } from '@/components/hooks/use-toast'; import { Button } from '@/components/ui/button'; -import { ScrollArea } from '@/components/ui/scroll-area'; import { Separator } from '@/components/ui/separator'; import { CustomSlateEditor } from '@/features/chat/customSlateEditor'; import { AdditionalChatRequestParams, CustomEditor, LanguageModelInfo, SBChatMessage, SearchScope, Source } from '@/features/chat/types'; @@ -12,6 +11,7 @@ import { CreateUIMessage, DefaultChatTransport } from 'ai'; import { ArrowDownIcon, CopyIcon } from 'lucide-react'; import { useNavigationGuard } from 'next-navigation-guard'; import { Fragment, useCallback, useEffect, useRef, useState } from 'react'; +import { useStickToBottom } from 'use-stick-to-bottom'; import { Descendant } from 'slate'; import { useMessagePairs } from '../../useMessagePairs'; import { useSelectedLanguageModel } from '../../useSelectedLanguageModel'; @@ -67,10 +67,8 @@ export const ChatThread = ({ chatName, }: ChatThreadProps) => { const [isErrorBannerVisible, setIsErrorBannerVisible] = useState(false); - const scrollAreaRef = useRef(null); - const latestMessagePairRef = useRef(null); const hasSubmittedInputMessage = useRef(false); - const [isAutoScrollEnabled, setIsAutoScrollEnabled] = useState(false); + const { scrollRef, contentRef, scrollToBottom, isAtBottom } = useStickToBottom({ initial: false }); const { toast } = useToast(); const router = useRouter(); const params = useParams<{ domain: string }>(); @@ -204,9 +202,9 @@ export const ChatThread = ({ } sendMessage(inputMessage); - setIsAutoScrollEnabled(true); + scrollToBottom(); hasSubmittedInputMessage.current = true; - }, [inputMessage, sendMessage]); + }, [inputMessage, scrollToBottom, sendMessage]); // Restore pending message after OAuth redirect (askgh login wall) useEffect(() => { @@ -234,28 +232,24 @@ export const ChatThread = ({ const mentions = getAllMentionElements(children); const message = createUIMessage(text, mentions.map(({ data }) => data), selectedSearchScopes); sendMessage(message); - setIsAutoScrollEnabled(true); + scrollToBottom(); } catch (error) { console.error('Failed to restore pending message:', error); } - }, [isAuthenticated, isOwner, chatId, sendMessage, selectedSearchScopes]); + }, [isAuthenticated, isOwner, chatId, sendMessage, selectedSearchScopes, scrollToBottom]); - // Track scroll position changes. + // Track scroll position for history state restoration. useEffect(() => { - const scrollElement = scrollAreaRef.current?.querySelector('[data-radix-scroll-area-viewport]') as HTMLElement; - if (!scrollElement) return; + const scrollElement = scrollRef.current; + if (!scrollElement) { + return; + } let timeout: NodeJS.Timeout | null = null; const handleScroll = () => { const scrollOffset = scrollElement.scrollTop; - const threshold = 50; // pixels from bottom to consider "at bottom" - const { scrollHeight, clientHeight } = scrollElement; - const isAtBottom = scrollHeight - scrollOffset - clientHeight <= threshold; - setIsAutoScrollEnabled(isAtBottom); - - // Debounce the history state update if (timeout) { clearTimeout(timeout); } @@ -279,10 +273,11 @@ export const ChatThread = ({ clearTimeout(timeout); } }; - }, []); + }, [scrollRef]); + // Restore scroll position from history state on mount. useEffect(() => { - const scrollElement = scrollAreaRef.current?.querySelector('[data-radix-scroll-area-viewport]') as HTMLElement; + const scrollElement = scrollRef.current; if (!scrollElement) { return; } @@ -298,26 +293,7 @@ export const ChatThread = ({ behavior: 'instant', }); }, 10); - }, []); - - // When messages are being streamed, scroll to the latest message - // assuming auto scrolling is enabled. - useEffect(() => { - if ( - !latestMessagePairRef.current || - !isAutoScrollEnabled || - messages.length === 0 - ) { - return; - } - - latestMessagePairRef.current.scrollIntoView({ - behavior: 'smooth', - block: 'end', - inline: 'nearest', - }); - - }, [isAutoScrollEnabled, messages]); + }, [scrollRef]); // Keep the error state & banner visibility in sync. @@ -345,10 +321,10 @@ export const ChatThread = ({ const message = createUIMessage(text, mentions.map(({ data }) => data), selectedSearchScopes); sendMessage(message); - setIsAutoScrollEnabled(true); + scrollToBottom(); resetEditor(editor); - }, [sendMessage, selectedSearchScopes, isAuthenticated, captureEvent, chatId]); + }, [sendMessage, selectedSearchScopes, isAuthenticated, captureEvent, chatId, scrollToBottom]); const onDuplicate = useCallback(async (newName: string): Promise => { if (!defaultChatId) { @@ -379,64 +355,61 @@ export const ChatThread = ({ /> )} - - { - messagePairs.length === 0 ? ( -
-

no messages

-
- ) : ( - <> - {messagePairs.map(([userMessage, assistantMessage], index) => { - const isLastPair = index === messagePairs.length - 1; - const isStreaming = isLastPair && (status === "streaming" || status === "submitted"); - // Use a stable key based on user message ID - const key = userMessage.id; - - return ( - - - {index !== messagePairs.length - 1 && ( - - )} - - ); - })} - - ) - } +
+
+
+ { + messagePairs.length === 0 ? ( +
+

no messages

+
+ ) : ( + <> + {messagePairs.map(([userMessage, assistantMessage], index) => { + const isLastPair = index === messagePairs.length - 1; + const isStreaming = isLastPair && (status === "streaming" || status === "submitted"); + // Use a stable key based on user message ID + const key = userMessage.id; + + return ( + + + {index !== messagePairs.length - 1 && ( + + )} + + ); + })} + + ) + } +
+
{ - (!isAutoScrollEnabled && status === "streaming") && ( + (!isAtBottom && status === "streaming") && (
) } - +
Date: Mon, 23 Mar 2026 11:44:20 -0700 Subject: [PATCH 2/4] chore: update CHANGELOG for #1031 Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2caff421..9d950fad2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed issue where ask responses would sometimes appear in the details panel while generating. [#1014](https://github.com/sourcebot-dev/sourcebot/pull/1014) - Fixed reference panel overflow issue in the ask UI. [#1014](https://github.com/sourcebot-dev/sourcebot/pull/1014) - Fixed homepage scrolling issue in the ask UI. [#1014](https://github.com/sourcebot-dev/sourcebot/pull/1014) +- Fixed auto-scroll behavior in the ask chat thread by replacing the manual scroll implementation with `useStickToBottom`. [#1031](https://github.com/sourcebot-dev/sourcebot/pull/1031) ## [4.15.11] - 2026-03-20 From d16b5d851c1742f73d493e79fbb4ddc4b2f741f4 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Mon, 23 Mar 2026 11:45:33 -0700 Subject: [PATCH 3/4] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d950fad2..ac4273056 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Changed the `webUrl` property of the `/api/repos` api to return a URL rather than just a path. [#1014](https://github.com/sourcebot-dev/sourcebot/pull/1014) - Changed the ask search scope selector to allow submitting questions with no search scope selected. When no selection is made, the agent will be able to search over all repos the user has access to. [#1014](https://github.com/sourcebot-dev/sourcebot/pull/1014) - Renamed the `search_code` tool to `grep` for ask and mcp. [#1014](https://github.com/sourcebot-dev/sourcebot/pull/1014) +- Improved auto-scroll behavior in the ask chat thread. [#1031](https://github.com/sourcebot-dev/sourcebot/pull/1031) ### Added - Added `glob`, `find_symbol_definitions`, and `find_symbol_references` tools to the ask agent and MCP server. [#1014](https://github.com/sourcebot-dev/sourcebot/pull/1014) @@ -24,7 +25,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed issue where ask responses would sometimes appear in the details panel while generating. [#1014](https://github.com/sourcebot-dev/sourcebot/pull/1014) - Fixed reference panel overflow issue in the ask UI. [#1014](https://github.com/sourcebot-dev/sourcebot/pull/1014) - Fixed homepage scrolling issue in the ask UI. [#1014](https://github.com/sourcebot-dev/sourcebot/pull/1014) -- Fixed auto-scroll behavior in the ask chat thread by replacing the manual scroll implementation with `useStickToBottom`. [#1031](https://github.com/sourcebot-dev/sourcebot/pull/1031) ## [4.15.11] - 2026-03-20 From d6ad6b4a8d439784a73158fcd315c95d8fbfe319 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Mon, 23 Mar 2026 13:36:45 -0700 Subject: [PATCH 4/4] feedabck --- .../web/src/features/chat/components/chatThread/chatThread.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/features/chat/components/chatThread/chatThread.tsx b/packages/web/src/features/chat/components/chatThread/chatThread.tsx index dfa68a2e0..cff328e4d 100644 --- a/packages/web/src/features/chat/components/chatThread/chatThread.tsx +++ b/packages/web/src/features/chat/components/chatThread/chatThread.tsx @@ -363,7 +363,7 @@ export const ChatThread = ({
{ messagePairs.length === 0 ? ( -
+

no messages

) : (