@@ -18,12 +18,17 @@ const ACP_INPUT_DRAFTS_STORAGE_KEY = 'acpInputDrafts';
1818const EMPTY_ARRAY : any [ ] = [ ] ;
1919
2020const findTextMatches = ( messages : AcpMessage [ ] , query : string ) => {
21- const normalizedQuery = query . toLocaleLowerCase ( ) ;
21+ const normalizedQuery = query . toLowerCase ( ) ;
2222 if ( ! normalizedQuery ) return [ ] ;
2323
2424 return messages . flatMap ( ( message , index ) => {
2525 if ( message . role !== 'user' && message . role !== 'assistant' && message . role !== 'thought' ) return [ ] ;
26- const count = message . content . toLocaleLowerCase ( ) . split ( normalizedQuery ) . length - 1 ;
26+ let count = 0 ;
27+ let offset = message . content . toLowerCase ( ) . indexOf ( normalizedQuery ) ;
28+ while ( offset >= 0 ) {
29+ count += 1 ;
30+ offset = message . content . toLowerCase ( ) . indexOf ( normalizedQuery , offset + normalizedQuery . length ) ;
31+ }
2732 return Array . from ( { length : count } , ( _ , occurrence ) => ( { messageIndex : index , occurrence } ) ) ;
2833 } ) ;
2934} ;
@@ -276,6 +281,7 @@ const AcpSessionComponent: React.FC<AcpSessionProps> = ({
276281 const [ searchOpen , setSearchOpen ] = useState ( false ) ;
277282 const [ searchQuery , setSearchQuery ] = useState ( '' ) ;
278283 const [ currentSearchMatch , setCurrentSearchMatch ] = useState ( 0 ) ;
284+ const [ searchRenderVersion , setSearchRenderVersion ] = useState ( 0 ) ;
279285 const [ inputValues , setInputValues ] = useState < Record < string , string > > ( ( ) => {
280286 const savedDrafts = loadItem < Record < string , unknown > > ( ACP_INPUT_DRAFTS_STORAGE_KEY ) ;
281287 if ( ! savedDrafts || typeof savedDrafts !== 'object' ) {
@@ -302,6 +308,9 @@ const AcpSessionComponent: React.FC<AcpSessionProps> = ({
302308 ) ;
303309 const activeSearchMessageIndex = searchMatches [ currentSearchMatch ] ?. messageIndex ;
304310 const activeOccurrence = searchMatches [ currentSearchMatch ] ?. occurrence ?? 0 ;
311+ const handleWorkGroupExpansionChange = useCallback ( ( ) => {
312+ setSearchRenderVersion ( ( version ) => version + 1 ) ;
313+ } , [ ] ) ;
305314
306315 useEffect ( ( ) => {
307316 if ( searchMatches . length === 0 || currentSearchMatch < searchMatches . length ) return ;
@@ -369,37 +378,70 @@ const AcpSessionComponent: React.FC<AcpSessionProps> = ({
369378 } , [ closeSearch , openSearch , searchOpen ] ) ;
370379
371380 useEffect ( ( ) => {
372- if ( activeSearchMessageIndex === undefined ) return ;
381+ const clearHighlights = ( ) => {
382+ CSS . highlights . delete ( 'acp-search-match' ) ;
383+ CSS . highlights . delete ( 'acp-search-current' ) ;
384+ } ;
385+
386+ if ( activeSearchMessageIndex === undefined || ! searchQuery ) {
387+ clearHighlights ( ) ;
388+ return ;
389+ }
390+
373391 const frame = requestAnimationFrame ( ( ) => {
374- const target = innerRef . current ?. querySelector < HTMLElement > (
375- `[data-message-index="${ activeSearchMessageIndex } "]` ,
376- ) ;
377- if ( ! target ) return ;
392+ const messageTargets = innerRef . current
393+ ? Array . from ( innerRef . current . querySelectorAll < HTMLElement > ( '[data-message-index]' ) )
394+ : [ ] ;
395+ if ( messageTargets . length === 0 ) {
396+ clearHighlights ( ) ;
397+ return ;
398+ }
378399
379- const query = searchQuery . toLocaleLowerCase ( ) ;
400+ const query = searchQuery . toLowerCase ( ) ;
380401 const ranges : Range [ ] = [ ] ;
381- const walker = document . createTreeWalker ( target , NodeFilter . SHOW_TEXT ) ;
382- let node = walker . nextNode ( ) ;
383- while ( node ) {
384- const text = node . textContent ?. toLocaleLowerCase ( ) ?? '' ;
385- let offset = text . indexOf ( query ) ;
386- while ( offset >= 0 ) {
387- const range = document . createRange ( ) ;
388- range . setStart ( node , offset ) ;
389- range . setEnd ( node , offset + searchQuery . length ) ;
390- ranges . push ( range ) ;
391- offset = text . indexOf ( query , offset + query . length ) ;
402+ let currentRange : Range | undefined ;
403+
404+ for ( const target of messageTargets ) {
405+ const messageIndex = Number ( target . dataset . messageIndex ) ;
406+ const targetRanges : Range [ ] = [ ] ;
407+ const walker = document . createTreeWalker ( target , NodeFilter . SHOW_TEXT ) ;
408+ let node = walker . nextNode ( ) ;
409+ while ( node ) {
410+ // Do not search text that is present in the DOM but is not visible.
411+ // ACP messages contain collapsed/auxiliary controls whose text can
412+ // otherwise produce a misleading highlight.
413+ const parent = node . parentElement ;
414+ const isVisible = ! ! parent && parent . getClientRects ( ) . length > 0 ;
415+ const text = isVisible ? node . textContent ?. toLowerCase ( ) ?? '' : '' ;
416+ let offset = text . indexOf ( query ) ;
417+ while ( offset >= 0 ) {
418+ const range = document . createRange ( ) ;
419+ range . setStart ( node , offset ) ;
420+ range . setEnd ( node , offset + query . length ) ;
421+ targetRanges . push ( range ) ;
422+ offset = text . indexOf ( query , offset + query . length ) ;
423+ }
424+ node = walker . nextNode ( ) ;
392425 }
393- node = walker . nextNode ( ) ;
426+
427+ if ( messageIndex === activeSearchMessageIndex ) {
428+ currentRange = targetRanges [ activeOccurrence ] ;
429+ }
430+ ranges . push ( ...targetRanges ) ;
394431 }
432+ clearHighlights ( ) ;
395433 CSS . highlights . set ( 'acp-search-match' , new Highlight ( ...ranges ) ) ;
396- if ( ranges [ activeOccurrence ] ) {
397- CSS . highlights . set ( 'acp-search-current' , new Highlight ( ranges [ activeOccurrence ] ) ) ;
434+ if ( currentRange ) {
435+ CSS . highlights . set ( 'acp-search-current' , new Highlight ( currentRange ) ) ;
398436 }
399437
400438 const scroller = contentRef . current ;
401439 if ( scroller ) {
402- const matchRect = ( ranges [ activeOccurrence ] ?? target ) . getBoundingClientRect ( ) ;
440+ const activeTarget = messageTargets . find (
441+ ( target ) => Number ( target . dataset . messageIndex ) === activeSearchMessageIndex ,
442+ ) ;
443+ const matchRect = ( currentRange ?? activeTarget ) ?. getBoundingClientRect ( ) ;
444+ if ( ! matchRect ) return ;
403445 const scrollerRect = scroller . getBoundingClientRect ( ) ;
404446 scroller . scrollTo ( {
405447 top : scroller . scrollTop + matchRect . top - scrollerRect . top
@@ -411,10 +453,9 @@ const AcpSessionComponent: React.FC<AcpSessionProps> = ({
411453
412454 return ( ) => {
413455 cancelAnimationFrame ( frame ) ;
414- CSS . highlights . delete ( 'acp-search-match' ) ;
415- CSS . highlights . delete ( 'acp-search-current' ) ;
456+ clearHighlights ( ) ;
416457 } ;
417- } , [ activeOccurrence , activeSearchMessageIndex , contentRef , innerRef , searchQuery ] ) ;
458+ } , [ activeOccurrence , activeSearchMessageIndex , contentRef , innerRef , searchQuery , searchRenderVersion ] ) ;
418459
419460 const handleUndoMessage = useCallback (
420461 ( message : AcpMessage ) => {
@@ -556,6 +597,7 @@ const AcpSessionComponent: React.FC<AcpSessionProps> = ({
556597 expandedToolResults = { expandedToolResults }
557598 expandedThoughts = { searchExpandedThoughts }
558599 activeSearchMessageIndex = { activeSearchMessageIndex }
600+ onWorkGroupExpansionChange = { handleWorkGroupExpansionChange }
559601 onToggleToolCall = { toggleToolCall }
560602 onToggleToolResult = { toggleToolResult }
561603 onToggleThought = { toggleThought }
0 commit comments