diff --git a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts index 821de1720b8..3b19598adeb 100644 --- a/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts +++ b/packages/core/src/amazonq/webview/ui/apps/cwChatConnector.ts @@ -357,7 +357,6 @@ export class Connector extends BaseConnector { if ( !this.onChatAnswerUpdated || ![ - 'accept-code-diff', 'reject-code-diff', 'run-shell-command', 'reject-shell-command', @@ -379,17 +378,6 @@ export class Connector extends BaseConnector { header: currentChatItem?.header ? { ...currentChatItem.header } : {}, } switch (action.id) { - case 'accept-code-diff': - if (answer.header) { - answer.header.status = { - icon: 'ok' as MynahIconsType, - text: 'Accepted', - status: 'success', - } - answer.header.buttons = [] - answer.body = ' ' - } - break case 'reject-code-diff': if (answer.header) { answer.header.status = { diff --git a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts index f7a1b7d125b..aac87312c3f 100644 --- a/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts +++ b/packages/core/src/codewhispererChat/clients/chat/v0/chat.ts @@ -24,22 +24,28 @@ export type ToolUseWithError = { error: Error | undefined } +type OperationType = 'read' | 'write' | 'listDir' + +interface FileOperation { + type: OperationType + filePaths: DocumentReference[] +} + export class ChatSession { private sessionId: string /** - * _readFiles = list of files read from the project to gather context before generating response. * _showDiffOnFileWrite = Controls whether to show diff view (true) or file context view (false) to the user * _context = Additional context to be passed to the LLM for generating the response * _messageIdToUpdate = messageId of a chat message to be updated, used for reducing consecutive tool messages + * _messageOperations = Maps messageId to filePaths which helps to open the read files and to open the code diff accordingly. */ - private _readFiles: DocumentReference[] = [] - private _readFolders: DocumentReference[] = [] private _toolUseWithError: ToolUseWithError | undefined private _showDiffOnFileWrite: boolean = false private _context: PromptMessage['context'] private _pairProgrammingModeOn: boolean = true private _fsWriteBackups: Map = new Map() private _agenticLoopInProgress: boolean = false + private _messageOperations: Map = new Map() /** * True if messages from local history have been sent to session. @@ -144,30 +150,12 @@ export class ChatSession { public setSessionID(id: string) { this.sessionId = id } - public get readFiles(): DocumentReference[] { - return this._readFiles - } - public get readFolders(): DocumentReference[] { - return this._readFolders - } public get showDiffOnFileWrite(): boolean { return this._showDiffOnFileWrite } public setShowDiffOnFileWrite(value: boolean) { this._showDiffOnFileWrite = value } - public addToReadFiles(filePath: DocumentReference) { - this._readFiles.push(filePath) - } - public clearListOfReadFiles() { - this._readFiles = [] - } - public setReadFolders(folder: DocumentReference) { - this._readFolders.push(folder) - } - public clearListOfReadFolders() { - this._readFolders = [] - } async chatIam(chatRequest: SendMessageRequest): Promise { const client = await createQDeveloperStreamingClient() @@ -196,4 +184,41 @@ export class ChatSession { return response } + + /** + * Adds a file operation for a specific message + * @param messageId The ID of the message + * @param type The type of operation ('read' or 'listDir' or 'write') + * @param filePaths Array of DocumentReference involved in the operation + */ + public addMessageOperation(messageId: string, type: OperationType, filePaths: DocumentReference[]) { + this._messageOperations.set(messageId, { type, filePaths }) + } + + /** + * Gets the file operation details for a specific message + * @param messageId The ID of the message + * @returns The file operation details or undefined if not found + */ + public getMessageOperation(messageId: string): FileOperation | undefined { + return this._messageOperations.get(messageId) + } + + /** + * Gets all file paths along with line ranges associated with a message + * @param messageId The ID of the message + * @returns Array of DocumentReference or empty array if message ID not found + */ + public getFilePathsByMessageId(messageId: string): DocumentReference[] { + return this._messageOperations.get(messageId)?.filePaths || [] + } + + /** + * Gets the operation type for a specific message + * @param messageId The ID of the message + * @returns The operation type or undefined if message ID not found + */ + public getOperationTypeByMessageId(messageId: string): OperationType | undefined { + return this._messageOperations.get(messageId)?.type + } } diff --git a/packages/core/src/codewhispererChat/controllers/chat/controller.ts b/packages/core/src/codewhispererChat/controllers/chat/controller.ts index 1093da9b020..a9711c3ab02 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/controller.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/controller.ts @@ -854,7 +854,7 @@ export class ChatController { } private async closeDiffView(message: CustomFormActionMessage) { - // Close the diff view if User rejected or accepted the generated code changes. + // Close the diff view if User rejected the generated code changes or asked a different question. if (vscode.window.tabGroups.activeTabGroup.activeTab?.label.includes(amazonQTabSuffix)) { await vscode.commands.executeCommand('workbench.action.closeActiveEditor') } @@ -919,9 +919,6 @@ export class ChatController { ) } break - case 'accept-code-diff': - await this.closeDiffView(message) - break case 'reject-code-diff': await this.restoreBackup(message) await this.closeDiffView(message) @@ -1006,7 +1003,26 @@ export class ChatController { } private async processFileClickMessage(message: FileClick) { + /** + * This function is used for 3 useCases + * 1. Read files/folders for Agentic chat + * 2. Read files in workspace context: Project falcon + * 3. Open code diff for generated files in Agentic chat. + */ const session = this.sessionStorage.getSession(message.tabID) + + if (session.getMessageOperation(message.messageId)?.type === 'read') { + try { + await vscode.commands.executeCommand('vscode.open', vscode.Uri.file(message.filePath)) + } catch { + void vscode.window.showInformationMessage( + `Sorry, Amazon Q failed to open the file: ${path.basename(message.filePath)}` + ) + } + } else if (session.getMessageOperation(message.messageId)?.type === 'listDir') { + void vscode.window.showInformationMessage(`Analyzed the directory: ${message.filePath}`) + } + // Check if user clicked on filePath in the contextList or in the fileListTree and perform the functionality accordingly. if (session.showDiffOnFileWrite) { const toolUseId = message.messageId @@ -1030,7 +1046,9 @@ export class ChatController { ) } catch (error) { getLogger().error(`Unexpected error in diff view generation: ${error}`) - void vscode.window.showErrorMessage(`Failed to open diff view.`) + void vscode.window.showErrorMessage( + `Sorry, Amazon Q failed to open the diff view for ${path.basename(message.filePath)}` + ) } } else { const lineRanges = session.contexts.get(message.filePath) @@ -1299,12 +1317,10 @@ export class ChatController { this.processException(e, message.tabID) } } - private sessionCleanUp(session: ChatSession) { + private initialCleanUp(session: ChatSession) { // Create a fresh token for this new conversation session.createNewTokenSource() session.setAgenticLoopInProgress(true) - session.clearListOfReadFiles() - session.clearListOfReadFolders() session.setShowDiffOnFileWrite(false) session.setMessageIdToUpdate(undefined) session.setMessageIdToUpdateListDirectory(undefined) @@ -1316,7 +1332,7 @@ export class ChatController { if (session.agenticLoopInProgress) { session.disposeTokenSource() } - this.sessionCleanUp(session) + this.initialCleanUp(session) this.editorContextExtractor .extractContextForTrigger('ChatMessage') .then(async (context) => { diff --git a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts index 2e02c2037fb..34e2ddc093c 100644 --- a/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts +++ b/packages/core/src/codewhispererChat/controllers/chat/messenger/messenger.ts @@ -56,7 +56,7 @@ import { } from '@aws/mynah-ui' import { Database } from '../../../../shared/db/chatDb/chatDb' import { TabType } from '../../../../amazonq/webview/ui/storages/tabsStorage' -import { ToolType, ToolUtils } from '../../../tools/toolUtils' +import { Tool, ToolType, ToolUtils } from '../../../tools/toolUtils' import { ChatStream } from '../../../tools/chatStream' import path from 'path' import { CommandValidation, ExecuteBashParams } from '../../../tools/executeBash' @@ -193,6 +193,29 @@ export class Messenger { return codeBlocks.length } + public handleFileReadOrListOperation = (session: ChatSession, toolUse: ToolUse, tool: Tool) => { + const messageIdToUpdate = + tool.type === ToolType.FsRead ? session.messageIdToUpdate : session.messageIdToUpdateListDirectory + const messageId = messageIdToUpdate ?? toolUse?.toolUseId ?? '' + const operationType = tool.type === ToolType.FsRead ? 'read' : 'listDir' + const input = toolUse.input as unknown as FsReadParams | ListDirectoryParams + const existingPaths = session.getFilePathsByMessageId(messageId) + + // Check if path already exists in the list + const isPathAlreadyProcessed = existingPaths.some((path) => path.relativeFilePath === input.path) + + if (!isPathAlreadyProcessed) { + session.addMessageOperation(messageId, operationType, [ + ...existingPaths, + { + relativeFilePath: input.path, + lineRanges: [{ first: -1, second: -1 }], + }, + ]) + } + return messageIdToUpdate + } + public async sendAIResponse( response: MessengerResponseType, session: ChatSession, @@ -335,32 +358,8 @@ export class Messenger { session.setShowDiffOnFileWrite(true) changeList = await tool.tool.getDiffChanges() } - if (tool.type === ToolType.FsRead) { - messageIdToUpdate = session.messageIdToUpdate - const input = toolUse.input as unknown as FsReadParams - // Check if this file path is already in the readFiles list - const isFileAlreadyRead = session.readFiles.some( - (file) => file.relativeFilePath === input.path - ) - if (!isFileAlreadyRead) { - session.addToReadFiles({ - relativeFilePath: input?.path, - lineRanges: [{ first: -1, second: -1 }], - }) - } - } else if (tool.type === ToolType.ListDirectory) { - messageIdToUpdate = session.messageIdToUpdateListDirectory - const input = toolUse.input as unknown as ListDirectoryParams - // Check if this folder is already in the readFolders list - const isFolderAlreadyRead = session.readFolders.some( - (folder) => folder.relativeFilePath === input.path - ) - if (!isFolderAlreadyRead) { - session.setReadFolders({ - relativeFilePath: input?.path, - lineRanges: [{ first: -1, second: -1 }], - }) - } + if (isReadOrList) { + messageIdToUpdate = this.handleFileReadOrListOperation(session, toolUse, tool) } const validation = ToolUtils.requiresAcceptance(tool) const chatStream = new ChatStream( @@ -381,14 +380,11 @@ export class Messenger { chatStream, chatStream.validation.requiresAcceptance ) - if (session.messageIdToUpdate === undefined && tool.type === ToolType.FsRead) { + if (!session.messageIdToUpdate && tool.type === ToolType.FsRead) { // Store the first messageId in a chain of tool uses session.setMessageIdToUpdate(toolUse.toolUseId) } - if ( - session.messageIdToUpdateListDirectory === undefined && - tool.type === ToolType.ListDirectory - ) { + if (!session.messageIdToUpdateListDirectory && tool.type === ToolType.ListDirectory) { session.setMessageIdToUpdateListDirectory(toolUse.toolUseId) } getLogger().debug( @@ -706,10 +702,10 @@ export class Messenger { triggerID: string, messageIdToUpdate?: string ) { - const contextList = toolUse.name === ToolType.ListDirectory ? session.readFolders : session.readFiles + const messageID = messageIdToUpdate ?? toolUse?.toolUseId ?? '' + const contextList = messageIdToUpdate || toolUse?.toolUseId ? session.getFilePathsByMessageId(messageID) : [] const isFileRead = toolUse.name === ToolType.FsRead - const items = isFileRead ? session.readFiles : session.readFolders - const itemCount = items.length + const itemCount = contextList.length const title = itemCount < 1 @@ -727,7 +723,7 @@ export class Messenger { followUpsHeader: undefined, relatedSuggestions: undefined, triggerID, - messageID: messageIdToUpdate ?? toolUse?.toolUseId ?? '', + messageID, userIntent: undefined, codeBlockLanguage: undefined, contextList, diff --git a/packages/core/src/codewhispererChat/tools/chatStream.ts b/packages/core/src/codewhispererChat/tools/chatStream.ts index 658c6bc2a46..bdd11e555e9 100644 --- a/packages/core/src/codewhispererChat/tools/chatStream.ts +++ b/packages/core/src/codewhispererChat/tools/chatStream.ts @@ -37,7 +37,7 @@ export class ChatStream extends Writable { ) { super() this.logger.debug( - `ChatStream created for tabID: ${tabID}, triggerID: ${triggerID}, readFiles: ${session.readFiles}, emitEvent to mynahUI: ${emitEvent}` + `ChatStream created for tabID: ${tabID}, triggerID: ${triggerID}, emitEvent to mynahUI: ${emitEvent}` ) if (!emitEvent) { return