From 8bf5fdf96e24f1d5ff624b837ac3b8a3dc68ca20 Mon Sep 17 00:00:00 2001 From: Thiago Verney Date: Thu, 31 Oct 2024 17:00:08 -0400 Subject: [PATCH 1/5] fix(dev): apply file level changes --- .../amazonqFeatureDev/FeatureDevApp.kt | 4 +- .../InboundAppMessagesHandler.kt | 1 + .../controller/FeatureDevController.kt | 28 +++++++++-- .../FeatureDevControllerExtensions.kt | 2 +- .../messages/FeatureDevMessage.kt | 5 ++ .../session/CodeGenerationState.kt | 2 + .../amazonqFeatureDev/session/Session.kt | 34 +++++++++++-- .../session/SessionStateTypes.kt | 2 + .../ui/apps/featureDevChatConnector.ts | 8 +-- .../mynah-ui/src/mynah-ui/ui/commands.ts | 1 + .../mynah-ui/src/mynah-ui/ui/connector.ts | 3 +- .../src/mynah-ui/ui/diffTree/actions.ts | 49 ++++++++++++++----- .../src/mynah-ui/ui/diffTree/types.ts | 1 + .../amazonq/mynah-ui/src/mynah-ui/ui/main.ts | 7 +-- 14 files changed, 117 insertions(+), 30 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevApp.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevApp.kt index 2eb428563ef..bc5bcfc6a75 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevApp.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevApp.kt @@ -42,7 +42,8 @@ class FeatureDevApp : AmazonQApp { "insert_code_at_cursor_position" to IncomingFeatureDevMessage.InsertCodeAtCursorPosition::class, "open-diff" to IncomingFeatureDevMessage.OpenDiff::class, "file-click" to IncomingFeatureDevMessage.FileClicked::class, - "stop-response" to IncomingFeatureDevMessage.StopResponse::class + "stop-response" to IncomingFeatureDevMessage.StopResponse::class, + "store-code-result-message-id" to IncomingFeatureDevMessage.StoreMessageIdMessage::class ) scope.launch { @@ -84,6 +85,7 @@ class FeatureDevApp : AmazonQApp { is IncomingFeatureDevMessage.OpenDiff -> inboundAppMessagesHandler.processOpenDiff(message) is IncomingFeatureDevMessage.FileClicked -> inboundAppMessagesHandler.processFileClicked(message) is IncomingFeatureDevMessage.StopResponse -> inboundAppMessagesHandler.processStopMessage(message) + is IncomingFeatureDevMessage.StoreMessageIdMessage -> inboundAppMessagesHandler.processStoreCodeResultMessageId(message) } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/InboundAppMessagesHandler.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/InboundAppMessagesHandler.kt index 14ca39cd555..6983e7f4ec9 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/InboundAppMessagesHandler.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/InboundAppMessagesHandler.kt @@ -18,4 +18,5 @@ interface InboundAppMessagesHandler { suspend fun processOpenDiff(message: IncomingFeatureDevMessage.OpenDiff) suspend fun processFileClicked(message: IncomingFeatureDevMessage.FileClicked) suspend fun processStopMessage(message: IncomingFeatureDevMessage.StopResponse) + suspend fun processStoreCodeResultMessageId(message: IncomingFeatureDevMessage.StoreMessageIdMessage) } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt index 84d2cd084c8..e82dc4f2ae2 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt @@ -93,6 +93,10 @@ class FeatureDevController( ) } + override suspend fun processStoreCodeResultMessageId(message: IncomingFeatureDevMessage.StoreMessageIdMessage) { + storeCodeResultMessageId(message) + } + override suspend fun processStopMessage(message: IncomingFeatureDevMessage.StopResponse) { handleStopMessage(message) } @@ -244,6 +248,7 @@ class FeatureDevController( val fileToUpdate = message.filePath val session = getSessionInfo(message.tabId) val messageId = message.messageId + val action = message.actionName var filePaths: List = emptyList() var deletedFiles: List = emptyList() @@ -253,10 +258,18 @@ class FeatureDevController( deletedFiles = state.deletedFiles } } - - // Mark the file as rejected or not depending on the previous state - filePaths.find { it.zipFilePath == fileToUpdate }?.let { it.rejected = !it.rejected } - deletedFiles.find { it.zipFilePath == fileToUpdate }?.let { it.rejected = !it.rejected } + if (action == "accept-change") { + session.insertChanges( + filePaths = filePaths.filter { it.zipFilePath == fileToUpdate }, + deletedFiles = deletedFiles.filter { it.zipFilePath == fileToUpdate }, + references = emptyList(), + messenger + ) + } else { + // Mark the file as rejected or not depending on the previous state + filePaths.find { it.zipFilePath == fileToUpdate }?.let { it.rejected = !it.rejected } + deletedFiles.find { it.zipFilePath == fileToUpdate }?.let { it.rejected = !it.rejected } + } messenger.updateFileComponent(message.tabId, filePaths, deletedFiles, messageId) } @@ -335,7 +348,8 @@ class FeatureDevController( session.insertChanges( filePaths = filePaths.filterNot { it.rejected }, deletedFiles = deletedFiles.filterNot { it.rejected }, - references = references + references = references, + messenger ) messenger.sendAnswer( @@ -546,6 +560,10 @@ class FeatureDevController( } } + private fun storeCodeResultMessageId(message: IncomingFeatureDevMessage.StoreMessageIdMessage) { + this.storeCodeResultMessageId(message) + } + private suspend fun handleChat( tabId: String, message: String, diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevControllerExtensions.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevControllerExtensions.kt index 949e1ca42ec..bee2a892822 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevControllerExtensions.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevControllerExtensions.kt @@ -88,7 +88,7 @@ suspend fun FeatureDevController.onCodeGeneration( } // Atm this is the only possible path as codegen is mocked to return empty. - if (filePaths.size or deletedFiles.size == 0) { + if (filePaths.size == 0 && deletedFiles.size == 0) { messenger.sendAnswer( tabId = tabId, messageType = FeatureDevMessageType.Answer, diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessage.kt index 1ad226f96b9..a330ac0848d 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessage.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessage.kt @@ -24,6 +24,11 @@ sealed interface IncomingFeatureDevMessage : FeatureDevBaseMessage { @JsonProperty("tabID") val tabId: String, ) : IncomingFeatureDevMessage + data class StoreMessageIdMessage( + val tabID: String, + val messageId: String, + ) : IncomingFeatureDevMessage + data class NewTabCreated( val command: String, @JsonProperty("tabID") val tabId: String, diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/CodeGenerationState.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/CodeGenerationState.kt index 4cccba2252d..802db7dcf31 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/CodeGenerationState.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/CodeGenerationState.kt @@ -221,6 +221,7 @@ fun registerNewFiles(newFileContents: Map): List zipFilePath = it.key, fileContent = it.value, rejected = false, + changeApplied = false ) } @@ -229,5 +230,6 @@ fun registerDeletedFiles(deletedFiles: List): List = DeletedFileInfo( zipFilePath = it, rejected = false, + changeApplied = false ) } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt index f02fc86609e..c27b8fa1567 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt @@ -7,18 +7,22 @@ import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VfsUtil import software.aws.toolkits.jetbrains.services.amazonq.FeatureDevSessionContext +import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.CODE_GENERATION_RETRY_LIMIT import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ConversationIdNotFoundException import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FEATURE_NAME import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.MAX_PROJECT_SIZE_BYTES import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.clients.FeatureDevClient +import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.IncomingFeatureDevMessage import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.sendAsyncEventProgress +import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.updateFileComponent import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.CancellationTokenSource import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FeatureDevService import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.resolveAndCreateOrUpdateFile import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.resolveAndDeleteFile import software.aws.toolkits.jetbrains.services.cwc.controller.ReferenceLogController +import software.aws.toolkits.jetbrains.services.cwc.storage.ChatSessionStorage class Session(val tabID: String, val project: Project) { var context: FeatureDevSessionContext @@ -31,6 +35,7 @@ class Session(val tabID: String, val project: Project) { private var task: String = "" private val proxyClient: FeatureDevClient private val featureDevService: FeatureDevService + private var _codeResultMessageId: String? = null // retry session state vars private var codegenRetries: Int @@ -86,20 +91,39 @@ class Session(val tabID: String, val project: Project) { ) } + private fun storeCodeResultMessageId(message: IncomingFeatureDevMessage.StoreMessageIdMessage) { + val messageId = message.messageId + this.updateCodeResultMessageId(messageId) + } + + private fun updateCodeResultMessageId(messageId: String) { + this._codeResultMessageId = messageId + } + + /** * Triggered by the Insert code follow-up button to apply code changes. */ - fun insertChanges(filePaths: List, deletedFiles: List, references: List) { + suspend fun insertChanges(filePaths: List, deletedFiles: List, references: List, messenger: MessagePublisher) { val selectedSourceFolder = context.selectedSourceFolder.toNioPath() + val newFilePaths = filePaths.filter { !it.rejected && !it.changeApplied } + val newDeletedFiles = deletedFiles.filter { !it.rejected && !it.changeApplied } + newFilePaths.forEach { + resolveAndCreateOrUpdateFile(selectedSourceFolder, it.zipFilePath, it.fileContent) + it.changeApplied = true + } - filePaths.forEach { resolveAndCreateOrUpdateFile(selectedSourceFolder, it.zipFilePath, it.fileContent) } - - deletedFiles.forEach { resolveAndDeleteFile(selectedSourceFolder, it.zipFilePath) } + newDeletedFiles.forEach { + resolveAndDeleteFile(selectedSourceFolder, it.zipFilePath) + it.changeApplied = true + } ReferenceLogController.addReferenceLog(references, project) - // Taken from https://intellij-support.jetbrains.com/hc/en-us/community/posts/206118439-Refresh-after-external-changes-to-project-structure-and-sources VfsUtil.markDirtyAndRefresh(true, true, true, context.selectedSourceFolder) + if (this._codeResultMessageId != null) { + messenger.updateFileComponent(this.tabID, filePaths, deletedFiles, this._codeResultMessageId!!) + } } suspend fun send(msg: String): Interaction { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionStateTypes.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionStateTypes.kt index 33ee8cb7e4b..39394b09233 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionStateTypes.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionStateTypes.kt @@ -42,11 +42,13 @@ data class NewFileZipInfo( val zipFilePath: String, val fileContent: String, var rejected: Boolean, + var changeApplied: Boolean ) data class DeletedFileInfo( val zipFilePath: String, // The string is the path of the file to be deleted var rejected: Boolean, + var changeApplied: Boolean ) data class CodeGenerationResult( diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/featureDevChatConnector.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/featureDevChatConnector.ts index 711569f65a9..f96b051039a 100644 --- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/featureDevChatConnector.ts +++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/featureDevChatConnector.ts @@ -28,7 +28,7 @@ export interface ConnectorProps { onUpdateAuthentication: (featureDevEnabled: boolean, codeTransformEnabled: boolean, authenticatingTabIDs: string[]) => void onNewTab: (tabType: TabType) => void tabsStorage: TabsStorage - onFileComponentUpdate: (tabID: string, filePaths: DiffTreeFileInfo[], deletedFiles: DiffTreeFileInfo[], messageId: string) => void + onFileComponentUpdate: (tabID: string, filePaths: DiffTreeFileInfo[], deletedFiles: DiffTreeFileInfo[], messageId: string, disableFileActions: boolean) => void } export class Connector { @@ -154,6 +154,8 @@ export class Connector { private processCodeResultMessage = async (messageData: any): Promise => { if (this.onChatAnswerReceived !== undefined) { + const messageId = messageData.messageID ?? messageData.triggerID ?? messageData.conversationID + // this.sendMessageToExtension({ command: 'store-code-result-message-id', tabID: messageData.tabID, messageId, tabType: 'featuredev' }) const actions = getActions([ ...messageData.filePaths, ...messageData.deletedFiles, @@ -165,7 +167,7 @@ export class Connector { canBeVoted: true, codeReference: messageData.references, // TODO get the backend to store a message id in addition to conversationID - messageId: messageData.messageID ?? messageData.triggerID ?? messageData.conversationID, + messageId, fileList: { rootFolderTitle: 'Changes', filePaths: (messageData.filePaths as DiffTreeFileInfo[]).map(path => path.zipFilePath), @@ -202,7 +204,7 @@ export class Connector { handleMessageReceive = async (messageData: any): Promise => { if (messageData.type === 'updateFileComponent') { - this.onFileComponentUpdate(messageData.tabID, messageData.filePaths, messageData.deletedFiles, messageData.messageId) + this.onFileComponentUpdate(messageData.tabID, messageData.filePaths, messageData.deletedFiles, messageData.messageId, messageData.disableFileActions) return } if (messageData.type === 'errorMessage') { diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/commands.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/commands.ts index c1405a1a6eb..20ad900e1e8 100644 --- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/commands.ts +++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/commands.ts @@ -44,5 +44,6 @@ type MessageCommand = | 'codetransform-pom-file-open-click' | 'file-click' | 'open-settings' + | 'store-code-result-message-id' export type ExtensionMessage = Record & { command: MessageCommand } diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/connector.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/connector.ts index cb5ecd1e08c..ab7fc6ac20b 100644 --- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/connector.ts +++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/connector.ts @@ -58,7 +58,8 @@ export interface ConnectorProps { tabID: string, filePaths: DiffTreeFileInfo[], deletedFiles: DiffTreeFileInfo[], - messageId: string + messageId: string, + disableFileActions: boolean ) => void onUpdatePlaceholder: (tabID: string, newPlaceholder: string) => void onChatInputEnabled: (tabID: string, enabled: boolean) => void diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/diffTree/actions.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/diffTree/actions.ts index 1ae3b01bfb3..6604eef54b5 100644 --- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/diffTree/actions.ts +++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/diffTree/actions.ts @@ -7,7 +7,13 @@ import { DiffTreeFileInfo } from './types' export function getDetails(filePaths: DiffTreeFileInfo[]): Record { return filePaths.reduce((details, filePath) => { - if (filePath.rejected) { + if (filePath.changeApplied) { + details[filePath.zipFilePath] = { + status: 'success', + label: 'File accepted', + icon: MynahIcons.OK, + } + } else if (filePath.rejected) { details[filePath.zipFilePath] = { status: 'error', label: 'File rejected', @@ -20,16 +26,37 @@ export function getDetails(filePaths: DiffTreeFileInfo[]): Record { return filePaths.reduce((actions, filePath) => { - actions[filePath.zipFilePath] = [filePath.rejected ? { - icon: MynahIcons.REVERT, - name: 'revert-rejection', - description: 'Revert rejection', - } : { - icon: MynahIcons.CANCEL_CIRCLE, - status: 'error', - name: 'reject-change', - description: 'Reject change', - }] + if (filePath.changeApplied) { + return actions + } + + actions[filePath.zipFilePath] = [ + { + icon: MynahIcons.OK, + status: 'success', + name: 'accept-change', + description: 'Accept file change', + } + ] + + switch (filePath.rejected) { + case true: + actions[filePath.zipFilePath].push({ + icon: MynahIcons.REVERT, + name: 'revert-rejection', + description: 'Revert rejection', + }) + break + case false: + actions[filePath.zipFilePath].push({ + icon: MynahIcons.CANCEL_CIRCLE, + status: 'error', + name: 'reject-change', + description: 'Reject change', + }) + break + } + return actions }, {} as Record) } diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/diffTree/types.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/diffTree/types.ts index a5776300a02..cd59f5f8393 100644 --- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/diffTree/types.ts +++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/diffTree/types.ts @@ -4,4 +4,5 @@ export type DiffTreeFileInfo = { zipFilePath: string rejected: boolean + changeApplied: boolean } diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/main.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/main.ts index 32fa406c759..126d10f446d 100644 --- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/main.ts +++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/main.ts @@ -296,7 +296,8 @@ export const createMynahUI = (ideApi: any, featureDevInitEnabled: boolean, codeT tabID: string, filePaths: DiffTreeFileInfo[], deletedFiles: DiffTreeFileInfo[], - messageId: string + messageId: string, + disableFileActions: boolean = false ) => { const updateWith: Partial = { type: ChatItemType.ANSWER, @@ -304,8 +305,8 @@ export const createMynahUI = (ideApi: any, featureDevInitEnabled: boolean, codeT rootFolderTitle: 'Changes', filePaths: filePaths.map(i => i.zipFilePath), deletedFiles: deletedFiles.map(i => i.zipFilePath), - details: getDetails(filePaths), - actions: getActions([...filePaths, ...deletedFiles]), + details: getDetails([...filePaths, ...deletedFiles]), + actions: disableFileActions ? undefined : getActions([...filePaths, ...deletedFiles]), }, } mynahUI.updateChatAnswerWithMessageId(tabID, messageId, updateWith) From ffd9a3e86140b92bd2ee5dd062201f15235fb5c2 Mon Sep 17 00:00:00 2001 From: Thiago Verney Date: Fri, 1 Nov 2024 17:11:28 -0400 Subject: [PATCH 2/5] fix(dev): include published message --- .../controller/FeatureDevController.kt | 17 +- .../messages/FeatureDevMessage.kt | 5 +- .../FeatureDevMessagePublisherExtensions.kt | 147 +++++--- .../amazonqFeatureDev/session/Session.kt | 91 +++-- .../session/SessionStateTypes.kt | 4 +- .../service/CodeWhispererServiceNew.kt | 336 +++++++++++------- .../ui/apps/featureDevChatConnector.ts | 2 +- 7 files changed, 379 insertions(+), 223 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt index e82dc4f2ae2..61d22801f67 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt @@ -130,6 +130,7 @@ class FeatureDevController( override suspend fun processChatItemVotedMessage(message: IncomingFeatureDevMessage.ChatItemVotedMessage) { logger.debug { "$FEATURE_NAME: Processing ChatItemVotedMessage: $message" } + this.disablePreviousFileList(message.tabId) val session = chatSessionStorage.getSession(message.tabId, context.project) when (message.vote) { @@ -393,6 +394,8 @@ class FeatureDevController( private suspend fun newTask(tabId: String, isException: Boolean? = false) { val session = getSessionInfo(tabId) val sessionLatency = System.currentTimeMillis() - session.sessionStartTime + + this.disablePreviousFileList(tabId) AmazonqTelemetry.endChat( amazonqConversationId = session.conversationId, amazonqEndOfTheConversationLatency = sessionLatency.toDouble(), @@ -413,6 +416,7 @@ class FeatureDevController( } private suspend fun closeSession(tabId: String) { + this.disablePreviousFileList(tabId) messenger.sendAnswer( tabId = tabId, messageType = FeatureDevMessageType.Answer, @@ -560,8 +564,19 @@ class FeatureDevController( } } + private suspend fun disablePreviousFileList(tabId: String) { + val session = getSessionInfo(tabId) + when (val sessionState = session.sessionState) { + is PrepareCodeGenerationState -> { + session.disableFileList(sessionState.filePaths, sessionState.deletedFiles, messenger) + } + } + } + private fun storeCodeResultMessageId(message: IncomingFeatureDevMessage.StoreMessageIdMessage) { - this.storeCodeResultMessageId(message) + val tabId = message.tabId + val session = getSessionInfo(tabId) + session.storeCodeResultMessageId(message) } private suspend fun handleChat( diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessage.kt index a330ac0848d..c2c2d2326e4 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessage.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessage.kt @@ -25,8 +25,8 @@ sealed interface IncomingFeatureDevMessage : FeatureDevBaseMessage { ) : IncomingFeatureDevMessage data class StoreMessageIdMessage( - val tabID: String, - val messageId: String, + @JsonProperty("tabID") val tabId: String, + val messageId: String?, ) : IncomingFeatureDevMessage data class NewTabCreated( @@ -154,6 +154,7 @@ data class FileComponent( val filePaths: List, val deletedFiles: List, val messageId: String, + val disableFileActions: Boolean, ) : UiMessage( tabId = tabId, type = "updateFileComponent" diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessagePublisherExtensions.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessagePublisherExtensions.kt index 1aa249b750b..92afebf6d49 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessagePublisherExtensions.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessagePublisherExtensions.kt @@ -30,7 +30,7 @@ suspend fun MessagePublisher.sendAnswer( message = message, followUps = followUp, canBeVoted = canBeVoted ?: false, - snapToTop = snapToTop ?: false + snapToTop = snapToTop ?: false, ) this.publish(chatMessage) } @@ -44,7 +44,7 @@ suspend fun MessagePublisher.sendAnswerPart( tabId = tabId, message = message, messageType = FeatureDevMessageType.AnswerPart, - canBeVoted = canBeVoted + canBeVoted = canBeVoted, ) } @@ -55,44 +55,66 @@ suspend fun MessagePublisher.sendSystemPrompt( this.sendAnswer( tabId = tabId, messageType = FeatureDevMessageType.SystemPrompt, - followUp = followUp + followUp = followUp, ) } -suspend fun MessagePublisher.updateFileComponent(tabId: String, filePaths: List, deletedFiles: List, messageId: String) { - val fileComponentMessage = FileComponent( - tabId = tabId, - filePaths = filePaths, - deletedFiles = deletedFiles, - messageId = messageId, - ) +suspend fun MessagePublisher.updateFileComponent( + tabId: String, + filePaths: List, + deletedFiles: List, + messageId: String, + disableFileActions: Boolean = false, +) { + val fileComponentMessage = + FileComponent( + tabId = tabId, + filePaths = filePaths, + deletedFiles = deletedFiles, + messageId = messageId, + disableFileActions = disableFileActions, + ) this.publish(fileComponentMessage) } -suspend fun MessagePublisher.sendAsyncEventProgress(tabId: String, inProgress: Boolean, message: String? = null) { - val asyncEventProgressMessage = AsyncEventProgressMessage( - tabId = tabId, - message = message, - inProgress = inProgress, - ) +suspend fun MessagePublisher.sendAsyncEventProgress( + tabId: String, + inProgress: Boolean, + message: String? = null, +) { + val asyncEventProgressMessage = + AsyncEventProgressMessage( + tabId = tabId, + message = message, + inProgress = inProgress, + ) this.publish(asyncEventProgressMessage) } -suspend fun MessagePublisher.sendUpdatePlaceholder(tabId: String, newPlaceholder: String) { - val updatePlaceholderMessage = UpdatePlaceholderMessage( - tabId = tabId, - newPlaceholder = newPlaceholder - ) +suspend fun MessagePublisher.sendUpdatePlaceholder( + tabId: String, + newPlaceholder: String, +) { + val updatePlaceholderMessage = + UpdatePlaceholderMessage( + tabId = tabId, + newPlaceholder = newPlaceholder, + ) this.publish(updatePlaceholderMessage) } -suspend fun MessagePublisher.sendAuthNeededException(tabId: String, triggerId: String, credentialState: AuthNeededState) { - val message = AuthNeededException( - tabId = tabId, - triggerId = triggerId, - authType = credentialState.authType, - message = credentialState.message, - ) +suspend fun MessagePublisher.sendAuthNeededException( + tabId: String, + triggerId: String, + credentialState: AuthNeededState, +) { + val message = + AuthNeededException( + tabId = tabId, + triggerId = triggerId, + authType = credentialState.authType, + message = credentialState.message, + ) this.publish(message) } @@ -103,15 +125,26 @@ suspend fun MessagePublisher.sendAuthenticationInProgressMessage(tabId: String) message = message("amazonqFeatureDev.follow_instructions_for_authentication"), ) } -suspend fun MessagePublisher.sendChatInputEnabledMessage(tabId: String, enabled: Boolean) { - val chatInputEnabledMessage = ChatInputEnabledMessage( - tabId, - enabled, - ) + +suspend fun MessagePublisher.sendChatInputEnabledMessage( + tabId: String, + enabled: Boolean, +) { + val chatInputEnabledMessage = + ChatInputEnabledMessage( + tabId, + enabled, + ) this.publish(chatInputEnabledMessage) } -suspend fun MessagePublisher.sendError(tabId: String, errMessage: String?, retries: Int, conversationId: String? = null, showDefaultMessage: Boolean? = false) { +suspend fun MessagePublisher.sendError( + tabId: String, + errMessage: String?, + retries: Int, + conversationId: String? = null, + showDefaultMessage: Boolean? = false, +) { val conversationIdText = if (conversationId == null) "" else "\n\nConversation ID: **$conversationId**" if (retries == 0) { @@ -124,12 +157,13 @@ suspend fun MessagePublisher.sendError(tabId: String, errMessage: String?, retri this.sendAnswer( tabId = tabId, messageType = FeatureDevMessageType.SystemPrompt, - followUp = listOf( + followUp = + listOf( FollowUp( pillText = message("amazonqFeatureDev.follow_up.send_feedback"), type = FollowUpTypes.SEND_FEEDBACK, - status = FollowUpStatusType.Info - ) + status = FollowUpStatusType.Info, + ), ), ) return @@ -144,12 +178,13 @@ suspend fun MessagePublisher.sendError(tabId: String, errMessage: String?, retri this.sendAnswer( tabId = tabId, messageType = FeatureDevMessageType.SystemPrompt, - followUp = listOf( + followUp = + listOf( FollowUp( pillText = message("amazonqFeatureDev.follow_up.retry"), type = FollowUpTypes.RETRY, - status = FollowUpStatusType.Warning - ) + status = FollowUpStatusType.Warning, + ), ), ) } @@ -158,7 +193,7 @@ suspend fun MessagePublisher.sendMonthlyLimitError(tabId: String) { this.sendAnswer( tabId = tabId, messageType = FeatureDevMessageType.Answer, - message = message("amazonqFeatureDev.exception.monthly_limit_error") + message = message("amazonqFeatureDev.exception.monthly_limit_error"), ) this.sendUpdatePlaceholder(tabId = tabId, newPlaceholder = message("amazonqFeatureDev.placeholder.after_monthly_limit")) } @@ -178,18 +213,20 @@ suspend fun MessagePublisher.sendCodeResult( deletedFiles: List, references: List, ) { - val refs = references.map { ref -> - CodeReference( - licenseName = ref.licenseName, - repository = ref.repository, - url = ref.url, - recommendationContentSpan = RecommendationContentSpan( - ref.recommendationContentSpan?.start ?: 0, - ref.recommendationContentSpan?.end ?: 0, - ), - information = "Reference code under **${ref.licenseName}** license from repository [${ref.repository}](${ref.url})" - ) - } + val refs = + references.map { ref -> + CodeReference( + licenseName = ref.licenseName, + repository = ref.repository, + url = ref.url, + recommendationContentSpan = + RecommendationContentSpan( + ref.recommendationContentSpan?.start ?: 0, + ref.recommendationContentSpan?.end ?: 0, + ), + information = "Reference code under **${ref.licenseName}** license from repository [${ref.repository}](${ref.url})", + ) + } this.publish( CodeResultMessage( @@ -197,7 +234,7 @@ suspend fun MessagePublisher.sendCodeResult( conversationId = uploadId, filePaths = filePaths, deletedFiles = deletedFiles, - references = refs - ) + references = refs, + ), ) } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt index c27b8fa1567..658279b3269 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt @@ -7,7 +7,6 @@ import com.intellij.openapi.diagnostic.logger import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VfsUtil import software.aws.toolkits.jetbrains.services.amazonq.FeatureDevSessionContext -import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.CODE_GENERATION_RETRY_LIMIT import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ConversationIdNotFoundException @@ -22,9 +21,11 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FeatureDe import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.resolveAndCreateOrUpdateFile import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.resolveAndDeleteFile import software.aws.toolkits.jetbrains.services.cwc.controller.ReferenceLogController -import software.aws.toolkits.jetbrains.services.cwc.storage.ChatSessionStorage -class Session(val tabID: String, val project: Project) { +class Session( + val tabID: String, + val project: Project, +) { var context: FeatureDevSessionContext val sessionStartTime = System.currentTimeMillis() @@ -57,7 +58,10 @@ class Session(val tabID: String, val project: Project) { /** * Preload any events that have to run before a chat message can be sent */ - suspend fun preloader(msg: String, messenger: MessagePublisher) { + suspend fun preloader( + msg: String, + messenger: MessagePublisher, + ) { if (!preloaderFinished) { setupConversation(msg, messenger) preloaderFinished = true @@ -69,7 +73,10 @@ class Session(val tabID: String, val project: Project) { /** * Starts a conversation with the backend and uploads the repo for the LLMs to be able to use it. */ - private fun setupConversation(msg: String, messenger: MessagePublisher) { + private fun setupConversation( + msg: String, + messenger: MessagePublisher, + ) { // Store the initial message when setting up the conversation so that if it fails we can retry with this message _latestMessage = msg @@ -77,34 +84,39 @@ class Session(val tabID: String, val project: Project) { logger().info(conversationIDLog(this.conversationId)) val sessionStateConfig = getSessionStateConfig().copy(conversationId = this.conversationId) - _state = PrepareCodeGenerationState( - tabID = sessionState.tabID, - approach = sessionState.approach, - config = sessionStateConfig, - filePaths = emptyList(), - deletedFiles = emptyList(), - references = emptyList(), - currentIteration = 1, // first code gen iteration - uploadId = "", // There is no code gen uploadId so far - messenger = messenger, - token = CancellationTokenSource() - ) + _state = + PrepareCodeGenerationState( + tabID = sessionState.tabID, + approach = sessionState.approach, + config = sessionStateConfig, + filePaths = emptyList(), + deletedFiles = emptyList(), + references = emptyList(), + currentIteration = 1, // first code gen iteration + uploadId = "", // There is no code gen uploadId so far + messenger = messenger, + token = CancellationTokenSource(), + ) } - private fun storeCodeResultMessageId(message: IncomingFeatureDevMessage.StoreMessageIdMessage) { + fun storeCodeResultMessageId(message: IncomingFeatureDevMessage.StoreMessageIdMessage) { val messageId = message.messageId this.updateCodeResultMessageId(messageId) } - private fun updateCodeResultMessageId(messageId: String) { + private fun updateCodeResultMessageId(messageId: String?) { this._codeResultMessageId = messageId } - /** * Triggered by the Insert code follow-up button to apply code changes. */ - suspend fun insertChanges(filePaths: List, deletedFiles: List, references: List, messenger: MessagePublisher) { + suspend fun insertChanges( + filePaths: List, + deletedFiles: List, + references: List, + messenger: MessagePublisher, + ) { val selectedSourceFolder = context.selectedSourceFolder.toNioPath() val newFilePaths = filePaths.filter { !it.rejected && !it.changeApplied } val newDeletedFiles = deletedFiles.filter { !it.rejected && !it.changeApplied } @@ -126,6 +138,21 @@ class Session(val tabID: String, val project: Project) { } } + suspend fun disableFileList( + filePaths: List, + deletedFiles: List, + messenger: MessagePublisher, + ) { + if (this._codeResultMessageId.isNullOrEmpty()) { + return + } + + if (this._codeResultMessageId != null) { + messenger.updateFileComponent(this.tabID, filePaths, deletedFiles, this._codeResultMessageId!!, true) + } + this._codeResultMessageId = null + } + suspend fun send(msg: String): Interaction { // When the task/"thing to do" hasn't been set yet, we want it to be the incoming message if (task.isEmpty() && msg.isNotEmpty()) { @@ -137,11 +164,12 @@ class Session(val tabID: String, val project: Project) { } private suspend fun nextInteraction(msg: String): Interaction { - var action = SessionStateAction( - task = task, - msg = msg, - token = sessionState.token - ) + var action = + SessionStateAction( + task = task, + msg = msg, + token = sessionState.token, + ) val resp = sessionState.interact(action) if (resp.nextState != null) { // Approach may have been changed after the interaction @@ -156,11 +184,12 @@ class Session(val tabID: String, val project: Project) { return resp.interaction } - private fun getSessionStateConfig(): SessionStateConfig = SessionStateConfig( - conversationId = this.conversationId, - repoContext = this.context, - featureDevService = this.featureDevService, - ) + private fun getSessionStateConfig(): SessionStateConfig = + SessionStateConfig( + conversationId = this.conversationId, + repoContext = this.context, + featureDevService = this.featureDevService, + ) val conversationId: String get() { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionStateTypes.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionStateTypes.kt index 39394b09233..e0af47770b9 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionStateTypes.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionStateTypes.kt @@ -42,13 +42,13 @@ data class NewFileZipInfo( val zipFilePath: String, val fileContent: String, var rejected: Boolean, - var changeApplied: Boolean + var changeApplied: Boolean, ) data class DeletedFileInfo( val zipFilePath: String, // The string is the path of the file to be deleted var rejected: Boolean, - var changeApplied: Boolean + var changeApplied: Boolean, ) data class CodeGenerationResult( diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererServiceNew.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererServiceNew.kt index 27f1a302f91..dfbbbc38ab4 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererServiceNew.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererServiceNew.kt @@ -95,7 +95,9 @@ import software.aws.toolkits.telemetry.CodewhispererTriggerType import java.util.concurrent.TimeUnit @Service -class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { +class CodeWhispererServiceNew( + private val cs: CoroutineScope, +) : Disposable { private val codeInsightSettingsFacade = CodeInsightsSettingsFacade() private var refreshFailure: Int = 0 private val ongoingRequests = mutableMapOf() @@ -108,15 +110,17 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { } private var job: Job? = null + fun showRecommendationsInPopup( editor: Editor, triggerTypeInfo: TriggerTypeInfo, latencyContext: LatencyContext, ): Job? { if (job == null || job?.isCompleted == true) { - job = cs.launch(getCoroutineBgContext()) { - doShowRecommendationsInPopup(editor, triggerTypeInfo, latencyContext) - } + job = + cs.launch(getCoroutineBgContext()) { + doShowRecommendationsInPopup(editor, triggerTypeInfo, latencyContext) + } } // did some wrangling, but compiler didn't believe this can't be null @@ -137,19 +141,21 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { if (isQExpired(project)) { // consider changing to only running once a ~minute since this is relatively expensive // say the connection is un-refreshable if refresh fails for 3 times - val shouldReauth = if (refreshFailure < MAX_REFRESH_ATTEMPT) { - val attempt = withContext(getCoroutineBgContext()) { - promptReAuth(project) - } + val shouldReauth = + if (refreshFailure < MAX_REFRESH_ATTEMPT) { + val attempt = + withContext(getCoroutineBgContext()) { + promptReAuth(project) + } - if (!attempt) { - refreshFailure++ - } + if (!attempt) { + refreshFailure++ + } - attempt - } else { - true - } + attempt + } else { + true + } if (shouldReauth) { return @@ -170,13 +176,14 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { if (isInjectedFile) return val currentJobId = jobId++ - val requestContext = try { - getRequestContext(triggerTypeInfo, editor, project, psiFile) - } catch (e: Exception) { - LOG.debug { e.message.toString() } - CodeWhispererTelemetryServiceNew.getInstance().sendFailedServiceInvocationEvent(project, e::class.simpleName) - return - } + val requestContext = + try { + getRequestContext(triggerTypeInfo, editor, project, psiFile) + } catch (e: Exception) { + LOG.debug { e.message.toString() } + CodeWhispererTelemetryServiceNew.getInstance().sendFailedServiceInvocationEvent(project, e::class.simpleName) + return + } val caretContext = requestContext.fileContextInfo.caretContext ongoingRequestsContext.forEach { (k, v) -> val vCaretContext = v.fileContextInfo.caretContext @@ -188,18 +195,20 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { val language = requestContext.fileContextInfo.programmingLanguage val leftContext = requestContext.fileContextInfo.caretContext.leftFileContext - if (!language.isCodeCompletionSupported() || ( - language is CodeWhispererJson && !isSupportedJsonFormat( - requestContext.fileContextInfo.filename, - leftContext - ) + if (!language.isCodeCompletionSupported() || + ( + language is CodeWhispererJson && + !isSupportedJsonFormat( + requestContext.fileContextInfo.filename, + leftContext, + ) ) ) { LOG.debug { "Programming language $language is not supported by CodeWhisperer" } if (triggerTypeInfo.triggerType == CodewhispererTriggerType.OnDemand) { showCodeWhispererInfoHint( requestContext.editor, - message("codewhisperer.language.error", psiFile.fileType.name) + message("codewhisperer.language.error", psiFile.fileType.name), ) } return @@ -219,7 +228,11 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { invokeCodeWhispererInBackground(requestContext, currentJobId, latencyContext) } - internal suspend fun invokeCodeWhispererInBackground(requestContext: RequestContextNew, currentJobId: Int, latencyContext: LatencyContext) { + internal suspend fun invokeCodeWhispererInBackground( + requestContext: RequestContextNew, + currentJobId: Int, + latencyContext: LatencyContext, + ) { ongoingRequestsContext[currentJobId] = requestContext val sessionContext = sessionContext ?: SessionContextNew(requestContext.project, requestContext.editor, latencyContext = latencyContext) @@ -240,13 +253,14 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { var lastRecommendationIndex = -1 try { - val responseIterable = CodeWhispererClientAdaptor.getInstance(requestContext.project).generateCompletionsPaginator( - buildCodeWhispererRequest( - requestContext.fileContextInfo, - requestContext.awaitSupplementalContext(), - requestContext.customizationArn + val responseIterable = + CodeWhispererClientAdaptor.getInstance(requestContext.project).generateCompletionsPaginator( + buildCodeWhispererRequest( + requestContext.fileContextInfo, + requestContext.awaitSupplementalContext(), + requestContext.customizationArn, + ), ) - ) var startTime = System.nanoTime() latencyContext.codewhispererPreprocessingEnd = System.nanoTime() @@ -272,7 +286,10 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { val responseContext = ResponseContext(sessionId) logServiceInvocation(requestId, requestContext, responseContext, response.completions(), latency, null) lastRecommendationIndex += response.completions().size - ApplicationManager.getApplication().messageBus.syncPublisher(CODEWHISPERER_CODE_COMPLETION_PERFORMED) + ApplicationManager + .getApplication() + .messageBus + .syncPublisher(CODEWHISPERER_CODE_COMPLETION_PERFORMED) .onSuccess(requestContext.fileContextInfo) CodeWhispererTelemetryServiceNew.getInstance().sendServiceInvocationEvent( currentJobId, @@ -282,7 +299,7 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { lastRecommendationIndex, true, latency, - null + null, ) val validatedResponse = validateResponse(response) @@ -316,7 +333,7 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { it, ongoingRequests[currentJobId], cs, - currentJobId + currentJobId, ) if (!ongoingRequests.contains(currentJobId)) { job?.cancel() @@ -333,7 +350,7 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { workerContext, ongoingRequests[currentJobId], cs, - currentJobId + currentJobId, ) if (!ongoingRequests.contains(currentJobId)) { job?.cancel() @@ -365,7 +382,12 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { (e as CodeWhispererRuntimeException) requestId = e.requestId() ?: "" - sessionId = e.awsErrorDetails().sdkHttpResponse().headers().getOrDefault(KET_SESSION_ID, listOf(requestId))[0] + sessionId = + e + .awsErrorDetails() + .sdkHttpResponse() + .headers() + .getOrDefault(KET_SESSION_ID, listOf(requestId))[0] val exceptionType = e::class.simpleName val responseContext = ResponseContext(sessionId) @@ -377,7 +399,7 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { lastRecommendationIndex, false, 0.0, - exceptionType + exceptionType, ) LOG.debug { @@ -390,14 +412,15 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { title = "", content = message("codewhisperer.notification.custom.not_available"), project = requestContext.project, - notificationActions = listOf( + notificationActions = + listOf( NotificationAction.create( - message("codewhisperer.notification.custom.simple.button.select_another_customization") + message("codewhisperer.notification.custom.simple.button.select_another_customization"), ) { _, notification -> CodeWhispererModelConfigurator.getInstance().showConfigDialog(requestContext.project) notification.expire() - } - ) + }, + ), ) CodeWhispererInvocationStatusNew.getInstance().finishInvocation() @@ -406,16 +429,26 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { showRecommendationsInPopup( requestContext.editor, requestContext.triggerTypeInfo, - latencyContext + latencyContext, ) return } else if (e is CodeWhispererException) { requestId = e.requestId() ?: "" - sessionId = e.awsErrorDetails().sdkHttpResponse().headers().getOrDefault(KET_SESSION_ID, listOf(requestId))[0] + sessionId = + e + .awsErrorDetails() + .sdkHttpResponse() + .headers() + .getOrDefault(KET_SESSION_ID, listOf(requestId))[0] displayMessage = e.awsErrorDetails().errorMessage() ?: message("codewhisperer.trigger.error.server_side") } else if (e is CodeWhispererRuntimeException) { requestId = e.requestId() ?: "" - sessionId = e.awsErrorDetails().sdkHttpResponse().headers().getOrDefault(KET_SESSION_ID, listOf(requestId))[0] + sessionId = + e + .awsErrorDetails() + .sdkHttpResponse() + .headers() + .getOrDefault(KET_SESSION_ID, listOf(requestId))[0] displayMessage = e.awsErrorDetails().errorMessage() ?: message("codewhisperer.trigger.error.server_side") } else { requestId = "" @@ -443,7 +476,7 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { lastRecommendationIndex, false, 0.0, - exceptionType + exceptionType, ) if (e is ThrottlingException && @@ -501,10 +534,11 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { CodeWhispererInvocationStatusNew.getInstance().finishInvocation() - val caretMovement = CodeWhispererEditorManagerNew.getInstance().getCaretMovement( - requestContext.editor, - requestContext.caretPosition - ) + val caretMovement = + CodeWhispererEditorManagerNew.getInstance().getCaretMovement( + requestContext.editor, + requestContext.caretPosition, + ) val isPopupShowing = checkRecommendationsValidity(currStates, false) val nextStates: InvocationContextNew? if (currStates == null) { @@ -543,11 +577,11 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { false, false, "", - CodewhispererCompletionType.Line + CodewhispererCompletionType.Line, ), -1, CodewhispererSuggestionState.Empty, - nextStates.recommendationContext.details.size + nextStates.recommendationContext.details.size, ) } if (!hasAtLeastOneValid) { @@ -577,19 +611,22 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { if (caretMovement == CaretMovement.MOVE_BACKWARD) { LOG.debug { "Caret moved backward, discarding all of the recommendations and exiting the session. Request ID: $requestId, jobId: $jobId" } - val detailContexts = recommendations.map { - DetailContextNew("", it, it, true, false, "", getCompletionType(it)) - }.toMutableList() + val detailContexts = + recommendations + .map { + DetailContextNew("", it, it, true, false, "", getCompletionType(it)) + }.toMutableList() val recommendationContext = RecommendationContextNew(detailContexts, "", "", VisualPosition(0, 0), jobId) ongoingRequests[jobId] = buildInvocationContext(requestContext, responseContext, recommendationContext) disposeDisplaySession(false) return null } - val userInputOriginal = CodeWhispererEditorManagerNew.getInstance().getUserInputSinceInvocation( - requestContext.editor, - requestContext.caretPosition.offset - ) + val userInputOriginal = + CodeWhispererEditorManagerNew.getInstance().getUserInputSinceInvocation( + requestContext.editor, + requestContext.caretPosition.offset, + ) val userInput = if (caretMovement == CaretMovement.NO_CHANGE) { LOG.debug { "Caret position not changed since invocation. Request ID: $requestId" } @@ -603,12 +640,13 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { } } } - val detailContexts = CodeWhispererRecommendationManager.getInstance().buildDetailContext( - requestContext, - userInput, - recommendations, - requestId - ) + val detailContexts = + CodeWhispererRecommendationManager.getInstance().buildDetailContext( + requestContext, + userInput, + recommendations, + requestId, + ) val recommendationContext = RecommendationContextNew(detailContexts, userInputOriginal, userInput, visualPosition, jobId) ongoingRequests[jobId] = buildInvocationContext(requestContext, responseContext, recommendationContext) return ongoingRequests[jobId] @@ -619,18 +657,22 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { response: GenerateCompletionsResponse, ): InvocationContextNew { val recommendationContext = states.recommendationContext - val newDetailContexts = CodeWhispererRecommendationManager.getInstance().buildDetailContext( - states.requestContext, - recommendationContext.userInputSinceInvocation, - response.completions(), - response.responseMetadata().requestId() - ) + val newDetailContexts = + CodeWhispererRecommendationManager.getInstance().buildDetailContext( + states.requestContext, + recommendationContext.userInputSinceInvocation, + response.completions(), + response.responseMetadata().requestId(), + ) recommendationContext.details.addAll(newDetailContexts) return states } - private fun checkRecommendationsValidity(states: InvocationContextNew?, showHint: Boolean): Boolean { + private fun checkRecommendationsValidity( + states: InvocationContextNew?, + showHint: Boolean, + ): Boolean { if (states == null) return false val details = states.recommendationContext.details @@ -640,13 +682,17 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { if (!hasAtLeastOneValid && showHint && states.requestContext.triggerTypeInfo.triggerType == CodewhispererTriggerType.OnDemand) { showCodeWhispererInfoHint( states.requestContext.editor, - message("codewhisperer.popup.no_recommendations") + message("codewhisperer.popup.no_recommendations"), ) } return hasAtLeastOneValid } - private fun updateCodeWhisperer(sessionContext: SessionContextNew, states: InvocationContextNew, recommendationAdded: Boolean) { + private fun updateCodeWhisperer( + sessionContext: SessionContextNew, + states: InvocationContextNew, + recommendationAdded: Boolean, + ) { CodeWhispererPopupManagerNew.getInstance().changeStatesForShowing(sessionContext, states, recommendationAdded) } @@ -694,14 +740,15 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { // the upper bound for supplemental context duration is 50ms // 2. supplemental context - val supplementalContext = cs.async { - try { - FileContextProvider.getInstance(project).extractSupplementalFileContext(psiFile, fileContext, timeout = SUPPLEMENTAL_CONTEXT_TIMEOUT) - } catch (e: Exception) { - LOG.warn { "Run into unexpected error when fetching supplemental context, error: ${e.message}" } - null + val supplementalContext = + cs.async { + try { + FileContextProvider.getInstance(project).extractSupplementalFileContext(psiFile, fileContext, timeout = SUPPLEMENTAL_CONTEXT_TIMEOUT) + } catch (e: Exception) { + LOG.warn { "Run into unexpected error when fetching supplemental context, error: ${e.message}" } + null + } } - } // 3. caret position val caretPosition = runReadAction { getCaretPosition(editor) } @@ -718,18 +765,21 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { fun validateResponse(response: GenerateCompletionsResponse): GenerateCompletionsResponse { // If contentSpans in reference are not consistent with content(recommendations), // remove the incorrect references. - val validatedRecommendations = response.completions().map { - val validReferences = it.hasReferences() && it.references().isNotEmpty() && - it.references().none { reference -> - val span = reference.recommendationContentSpan() - span.start() > span.end() || span.start() < 0 || span.end() > it.content().length + val validatedRecommendations = + response.completions().map { + val validReferences = + it.hasReferences() && + it.references().isNotEmpty() && + it.references().none { reference -> + val span = reference.recommendationContentSpan() + span.start() > span.end() || span.start() < 0 || span.end() > it.content().length + } + if (validReferences) { + it + } else { + it.toBuilder().references(DefaultSdkAutoConstructList.getInstance()).build() } - if (validReferences) { - it - } else { - it.toBuilder().references(DefaultSdkAutoConstructList.getInstance()).build() } - } return response.toBuilder().completions(validatedRecommendations).build() } @@ -759,8 +809,10 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { latency: Double?, exceptionType: String?, ) { - val recommendationLogs = recommendations.map { it.content().trimEnd() } - .reduceIndexedOrNull { index, acc, recommendation -> "$acc\n[${index + 1}]\n$recommendation" } + val recommendationLogs = + recommendations + .map { it.content().trimEnd() } + .reduceIndexedOrNull { index, acc, recommendation -> "$acc\n[${index + 1}]\n$recommendation" } LOG.info { "SessionId: ${responseContext.sessionId}, " + "RequestId: $requestId, " + @@ -776,7 +828,10 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { } } - fun canDoInvocation(editor: Editor, type: CodewhispererTriggerType): Boolean { + fun canDoInvocation( + editor: Editor, + type: CodewhispererTriggerType, + ): Boolean { editor.project?.let { if (!isCodeWhispererEnabled(it)) { return false @@ -795,11 +850,17 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { return true } - fun showCodeWhispererInfoHint(editor: Editor, message: String) { + fun showCodeWhispererInfoHint( + editor: Editor, + message: String, + ) { HintManager.getInstance().showInformationHint(editor, message, HintManager.UNDER) } - fun showCodeWhispererErrorHint(editor: Editor, message: String) { + fun showCodeWhispererErrorHint( + editor: Editor, + message: String, + ) { HintManager.getInstance().showErrorHint(editor, message, HintManager.UNDER) } @@ -810,13 +871,15 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { private const val MAX_REFRESH_ATTEMPT = 3 private const val PAGINATION_REQUEST_COUNT_ALLOWED = 1 - val CODEWHISPERER_INTELLISENSE_POPUP_ON_HOVER: Topic = Topic.create( - "CodeWhisperer intelliSense popup on hover", - CodeWhispererIntelliSenseOnHoverListener::class.java - ) + val CODEWHISPERER_INTELLISENSE_POPUP_ON_HOVER: Topic = + Topic.create( + "CodeWhisperer intelliSense popup on hover", + CodeWhispererIntelliSenseOnHoverListener::class.java, + ) val KEY_SESSION_CONTEXT = Key.create("codewhisperer.session") fun getInstance(): CodeWhispererServiceNew = service() + const val KET_SESSION_ID = "x-amzn-SessionId" private var reAuthPromptShown = false @@ -831,28 +894,38 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { supplementalContext: SupplementalContextInfo?, customizationArn: String?, ): GenerateCompletionsRequest { - val programmingLanguage = ProgrammingLanguage.builder() - .languageName(fileContextInfo.programmingLanguage.toCodeWhispererRuntimeLanguage().languageId) - .build() - val fileContext = FileContext.builder() - .leftFileContent(fileContextInfo.caretContext.leftFileContext) - .rightFileContent(fileContextInfo.caretContext.rightFileContext) - .filename(fileContextInfo.fileRelativePath ?: fileContextInfo.filename) - .programmingLanguage(programmingLanguage) - .build() - val supplementalContexts = supplementalContext?.contents?.map { - SupplementalContext.builder() - .content(it.content) - .filePath(it.path) + val programmingLanguage = + ProgrammingLanguage + .builder() + .languageName(fileContextInfo.programmingLanguage.toCodeWhispererRuntimeLanguage().languageId) .build() - }.orEmpty() - val includeCodeWithReference = if (CodeWhispererSettings.getInstance().isIncludeCodeWithReference()) { - RecommendationsWithReferencesPreference.ALLOW - } else { - RecommendationsWithReferencesPreference.BLOCK - } + val fileContext = + FileContext + .builder() + .leftFileContent(fileContextInfo.caretContext.leftFileContext) + .rightFileContent(fileContextInfo.caretContext.rightFileContext) + .filename(fileContextInfo.fileRelativePath ?: fileContextInfo.filename) + .programmingLanguage(programmingLanguage) + .build() + val supplementalContexts = + supplementalContext + ?.contents + ?.map { + SupplementalContext + .builder() + .content(it.content) + .filePath(it.path) + .build() + }.orEmpty() + val includeCodeWithReference = + if (CodeWhispererSettings.getInstance().isIncludeCodeWithReference()) { + RecommendationsWithReferencesPreference.ALLOW + } else { + RecommendationsWithReferencesPreference.BLOCK + } - return GenerateCompletionsRequest.builder() + return GenerateCompletionsRequest + .builder() .fileContext(fileContext) .supplementalContexts(supplementalContexts) .referenceTrackerConfiguration { it.recommendationsWithReferences(includeCodeWithReference) } @@ -876,16 +949,17 @@ data class RequestContextNew( // TODO: should make the entire getRequestContext() suspend function instead of making supplemental context only var supplementalContext: SupplementalContextInfo? = null private set - get() = when (field) { - null -> { - if (!supplementalContextDeferred.isCompleted) { - error("attempt to access supplemental context before awaiting the deferred") - } else { - null + get() = + when (field) { + null -> { + if (!supplementalContextDeferred.isCompleted) { + error("attempt to access supplemental context before awaiting the deferred") + } else { + null + } } + else -> field } - else -> field - } suspend fun awaitSupplementalContext(): SupplementalContextInfo? { supplementalContext = supplementalContextDeferred.await() diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/featureDevChatConnector.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/featureDevChatConnector.ts index f96b051039a..033701f63bc 100644 --- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/featureDevChatConnector.ts +++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/featureDevChatConnector.ts @@ -155,7 +155,7 @@ export class Connector { private processCodeResultMessage = async (messageData: any): Promise => { if (this.onChatAnswerReceived !== undefined) { const messageId = messageData.messageID ?? messageData.triggerID ?? messageData.conversationID - // this.sendMessageToExtension({ command: 'store-code-result-message-id', tabID: messageData.tabID, messageId, tabType: 'featuredev' }) + this.sendMessageToExtension({ command: 'store-code-result-message-id', tabID: messageData.tabID, messageId, tabType: 'featuredev' }) const actions = getActions([ ...messageData.filePaths, ...messageData.deletedFiles, From a18baf737907b32fe5e50c63fbe6826b7e094afb Mon Sep 17 00:00:00 2001 From: Will Lo <96078566+Will-ShaoHua@users.noreply.github.com> Date: Fri, 1 Nov 2024 12:45:07 -0700 Subject: [PATCH 3/5] fix supplemental context log latency always says 0ms (#5040) --- .../codewhisperer/util/CodeWhispererFileContextProvider.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererFileContextProvider.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererFileContextProvider.kt index 6fb17963d2b..8f476f999d7 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererFileContextProvider.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererFileContextProvider.kt @@ -144,9 +144,10 @@ class DefaultCodeWhispererFileContextProvider(private val project: Project) : Fi } return supplementalContext?.let { + val latency = System.currentTimeMillis() - startFetchingTimestamp if (it.contents.isNotEmpty()) { val logStr = buildString { - append("Successfully fetched supplemental context with strategy ${it.strategy} with ${it.latency} ms") + append("Successfully fetched supplemental context with strategy ${it.strategy} with $latency ms") it.contents.forEachIndexed { index, chunk -> append( """ @@ -166,8 +167,7 @@ class DefaultCodeWhispererFileContextProvider(private val project: Project) : Fi LOG.warn { "Failed to fetch supplemental context, empty list." } } - // TODO: fix this latency - it.copy(latency = System.currentTimeMillis() - startFetchingTimestamp) + it.copy(latency = latency) } } catch (e: TimeoutCancellationException) { LOG.debug { From 4381e6606926733e046195a672f16b918ea36a79 Mon Sep 17 00:00:00 2001 From: Thiago Verney Date: Mon, 4 Nov 2024 13:55:54 -0500 Subject: [PATCH 4/5] fix(dev): disable past messages --- .../InboundAppMessagesHandler.kt | 1 + .../controller/FeatureDevController.kt | 10 +++- .../FeatureDevControllerExtensions.kt | 8 ++- .../messages/FeatureDevMessage.kt | 2 +- .../FeatureDevMessagePublisherExtensions.kt | 9 ++++ .../amazonqFeatureDev/session/Session.kt | 2 +- .../util/FeatureDevControllerUtil.kt | 5 +- .../controller/FeatureDevControllerTest.kt | 14 +++--- .../ui/apps/featureDevChatConnector.ts | 49 ++++++++++++------- .../src/mynah-ui/ui/diffTree/actions.ts | 19 ++++--- 10 files changed, 78 insertions(+), 41 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/InboundAppMessagesHandler.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/InboundAppMessagesHandler.kt index 6983e7f4ec9..2c4d8cd0e16 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/InboundAppMessagesHandler.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/InboundAppMessagesHandler.kt @@ -6,6 +6,7 @@ package software.aws.toolkits.jetbrains.services.amazonqFeatureDev import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.IncomingFeatureDevMessage interface InboundAppMessagesHandler { + suspend fun processPromptChatMessage(message: IncomingFeatureDevMessage.ChatPrompt) suspend fun processNewTabCreatedMessage(message: IncomingFeatureDevMessage.NewTabCreated) suspend fun processTabRemovedMessage(message: IncomingFeatureDevMessage.TabRemoved) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt index 61d22801f67..3d43a66231d 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt @@ -273,6 +273,8 @@ class FeatureDevController( } messenger.updateFileComponent(message.tabId, filePaths, deletedFiles, messageId) + + //TODO: session.acceptCodeMessageId } private suspend fun newTabOpened(tabId: String) { @@ -353,6 +355,9 @@ class FeatureDevController( messenger ) + //TODO: if (session.acceptCodeMessageId) { + + messenger.sendAnswer( tabId = tabId, message = message("amazonqFeatureDev.code_generation.updated_code"), @@ -392,10 +397,11 @@ class FeatureDevController( } private suspend fun newTask(tabId: String, isException: Boolean? = false) { + this.disablePreviousFileList(tabId) + val session = getSessionInfo(tabId) val sessionLatency = System.currentTimeMillis() - session.sessionStartTime - this.disablePreviousFileList(tabId) AmazonqTelemetry.endChat( amazonqConversationId = session.conversationId, amazonqEndOfTheConversationLatency = sessionLatency.toDouble(), @@ -584,6 +590,8 @@ class FeatureDevController( message: String, ) { var session: Session? = null + + this.disablePreviousFileList(tabId) try { logger.debug { "$FEATURE_NAME: Processing message: $message" } session = getSessionInfo(tabId) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevControllerExtensions.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevControllerExtensions.kt index bee2a892822..c6992a0b20d 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevControllerExtensions.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevControllerExtensions.kt @@ -16,6 +16,8 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.sendC import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.sendCodeResult import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.sendSystemPrompt import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.sendUpdatePlaceholder +import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.updateChatAnswer +import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.updateFileComponent import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.CodeReferenceGenerated import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.DeletedFileInfo import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.NewFileZipInfo @@ -132,7 +134,11 @@ suspend fun FeatureDevController.onCodeGeneration( ) } - messenger.sendSystemPrompt(tabId = tabId, followUp = getFollowUpOptions(session.sessionState.phase)) + if (filePaths.any { it.rejected or it.changeApplied } or deletedFiles.any { it.rejected or it.changeApplied }) { + messenger.sendSystemPrompt(tabId = tabId, followUp = getFollowUpOptions(session.sessionState.phase, "Accept remaining changes")) + } else { + messenger.sendSystemPrompt(tabId = tabId, followUp = getFollowUpOptions(session.sessionState.phase, null)) + } messenger.sendUpdatePlaceholder(tabId = tabId, newPlaceholder = message("amazonqFeatureDev.placeholder.after_code_generation")) } finally { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessage.kt index c2c2d2326e4..ed475559b77 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessage.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessage.kt @@ -154,7 +154,7 @@ data class FileComponent( val filePaths: List, val deletedFiles: List, val messageId: String, - val disableFileActions: Boolean, + val disableFileActions: Boolean = false, ) : UiMessage( tabId = tabId, type = "updateFileComponent" diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessagePublisherExtensions.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessagePublisherExtensions.kt index 92afebf6d49..c8135675f4b 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessagePublisherExtensions.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessagePublisherExtensions.kt @@ -59,6 +59,15 @@ suspend fun MessagePublisher.sendSystemPrompt( ) } +suspend fun MessagePublisher.updateChatAnswer(tabId: String, messageId: String, message: String?, followUps: List) { + this.updateChatAnswer( + tabId, + messageId, + message, + followUps, + ) +} + suspend fun MessagePublisher.updateFileComponent( tabId: String, filePaths: List, diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt index 658279b3269..9b2414d7d98 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt @@ -36,7 +36,7 @@ class Session( private var task: String = "" private val proxyClient: FeatureDevClient private val featureDevService: FeatureDevService - private var _codeResultMessageId: String? = null + var _codeResultMessageId: String? = null // retry session state vars private var codegenRetries: Int diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/util/FeatureDevControllerUtil.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/util/FeatureDevControllerUtil.kt index d231106b928..b49289838cc 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/util/FeatureDevControllerUtil.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/util/FeatureDevControllerUtil.kt @@ -10,12 +10,13 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.Follo import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStatePhase import software.aws.toolkits.resources.message -fun getFollowUpOptions(phase: SessionStatePhase?): List { +fun getFollowUpOptions(phase: SessionStatePhase?, insertCodePillText: String?): List { when (phase) { SessionStatePhase.CODEGEN -> { + val messageText = insertCodePillText ?: message("amazonqFeatureDev.follow_up.insert_code") return listOf( FollowUp( - pillText = message("amazonqFeatureDev.follow_up.insert_code"), + pillText = messageText, type = FollowUpTypes.INSERT_CODE, icon = FollowUpIcons.Ok, status = FollowUpStatusType.Success diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevControllerTest.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevControllerTest.kt index 8992baaa5f1..95c70ed4a5d 100644 --- a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevControllerTest.kt +++ b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevControllerTest.kt @@ -81,13 +81,13 @@ class FeatureDevControllerTest : FeatureDevTestBase() { private val newFileContents = listOf( - NewFileZipInfo("test.ts", "This is a comment", false), - NewFileZipInfo("test2.ts", "This is a rejected file", true), + NewFileZipInfo("test.ts", "This is a comment", false, false), + NewFileZipInfo("test2.ts", "This is a rejected file", true, false), ) private val deletedFiles = listOf( - DeletedFileInfo("delete.ts", false), - DeletedFileInfo("delete2.ts", true), + DeletedFileInfo("delete.ts", false, false), + DeletedFileInfo("delete2.ts", true, false), ) @Before @@ -238,7 +238,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() { ), ) - doNothing().`when`(spySession).insertChanges(any(), any(), any()) + doNothing().`when`(spySession).insertChanges(any(), any(), any(), any()) spySession.preloader(userMessage, messenger) controller.processFollowupClickedMessage(message) @@ -246,7 +246,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() { mockitoVerify( spySession, times(1), - ).insertChanges(listOf(newFileContents[0]), listOf(deletedFiles[0]), testReferences) // insert changes for only non rejected files + ).insertChanges(listOf(newFileContents[0]), listOf(deletedFiles[0]), testReferences, messenger) // insert changes for only non rejected files coVerifyOrder { AmazonqTelemetry.isAcceptedCodeChanges( amazonqNumberOfFilesAccepted = 2.0, // it should be 2 files per test setup @@ -304,7 +304,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() { messenger.sendAnswer(testTabId, message("amazonqFeatureDev.chat_message.requesting_changes"), FeatureDevMessageType.AnswerStream) messenger.sendUpdatePlaceholder(testTabId, message("amazonqFeatureDev.placeholder.generating_code")) messenger.sendCodeResult(testTabId, testUploadId, newFileContents, deletedFiles, testReferences) - messenger.sendSystemPrompt(testTabId, getFollowUpOptions(SessionStatePhase.CODEGEN)) + messenger.sendSystemPrompt(testTabId, getFollowUpOptions(SessionStatePhase.CODEGEN, null)) messenger.sendUpdatePlaceholder(testTabId, message("amazonqFeatureDev.placeholder.after_code_generation")) messenger.sendAsyncEventProgress(testTabId, false) messenger.sendChatInputEnabledMessage(testTabId, false) diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/featureDevChatConnector.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/featureDevChatConnector.ts index 033701f63bc..ee688550fe2 100644 --- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/featureDevChatConnector.ts +++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/featureDevChatConnector.ts @@ -20,6 +20,7 @@ export interface ConnectorProps { onMessageReceived?: (tabID: string, messageData: any, needToShowAPIDocsTab: boolean) => void onAsyncEventProgress: (tabID: string, inProgress: boolean, message: string) => void onChatAnswerReceived?: (tabID: string, message: ChatItem) => void + onChatAnswerUpdated?: (tabID: string, message: ChatItem) => void sendFeedback?: (tabId: string, feedbackPayload: FeedbackPayload) => void | undefined onError: (tabID: string, message: string, title: string) => void onWarning: (tabID: string, message: string, title: string) => void @@ -36,6 +37,7 @@ export class Connector { private readonly onError private readonly onWarning private readonly onChatAnswerReceived + private readonly onChatAnswerUpdated private readonly onAsyncEventProgress private readonly updatePlaceholder private readonly chatInputEnabled @@ -47,6 +49,7 @@ export class Connector { constructor(props: ConnectorProps) { this.sendMessageToExtension = props.sendMessageToExtension this.onChatAnswerReceived = props.onChatAnswerReceived + this.onChatAnswerUpdated = props.onChatAnswerUpdated this.onWarning = props.onWarning this.onError = props.onError this.onAsyncEventProgress = props.onAsyncEventProgress @@ -130,24 +133,7 @@ export class Connector { private processChatMessage = async (messageData: any): Promise => { if (this.onChatAnswerReceived !== undefined) { - const answer: ChatItem = { - type: messageData.messageType, - body: messageData.message ?? undefined, - messageId: messageData.messageID ?? messageData.triggerID ?? '', - relatedContent: undefined, - canBeVoted: messageData.canBeVoted, - snapToTop: messageData.snapToTop, - followUp: - messageData.followUps !== undefined && messageData.followUps.length > 0 - ? { - text: - messageData.messageType === ChatItemType.SYSTEM_PROMPT - ? '' - : 'Please follow up with one of these', - options: messageData.followUps, - } - : undefined, - } + const answer: ChatItem = this.createAnswer(messageData) this.onChatAnswerReceived(messageData.tabID, answer) } } @@ -202,11 +188,38 @@ export class Connector { return } + private createAnswer = (messageData: any): ChatItem => { + return { + type: messageData.messageType, + body: messageData.message ?? undefined, + messageId: messageData.messageId ?? messageData.messageID ?? messageData.triggerID ?? '', + relatedContent: undefined, + canBeVoted: messageData.canBeVoted ?? undefined, + snapToTop: messageData.snapToTop ?? undefined, + followUp: + messageData.followUps !== undefined && Array.isArray(messageData.followUps) + ? { + text: + messageData.messageType === ChatItemType.SYSTEM_PROMPT || + messageData.followUps.length === 0 + ? '' + : 'Please follow up with one of these', + options: messageData.followUps, + } + : undefined, + } + } + handleMessageReceive = async (messageData: any): Promise => { if (messageData.type === 'updateFileComponent') { this.onFileComponentUpdate(messageData.tabID, messageData.filePaths, messageData.deletedFiles, messageData.messageId, messageData.disableFileActions) return } + if (messageData.type === 'updateChatAnswer') { + const answer = this.createAnswer(messageData) + this.onChatAnswerUpdated?.(messageData.tabID, answer) + return + } if (messageData.type === 'errorMessage') { this.onError(messageData.tabID, messageData.message, messageData.title) return diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/diffTree/actions.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/diffTree/actions.ts index 6604eef54b5..f0ce6a6a2c8 100644 --- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/diffTree/actions.ts +++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/diffTree/actions.ts @@ -10,13 +10,13 @@ export function getDetails(filePaths: DiffTreeFileInfo[]): Record Date: Mon, 4 Nov 2024 18:06:27 -0500 Subject: [PATCH 5/5] fix(dev): add messageId to disable previous iterations --- .../services/amazonqFeatureDev/messages/FeatureDevMessage.kt | 2 ++ .../messages/FeatureDevMessagePublisherExtensions.kt | 2 ++ .../jetbrains/services/amazonqFeatureDev/session/Session.kt | 2 +- .../mynah-ui/src/mynah-ui/ui/apps/featureDevChatConnector.ts | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessage.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessage.kt index ed475559b77..25eb2b009cb 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessage.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessage.kt @@ -26,6 +26,7 @@ sealed interface IncomingFeatureDevMessage : FeatureDevBaseMessage { data class StoreMessageIdMessage( @JsonProperty("tabID") val tabId: String, + val command: String, val messageId: String?, ) : IncomingFeatureDevMessage @@ -204,6 +205,7 @@ data class CodeResultMessage( val filePaths: List, val deletedFiles: List, val references: List, + val messageId: String? ) : UiMessage( tabId = tabId, type = "codeResultMessage" diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessagePublisherExtensions.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessagePublisherExtensions.kt index c8135675f4b..1e10671f830 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessagePublisherExtensions.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/messages/FeatureDevMessagePublisherExtensions.kt @@ -222,6 +222,7 @@ suspend fun MessagePublisher.sendCodeResult( deletedFiles: List, references: List, ) { + val messageId = UUID.randomUUID() val refs = references.map { ref -> CodeReference( @@ -244,6 +245,7 @@ suspend fun MessagePublisher.sendCodeResult( filePaths = filePaths, deletedFiles = deletedFiles, references = refs, + messageId = messageId.toString(), ), ) } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt index 9b2414d7d98..658279b3269 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt @@ -36,7 +36,7 @@ class Session( private var task: String = "" private val proxyClient: FeatureDevClient private val featureDevService: FeatureDevService - var _codeResultMessageId: String? = null + private var _codeResultMessageId: String? = null // retry session state vars private var codegenRetries: Int diff --git a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/featureDevChatConnector.ts b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/featureDevChatConnector.ts index ee688550fe2..bfef0844e0a 100644 --- a/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/featureDevChatConnector.ts +++ b/plugins/amazonq/mynah-ui/src/mynah-ui/ui/apps/featureDevChatConnector.ts @@ -140,7 +140,7 @@ export class Connector { private processCodeResultMessage = async (messageData: any): Promise => { if (this.onChatAnswerReceived !== undefined) { - const messageId = messageData.messageID ?? messageData.triggerID ?? messageData.conversationID + const messageId = messageData.messageId ?? messageData.messageID ?? messageData.triggerID ?? messageData.conversationID ?? messageData.codeGenerationId this.sendMessageToExtension({ command: 'store-code-result-message-id', tabID: messageData.tabID, messageId, tabType: 'featuredev' }) const actions = getActions([ ...messageData.filePaths,