@@ -169,7 +169,7 @@ import { ExecuteBash, ExecuteBashParams } from './tools/executeBash'
169169import  {  ExplanatoryParams ,  InvokeOutput ,  ToolApprovalException  }  from  './tools/toolShared' 
170170import  {  validatePathBasic ,  validatePathExists ,  validatePaths  as  validatePathsSync  }  from  './utils/pathValidation' 
171171import  {  GrepSearch ,  SanitizedRipgrepOutput  }  from  './tools/grepSearch' 
172- import  {  FileSearch ,  FileSearchParams  }  from  './tools/fileSearch' 
172+ import  {  FileSearch ,  FileSearchParams ,   isFileSearchParams  }  from  './tools/fileSearch' 
173173import  {  FsReplace ,  FsReplaceParams  }  from  './tools/fsReplace' 
174174import  {  loggingUtils ,  timeoutUtils  }  from  '@aws/lsp-core' 
175175import  {  diffLines  }  from  'diff' 
@@ -1695,8 +1695,7 @@ export class AgenticChatController implements ChatHandlers {
16951695                // remove progress UI 
16961696                await  chatResultStream . removeResultBlockAndUpdateUI ( progressPrefix  +  toolUse . toolUseId ) 
16971697
1698-                 // fsRead and listDirectory write to an existing card and could show nothing in the current position 
1699-                 if  ( ! [ FS_WRITE ,  FS_REPLACE ,  FS_READ ,  LIST_DIRECTORY ] . includes ( toolUse . name ) )  { 
1698+                 if  ( ! [ FS_WRITE ,  FS_REPLACE ] . includes ( toolUse . name ) )  { 
17001699                    await  this . #showUndoAllIfRequired( chatResultStream ,  session ) 
17011700                } 
17021701                // fsWrite can take a long time, so we render fsWrite  Explanatory upon partial streaming responses. 
@@ -1911,10 +1910,19 @@ export class AgenticChatController implements ChatHandlers {
19111910                switch  ( toolUse . name )  { 
19121911                    case  FS_READ :
19131912                    case  LIST_DIRECTORY :
1913+                         const  readToolResult  =  await  this . #processReadTool( toolUse ,  chatResultStream ) 
1914+                         if  ( readToolResult )  { 
1915+                             await  chatResultStream . writeResultBlock ( readToolResult ) 
1916+                         } 
1917+                         break 
19141918                    case  FILE_SEARCH :
1915-                         const  initialListDirResult  =  this . #processReadOrListOrSearch( toolUse ,  chatResultStream ) 
1916-                         if  ( initialListDirResult )  { 
1917-                             await  chatResultStream . writeResultBlock ( initialListDirResult ) 
1919+                         if  ( isFileSearchParams ( toolUse . input ) )  { 
1920+                             await  this . #processFileSearchTool( 
1921+                                 toolUse . input , 
1922+                                 toolUse . toolUseId , 
1923+                                 result , 
1924+                                 chatResultStream 
1925+                             ) 
19181926                        } 
19191927                        break 
19201928                    // no need to write tool result for listDir,fsRead,fileSearch into chat stream 
@@ -2315,7 +2323,6 @@ export class AgenticChatController implements ChatHandlers {
23152323        } 
23162324
23172325        const  toolMsgId  =  toolUse . toolUseId ! 
2318-         const  chatMsgId  =  chatResultStream . getResult ( ) . messageId 
23192326        let  headerEmitted  =  false 
23202327
23212328        const  initialHeader : ChatMessage [ 'header' ]  =  { 
@@ -2353,13 +2360,6 @@ export class AgenticChatController implements ChatHandlers {
23532360                    header : completedHeader , 
23542361                } ) 
23552362
2356-                 await  chatResultStream . writeResultBlock ( { 
2357-                     type : 'answer' , 
2358-                     messageId : chatMsgId , 
2359-                     body : '' , 
2360-                     header : undefined , 
2361-                 } ) 
2362- 
23632363                this . #stoppedToolUses. add ( toolMsgId ) 
23642364            } , 
23652365        } ) 
@@ -2877,70 +2877,135 @@ export class AgenticChatController implements ChatHandlers {
28772877        } 
28782878    } 
28792879
2880-     #processReadOrListOrSearch( toolUse : ToolUse ,  chatResultStream : AgenticChatResultStream ) : ChatMessage  |  undefined  { 
2881-         let  messageIdToUpdate  =  toolUse . toolUseId ! 
2882-         const  currentId  =  chatResultStream . getMessageIdToUpdateForTool ( toolUse . name ! ) 
2880+     async  #processFileSearchTool( 
2881+         toolInput : FileSearchParams , 
2882+         toolUseId : string , 
2883+         result : InvokeOutput , 
2884+         chatResultStream : AgenticChatResultStream 
2885+     ) : Promise < void >  { 
2886+         if  ( typeof  result . output . content  !==  'string' )  return 
28832887
2884-         if  ( currentId )  { 
2885-             messageIdToUpdate  =  currentId 
2886-         }  else  { 
2887-             chatResultStream . setMessageIdToUpdateForTool ( toolUse . name ! ,  messageIdToUpdate ) 
2888+         const  {  queryName,  path : inputPath  }  =  toolInput 
2889+         const  resultCount  =  result . output . content 
2890+             . split ( '\n' ) 
2891+             . filter ( line  =>  line . trim ( ) . startsWith ( '[F]' )  ||  line . trim ( ) . startsWith ( '[D]' ) ) . length 
2892+ 
2893+         const  chatMessage : ChatMessage  =  { 
2894+             type : 'tool' , 
2895+             messageId : toolUseId , 
2896+             header : { 
2897+                 body : `Searched for "${ queryName }  , 
2898+                 icon : 'search' , 
2899+                 status : { 
2900+                     text : `${ resultCount } ${ resultCount  !==  1  ? 's'  : '' }  , 
2901+                 } , 
2902+                 fileList : { 
2903+                     filePaths : [ inputPath ] , 
2904+                     details : { 
2905+                         [ inputPath ] : { 
2906+                             description : inputPath , 
2907+                             visibleName : path . basename ( inputPath ) , 
2908+                             clickable : false , 
2909+                         } , 
2910+                     } , 
2911+                 } , 
2912+             } , 
28882913        } 
2889-         let  currentPaths  =  [ ] 
2914+         await  chatResultStream . writeResultBlock ( chatMessage ) 
2915+     } 
2916+ 
2917+     async  #processReadTool( 
2918+         toolUse : ToolUse , 
2919+         chatResultStream : AgenticChatResultStream 
2920+     ) : Promise < ChatMessage  |  undefined >  { 
2921+         let  currentPaths : string [ ]  =  [ ] 
28902922        if  ( toolUse . name  ===  FS_READ )  { 
2891-             currentPaths  =  ( toolUse . input  as  unknown  as  FsReadParams ) ?. paths 
2923+             currentPaths  =  ( toolUse . input  as  unknown  as  FsReadParams ) ?. paths  ||  [ ] 
2924+         }  else  if  ( toolUse . name  ===  LIST_DIRECTORY )  { 
2925+             const  singlePath  =  ( toolUse . input  as  unknown  as  ListDirectoryParams ) ?. path 
2926+             if  ( singlePath )  { 
2927+                 currentPaths  =  [ singlePath ] 
2928+             } 
2929+         }  else  if  ( toolUse . name  ===  FILE_SEARCH )  { 
2930+             const  queryName  =  ( toolUse . input  as  unknown  as  FileSearchParams ) ?. queryName 
2931+             if  ( queryName )  { 
2932+                 currentPaths  =  [ queryName ] 
2933+             } 
28922934        }  else  { 
2893-             currentPaths . push ( ( toolUse . input   as   unknown   as   ListDirectoryParams   |   FileSearchParams ) ?. path ) 
2935+             return 
28942936        } 
28952937
2896-         if  ( ! currentPaths )  return 
2938+         if  ( currentPaths . length   ===   0 )  return 
28972939
2898-         for  ( const  currentPath  of  currentPaths )  { 
2899-             const  existingPaths  =  chatResultStream . getMessageOperation ( messageIdToUpdate ) ?. filePaths  ||  [ ] 
2900-             // Check if path already exists in the list 
2901-             const  isPathAlreadyProcessed  =  existingPaths . some ( path  =>  path . relativeFilePath  ===  currentPath ) 
2902-             if  ( ! isPathAlreadyProcessed )  { 
2903-                 const  currentFileDetail  =  { 
2904-                     relativeFilePath : currentPath , 
2905-                     lineRanges : [ {  first : - 1 ,  second : - 1  } ] , 
2906-                 } 
2907-                 chatResultStream . addMessageOperation ( messageIdToUpdate ,  toolUse . name ! ,  [ 
2908-                     ...existingPaths , 
2909-                     currentFileDetail , 
2910-                 ] ) 
2940+         // Check if the last message is the same tool type 
2941+         const  lastMessage  =  chatResultStream . getLastMessage ( ) 
2942+         const  isSameToolType  = 
2943+             lastMessage ?. type  ===  'tool'  &&  lastMessage . header ?. icon  ===  this . #toolToIcon( toolUse . name ) 
2944+ 
2945+         let  allPaths  =  currentPaths 
2946+ 
2947+         if  ( isSameToolType  &&  lastMessage . messageId )  { 
2948+             // Combine with existing paths and overwrite the last message 
2949+             const  existingPaths  =  lastMessage . header ?. fileList ?. filePaths  ||  [ ] 
2950+             allPaths  =  [ ...existingPaths ,  ...currentPaths ] 
2951+ 
2952+             const  blockId  =  chatResultStream . getMessageBlockId ( lastMessage . messageId ) 
2953+             if  ( blockId  !==  undefined )  { 
2954+                 // Create the updated message with combined paths 
2955+                 const  updatedMessage  =  this . #createFileListToolMessage( toolUse ,  allPaths ,  lastMessage . messageId ) 
2956+                 // Overwrite the existing block 
2957+                 await  chatResultStream . overwriteResultBlock ( updatedMessage ,  blockId ) 
2958+                 return  undefined  // Don't return a message since we already wrote it 
29112959            } 
29122960        } 
2961+ 
2962+         // Create new message with current paths 
2963+         return  this . #createFileListToolMessage( toolUse ,  allPaths ,  toolUse . toolUseId ! ) 
2964+     } 
2965+ 
2966+     #createFileListToolMessage( toolUse : ToolUse ,  filePaths : string [ ] ,  messageId : string ) : ChatMessage  { 
2967+         const  itemCount  =  filePaths . length 
29132968        let  title : string 
2914-         const  itemCount  =  chatResultStream . getMessageOperation ( messageIdToUpdate ) ?. filePaths . length 
2915-         const  filePathsPushed  =  chatResultStream . getMessageOperation ( messageIdToUpdate ) ?. filePaths  ??  [ ] 
2916-         if  ( ! itemCount )  { 
2969+         if  ( itemCount  ===  0 )  { 
29172970            title  =  'Gathering context' 
29182971        }  else  { 
29192972            title  = 
29202973                toolUse . name  ===  FS_READ 
29212974                    ? `${ itemCount } ${ itemCount  >  1  ? 's'  : '' }  
2922-                     : toolUse . name  ===  FILE_SEARCH 
2923-                       ? `${ itemCount } ${ itemCount  ===  1  ? 'directory'  : 'directories' } searched ` 
2924-                       : ` ${ itemCount }   ${ itemCount   ===   1  ?  'directory'  :  'directories' }  listed` 
2975+                     : toolUse . name  ===  LIST_DIRECTORY 
2976+                       ? `${ itemCount } ${ itemCount  ===  1  ? 'directory'  : 'directories' } listed ` 
2977+                       : '' 
29252978        } 
29262979        const  details : Record < string ,  FileDetails >  =  { } 
2927-         for  ( const  item  of  filePathsPushed )  { 
2928-             details [ item . relativeFilePath ]  =  { 
2929-                 lineRanges : item . lineRanges , 
2930-                 description : item . relativeFilePath , 
2980+         for  ( const  filePath  of  filePaths )  { 
2981+             details [ filePath ]  =  { 
2982+                 description : filePath , 
2983+                 visibleName : path . basename ( filePath ) , 
2984+                 clickable : toolUse . name  ===  FS_READ , 
29312985            } 
29322986        } 
2933- 
2934-         const  fileList : FileList  =  { 
2935-             rootFolderTitle : title , 
2936-             filePaths : filePathsPushed . map ( item  =>  item . relativeFilePath ) , 
2937-             details, 
2938-         } 
29392987        return  { 
29402988            type : 'tool' , 
2941-             fileList, 
2942-             messageId : messageIdToUpdate , 
2943-             body : '' , 
2989+             header : { 
2990+                 body : title , 
2991+                 icon : this . #toolToIcon( toolUse . name ) , 
2992+                 fileList : { 
2993+                     filePaths, 
2994+                     details, 
2995+                 } , 
2996+             } , 
2997+             messageId, 
2998+         } 
2999+     } 
3000+ 
3001+     #toolToIcon( toolName : string  |  undefined ) : string  |  undefined  { 
3002+         switch  ( toolName )  { 
3003+             case  FS_READ :
3004+                 return  'eye' 
3005+             case  LIST_DIRECTORY :
3006+                 return  'check-list' 
3007+             default :
3008+                 return  undefined 
29443009        } 
29453010    } 
29463011
@@ -2956,14 +3021,7 @@ export class AgenticChatController implements ChatHandlers {
29563021            return  undefined 
29573022        } 
29583023
2959-         let  messageIdToUpdate  =  toolUse . toolUseId ! 
2960-         const  currentId  =  chatResultStream . getMessageIdToUpdateForTool ( toolUse . name ! ) 
2961- 
2962-         if  ( currentId )  { 
2963-             messageIdToUpdate  =  currentId 
2964-         }  else  { 
2965-             chatResultStream . setMessageIdToUpdateForTool ( toolUse . name ! ,  messageIdToUpdate ) 
2966-         } 
3024+         const  messageIdToUpdate  =  toolUse . toolUseId ! 
29673025
29683026        // Extract search results from the tool output 
29693027        const  output  =  result . output . content  as  SanitizedRipgrepOutput 
0 commit comments