@@ -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 } " in ` ,
2898+ icon : 'search' ,
2899+ status : {
2900+ text : `${ resultCount } result${ resultCount !== 1 ? 's' : '' } found` ,
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 } file${ itemCount > 1 ? 's' : '' } read`
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