@@ -18,6 +18,7 @@ import { defaultRangeExtractor, type Range, useVirtualizer } from '@tanstack/rea
1818import { SMOOTH_CHASE_RATE } from '@/lib/core/utils/smooth-bottom-chase'
1919import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
2020import { inter } from '@/app/_styles/fonts/inter/inter'
21+ import { FindBar } from '@/app/workspace/[workspaceId]/components/find-bar'
2122import { MessageActions } from '@/app/workspace/[workspaceId]/components/message-actions'
2223import { ChatMessageAttachments } from '@/app/workspace/[workspaceId]/home/components/chat-message-attachments'
2324import { ChatSurfaceProvider } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
@@ -41,6 +42,7 @@ import {
4142 toCopyableMarkdown ,
4243} from '@/app/workspace/[workspaceId]/home/components/mothership-chat/copyable-markdown'
4344import { nextSizerFloor } from '@/app/workspace/[workspaceId]/home/components/mothership-chat/sizer-floor'
45+ import { useChatFind } from '@/app/workspace/[workspaceId]/home/components/mothership-chat/use-chat-find'
4446import { QueuedMessages } from '@/app/workspace/[workspaceId]/home/components/queued-messages'
4547import {
4648 UserInput ,
@@ -217,7 +219,7 @@ const UserMessageRow = memo(function UserMessageRow({
217219 className = { attachmentWidthClassName }
218220 />
219221 ) }
220- < div className = { bubbleClassName } >
222+ < div className = { bubbleClassName } data-chat-find-content >
221223 < UserMessageContent content = { content } contexts = { contexts } />
222224 </ div >
223225 </ div >
@@ -386,8 +388,8 @@ export function MothershipChat({
386388 */
387389 const messages = useDeferredValue ( messagesProp )
388390 const [ lastRowAnimating , setLastRowAnimating ] = useState ( false )
391+ const containerRef = useRef < HTMLDivElement > ( null )
389392 const scrollElementRef = useRef < HTMLDivElement | null > ( null )
390- const { ref : autoScrollRef } = useAutoScroll ( isStreamActive || lastRowAnimating )
391393 const sizerRef = useRef < HTMLDivElement | null > ( null )
392394 const scrollerPaddingRef = useRef < { top : number ; bottom : number } | null > ( null )
393395 const sizerFloorAppliedRef = useRef ( 0 )
@@ -525,13 +527,6 @@ export function MothershipChat({
525527 sizerFloorAppliedRef . current = floor
526528 sizer . style . minHeight = `${ floor } px`
527529 } )
528- const setScrollElement = useCallback (
529- ( el : HTMLDivElement | null ) => {
530- scrollElementRef . current = el
531- autoScrollRef ( el )
532- } ,
533- [ autoScrollRef ]
534- )
535530
536531 const hasMessages = messages . length > 0
537532
@@ -695,6 +690,23 @@ export function MothershipChat({
695690 useAnimationFrameWithResizeObserver : true ,
696691 } )
697692
693+ const find = useChatFind ( {
694+ chatId,
695+ messages,
696+ hiddenUserByIndex : interactionPairing . hiddenUserByIndex ,
697+ containerRef,
698+ scrollElementRef,
699+ virtualizer,
700+ } )
701+ const { ref : autoScrollRef } = useAutoScroll ( isStreamActive || lastRowAnimating , find . isOpen )
702+ const setScrollElement = useCallback (
703+ ( el : HTMLDivElement | null ) => {
704+ scrollElementRef . current = el
705+ autoScrollRef ( el )
706+ } ,
707+ [ autoScrollRef ]
708+ )
709+
698710 /**
699711 * Instance property — silently ignored if passed as a `useVirtualizer`
700712 * option. Skips scroll compensation for the streaming last row: it starts
@@ -784,7 +796,7 @@ export function MothershipChat({
784796 } , [ chatId , hasMessages , initialScrollBlocked , lastIndex , virtualizer ] )
785797
786798 /**
787- * The user's OWN send always snaps the viewport to their message: sending IS the intent
799+ * With find closed, the user's own send snaps the viewport to their message: sending is the intent
788800 * to watch the reply, and the streaming sticky-scroll only engages when already pinned
789801 * to the bottom — from a scrolled-up position a fresh turn would stream out of view
790802 * (verified live, three-for-three, during the revamp browser pass).
@@ -795,11 +807,11 @@ export function MothershipChat({
795807 const scrolledForUserMsgRef = useRef < string | undefined > ( undefined )
796808 useLayoutEffect ( ( ) => {
797809 if ( ! lastUserMessageId || scrolledForUserMsgRef . current === lastUserMessageId ) return
798- if ( isSending && initialScrollBlocked ) return
810+ if ( find . isOpen || ( isSending && initialScrollBlocked ) ) return
799811 scrolledForUserMsgRef . current = lastUserMessageId
800812 if ( ! isSending ) return
801813 virtualizer . scrollToIndex ( lastIndex , { align : 'end' } )
802- } , [ lastUserMessageId , lastIndex , isSending , initialScrollBlocked , virtualizer ] )
814+ } , [ lastUserMessageId , lastIndex , isSending , initialScrollBlocked , virtualizer , find . isOpen ] )
803815
804816 const virtualItems = virtualizer . getVirtualItems ( )
805817
@@ -813,7 +825,32 @@ export function MothershipChat({
813825 onViewSources = { onViewSources }
814826 onWorkspaceResourceSelect = { onWorkspaceResourceSelect }
815827 >
816- < div className = { cn ( 'flex h-full min-h-0 flex-col' , inter . className , className ) } >
828+ < div
829+ ref = { containerRef }
830+ onKeyDown = { find . onKeyDown }
831+ tabIndex = { - 1 }
832+ className = { cn (
833+ 'relative flex h-full min-h-0 flex-col [&::highlight(chat-find)]:bg-[var(--highlight-match-bg)] [&::highlight(chat-find)]:text-[var(--highlight-match-text)] [&::highlight(chat-find-active)]:bg-[var(--brand-secondary)] [&::highlight(chat-find-active)]:text-[var(--color-black)]' ,
834+ inter . className ,
835+ className
836+ ) }
837+ >
838+ { find . isOpen && (
839+ < FindBar
840+ ariaLabel = 'Find in chat'
841+ query = { find . query }
842+ onQueryChange = { find . onQueryChange }
843+ onNext = { find . next }
844+ onPrev = { find . prev }
845+ onClose = { find . close }
846+ count = { find . count }
847+ currentIndex = { find . currentIndex }
848+ truncated = { find . truncated }
849+ isLoading = { find . isStale }
850+ canNavigate = { ! find . isStale }
851+ inputRef = { find . inputRef }
852+ />
853+ ) }
817854 < div ref = { setScrollElement } className = { styles . scrollContainer } onCopy = { handleCopy } >
818855 { isLoading && ! hasMessages ? (
819856 < MothershipChatSkeleton layout = { layout } />
0 commit comments