From eb22f093a322ffb2149941a6cad0d3eade2a0c3e Mon Sep 17 00:00:00 2001 From: wilson Date: Mon, 18 Nov 2024 14:35:28 -0500 Subject: [PATCH 1/5] feat(feature dev): Add setting to allow Q to run code and test commands (#5077) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Q /dev team is launching a new feature: Allow Q to execute build/test based on customer's configuration 🎉🎉 We have made a couple tweaks to incorporate the user experience - Added a new setting to allow Q to run code and test commands per project - Added a pre-filled prompt option for customer to generate a configuration for this feature - Added a couple follow up buttons throughout the /dev flow and added logic around them - Updated copies - Updated `codefileExtensions` set --- ...-53e027cd-9c7f-4b09-bc4e-8ab2a20aba2a.json | 4 + .../amazonqFeatureDev/FeatureDevConstants.kt | 3 + .../controller/FeatureDevController.kt | 122 ++++++++++++++---- .../messages/FeatureDevMessage.kt | 3 + .../session/PrepareCodeGenerationState.kt | 6 +- .../amazonqFeatureDev/session/Session.kt | 14 +- .../FeatureDevSessionContextTest.kt | 2 +- .../controller/FeatureDevControllerTest.kt | 13 +- .../session/PrepareCodeGenerationStateTest.kt | 4 +- .../amazonqFeatureDev/session/SessionTest.kt | 2 +- .../settings/CodeWhispererConfigurable.kt | 23 ++++ .../settings/CodeWhispererSettings.kt | 11 ++ .../amazonq/FeatureDevSessionContext.kt | 25 +++- .../services/telemetry/TelemetryUtils.kt | 9 ++ .../toolkits/jetbrains/utils/DevFileUtils.kt | 8 ++ .../telemetry/OpenedFileTypeMetricsTest.kt | 2 +- .../resources/MessagesBundle.properties | 7 + 17 files changed, 211 insertions(+), 47 deletions(-) create mode 100644 .changes/next-release/feature-53e027cd-9c7f-4b09-bc4e-8ab2a20aba2a.json create mode 100644 plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/utils/DevFileUtils.kt diff --git a/.changes/next-release/feature-53e027cd-9c7f-4b09-bc4e-8ab2a20aba2a.json b/.changes/next-release/feature-53e027cd-9c7f-4b09-bc4e-8ab2a20aba2a.json new file mode 100644 index 00000000000..aff739da32f --- /dev/null +++ b/.changes/next-release/feature-53e027cd-9c7f-4b09-bc4e-8ab2a20aba2a.json @@ -0,0 +1,4 @@ +{ + "type" : "feature", + "description" : "Add setting to allow Q /dev to run code and test commands" +} \ No newline at end of file diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevConstants.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevConstants.kt index b8678aead68..c7c76f47385 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevConstants.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevConstants.kt @@ -7,6 +7,9 @@ const val FEATURE_EVALUATION_PRODUCT_NAME = "FeatureDev" const val FEATURE_NAME = "Amazon Q Developer Agent for software development" +@Suppress("MaxLineLength") +const val GENERATE_DEV_FILE_PROMPT = "generate a devfile in my repository. Note that you should only use devfile version 2.0.0 and the only supported command is test, so you should bundle all install, build and test commands in “test”. also you can use “public.ecr.aws/aws-mde/universal-image:latest” as universal image if you aren’t sure which image to use. here is an example for a node repository (but don't assume it's always a node project. look at the existing repository structure before generating the devfile): schemaVersion: 2.0.0 components: - name: dev container: image: public.ecr.aws/aws-mde/universal-image:latest commands: - id: test exec: component: dev commandLine: \"npm install && npm run build && npm run test\"" + // Max number of times a user can attempt to retry a code generation request if it fails const val CODE_GENERATION_RETRY_LIMIT = 3 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 9483b320bd2..58a079d3b6f 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 @@ -31,11 +31,13 @@ import software.aws.toolkits.jetbrains.core.coroutines.EDT import software.aws.toolkits.jetbrains.services.amazonq.RepoSizeError import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController +import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindowFactory import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.CodeIterationLimitException import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.DEFAULT_RETRY_LIMIT import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FEATURE_NAME import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FeatureDevException +import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.GENERATE_DEV_FILE_PROMPT import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.InboundAppMessagesHandler import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ModifySourceFolderErrorReason import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.MonthlyConversationLimitError @@ -75,6 +77,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.util.content import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.FeedbackComment import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService +import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings import software.aws.toolkits.jetbrains.ui.feedback.FeatureDevFeedbackDialog import software.aws.toolkits.jetbrains.utils.notifyError import software.aws.toolkits.resources.message @@ -133,6 +136,16 @@ class FeatureDevController( FollowUpTypes.PROVIDE_FEEDBACK_AND_REGENERATE_CODE -> provideFeedbackAndRegenerateCode(message.tabId) FollowUpTypes.NEW_TASK -> newTask(message.tabId) FollowUpTypes.CLOSE_SESSION -> closeSession(message.tabId) + FollowUpTypes.ACCEPT_AUTO_BUILD -> handleDevCommandUserSetting(message.tabId, true) + FollowUpTypes.DENY_AUTO_BUILD -> handleDevCommandUserSetting(message.tabId, false) + FollowUpTypes.GENERATE_DEV_FILE -> { + messenger.sendAnswer( + tabId = message.tabId, + messageType = FeatureDevMessageType.SystemPrompt, + message = message("amazonqFeatureDev.follow_up.generate_dev_file") + ) + newTask(tabId = message.tabId, prefilledPrompt = GENERATE_DEV_FILE_PROMPT) + } } } @@ -439,20 +452,38 @@ class FeatureDevController( canBeVoted = true ) - messenger.sendSystemPrompt( - tabId = tabId, - followUp = listOf( - FollowUp( - pillText = message("amazonqFeatureDev.follow_up.new_task"), - type = FollowUpTypes.NEW_TASK, - status = FollowUpStatusType.Info - ), + val followUps = mutableListOf( + FollowUp( + pillText = message("amazonqFeatureDev.follow_up.new_task"), + type = FollowUpTypes.NEW_TASK, + status = FollowUpStatusType.Info + ), + FollowUp( + pillText = message("amazonqFeatureDev.follow_up.close_session"), + type = FollowUpTypes.CLOSE_SESSION, + status = FollowUpStatusType.Info + ), + ) + + if (!session.context.checkForDevFile()) { + followUps.add( FollowUp( - pillText = message("amazonqFeatureDev.follow_up.close_session"), - type = FollowUpTypes.CLOSE_SESSION, + pillText = message("amazonqFeatureDev.follow_up.generate_dev_file"), + type = FollowUpTypes.GENERATE_DEV_FILE, status = FollowUpStatusType.Info ) ) + + messenger.sendAnswer( + tabId = tabId, + message = message("amazonqFeatureDev.chat_message.generate_dev_file"), + messageType = FeatureDevMessageType.Answer + ) + } + + messenger.sendSystemPrompt( + tabId = tabId, + followUp = followUps ) messenger.sendUpdatePlaceholder( @@ -470,9 +501,7 @@ class FeatureDevController( } } - private suspend fun newTask(tabId: String, isException: Boolean? = false) { - this.disablePreviousFileList(tabId) - + private suspend fun newTask(tabId: String, isException: Boolean? = false, prefilledPrompt: String? = null) { val session = getSessionInfo(tabId) val sessionLatency = System.currentTimeMillis() - session.sessionStartTime @@ -484,15 +513,30 @@ class FeatureDevController( chatSessionStorage.deleteSession(tabId) newTabOpened(tabId) - if (isException != null && !isException) { - messenger.sendAnswer( - tabId = tabId, - messageType = FeatureDevMessageType.Answer, - message = message("amazonqFeatureDev.chat_message.ask_for_new_task") - ) + + if (prefilledPrompt != null && isException != null && !isException) { + handleChat(tabId = tabId, message = prefilledPrompt) + } else { + if (isException != null && !isException) { + messenger.sendAnswer( + tabId = tabId, + messageType = FeatureDevMessageType.Answer, + message = message("amazonqFeatureDev.chat_message.ask_for_new_task") + ) + } + messenger.sendUpdatePlaceholder(tabId = tabId, newPlaceholder = message("amazonqFeatureDev.placeholder.new_plan")) + messenger.sendChatInputEnabledMessage(tabId = tabId, enabled = true) } - messenger.sendUpdatePlaceholder(tabId = tabId, newPlaceholder = message("amazonqFeatureDev.placeholder.new_plan")) - messenger.sendChatInputEnabledMessage(tabId = tabId, enabled = true) + } + + private suspend fun handleDevCommandUserSetting(tabId: String, value: Boolean) { + CodeWhispererSettings.getInstance().toggleAutoBuildFeature(context.project.basePath, value) + messenger.sendAnswer( + tabId = tabId, + message = message("amazonqFeatureDev.chat_message.setting_updated"), + messageType = FeatureDevMessageType.Answer, + ) + this.retryRequests(tabId) } private suspend fun closeSession(tabId: String) { @@ -669,6 +713,7 @@ class FeatureDevController( try { logger.debug { "$FEATURE_NAME: Processing message: $message" } session = getSessionInfo(tabId) + session.latestMessage = message val credentialState = authController.getAuthNeededStates(context.project).amazonQ if (credentialState != null) { @@ -681,7 +726,16 @@ class FeatureDevController( return } - session.preloader(message, messenger) + val codeWhispererSettings = CodeWhispererSettings.getInstance().getAutoBuildFeatureConfiguration() + val hasDevFile = session.context.checkForDevFile() + val isPromptedForAutoBuildFeature = codeWhispererSettings.containsKey(session.context.getWorkspaceRoot()) + + if (hasDevFile && !isPromptedForAutoBuildFeature) { + promptAllowQCommandsConsent(messenger, tabId) + return + } + + session.preloader(messenger) when (session.sessionState.phase) { SessionStatePhase.CODEGEN -> onCodeGeneration(session, message, tabId) @@ -695,6 +749,30 @@ class FeatureDevController( } } + private suspend fun promptAllowQCommandsConsent(messenger: MessagePublisher, tabID: String) { + messenger.sendAnswer( + tabId = tabID, + message = message("amazonqFeatureDev.chat_message.devFileInRepository"), + messageType = FeatureDevMessageType.Answer + ) + messenger.sendAnswer( + tabId = tabID, + messageType = FeatureDevMessageType.SystemPrompt, + followUp = listOf( + FollowUp( + pillText = message("amazonqFeatureDev.follow_up.accept_for_project"), + type = FollowUpTypes.ACCEPT_AUTO_BUILD, + status = FollowUpStatusType.Success + ), + FollowUp( + pillText = message("amazonqFeatureDev.follow_up.decline_for_project"), + type = FollowUpTypes.DENY_AUTO_BUILD, + status = FollowUpStatusType.Error + ) + ) + ) + } + private suspend fun retryRequests(tabId: String) { var session: Session? = null try { 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 be8be6d18a8..339a0c080ba 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 @@ -247,6 +247,9 @@ enum class FollowUpTypes( PROVIDE_FEEDBACK_AND_REGENERATE_CODE("ProvideFeedbackAndRegenerateCode"), NEW_TASK("NewTask"), CLOSE_SESSION("CloseSession"), + ACCEPT_AUTO_BUILD("AcceptAutoBuild"), + DENY_AUTO_BUILD("DenyAutoBuild"), + GENERATE_DEV_FILE("GenerateDevFile"), } // Util classes diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/PrepareCodeGenerationState.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/PrepareCodeGenerationState.kt index 5a397919ac5..c6b8bc5676b 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/PrepareCodeGenerationState.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/PrepareCodeGenerationState.kt @@ -13,6 +13,7 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.Cancellat import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.deleteUploadArtifact import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.uploadArtifactToS3 import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl +import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings import software.aws.toolkits.resources.message import software.aws.toolkits.telemetry.AmazonqTelemetry import software.aws.toolkits.telemetry.AmazonqUploadIntent @@ -47,7 +48,8 @@ class PrepareCodeGenerationState( messenger.sendAnswerPart(tabId = this.tabID, message = message("amazonqFeatureDev.chat_message.uploading_code")) messenger.sendUpdatePlaceholder(tabId = this.tabID, newPlaceholder = message("amazonqFeatureDev.chat_message.uploading_code")) - val repoZipResult = config.repoContext.getProjectZip() + val isAutoBuildFeatureEnabled = CodeWhispererSettings.getInstance().isAutoBuildFeatureEnabled(this.config.repoContext.getWorkspaceRoot()) + val repoZipResult = config.repoContext.getProjectZip(isAutoBuildFeatureEnabled = isAutoBuildFeatureEnabled) val zipFileChecksum = repoZipResult.checksum zipFileLength = repoZipResult.contentLength val fileToUpload = repoZipResult.payload @@ -94,7 +96,7 @@ class PrepareCodeGenerationState( credentialStartUrl = getStartUrl(config.featureDevService.project) ) } - // It is essential to interact with the next state outside of try-catch block for the telemetry to capture events for the states separately + // It is essential to interact with the next state outside try-catch block for the telemetry to capture events for the states separately return nextState.interact(action) } } 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 2a37e020eee..dae8150625d 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 @@ -55,9 +55,9 @@ 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(messenger: MessagePublisher) { if (!preloaderFinished) { - setupConversation(msg, messenger) + setupConversation(messenger) preloaderFinished = true messenger.sendAsyncEventProgress(tabId = this.tabID, inProgress = true) featureDevService.sendFeatureDevEvent(this.conversationId) @@ -67,10 +67,7 @@ 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) { - // Store the initial message when setting up the conversation so that if it fails we can retry with this message - _latestMessage = msg - + private fun setupConversation(messenger: MessagePublisher) { _conversationId = featureDevService.createConversation() logger().info(conversationIDLog(this.conversationId)) @@ -204,8 +201,11 @@ class Session(val tabID: String, val project: Project) { } } - val latestMessage: String + var latestMessage: String get() = this._latestMessage + set(value) { + this._latestMessage = value + } val retries: Int get() = codegenRetries diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt index 60d7b674c9e..a46cb908863 100644 --- a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt +++ b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt @@ -46,7 +46,7 @@ class FeatureDevSessionContextTest : FeatureDevTestBase() { @Test fun testWithInvalidFile() { val txtFile = mock() - whenever(txtFile.extension).thenReturn("txt") + whenever(txtFile.extension).thenReturn("mp4") assertFalse(featureDevSessionContext.isFileExtensionAllowed(txtFile)) } } 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 7b328b4e49f..9a5c7e08f10 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 @@ -160,7 +160,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() { every { AmazonqTelemetry.endChat(amazonqConversationId = any(), amazonqEndOfTheConversationLatency = any()) } just runs runTest { - spySession.preloader(userMessage, messenger) + spySession.preloader(messenger) controller.processFollowupClickedMessage(message) } @@ -189,7 +189,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() { mockkObject(AmazonqTelemetry) every { AmazonqTelemetry.isProvideFeedbackForCodeGen(amazonqConversationId = any(), enabled = any()) } just runs - spySession.preloader(userMessage, messenger) + spySession.preloader(messenger) controller.processFollowupClickedMessage(message) coVerifyOrder { @@ -241,7 +241,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() { doReturn(Unit).`when`(spySession).insertChanges(any(), any(), any(), any()) - spySession.preloader(userMessage, messenger) + spySession.preloader(messenger) controller.processFollowupClickedMessage(message) mockitoVerify( @@ -266,6 +266,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() { listOf( FollowUp(FollowUpTypes.NEW_TASK, message("amazonqFeatureDev.follow_up.new_task"), status = FollowUpStatusType.Info), FollowUp(FollowUpTypes.CLOSE_SESSION, message("amazonqFeatureDev.follow_up.close_session"), status = FollowUpStatusType.Info), + FollowUp(FollowUpTypes.GENERATE_DEV_FILE, message("amazonqFeatureDev.follow_up.generate_dev_file"), status = FollowUpStatusType.Info) ), ) messenger.sendUpdatePlaceholder(testTabId, message("amazonqFeatureDev.placeholder.additional_improvements")) @@ -565,7 +566,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() { mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt") every { selectFolder(any(), any()) } returns null - spySession.preloader(userMessage, messenger) + spySession.preloader(messenger) controller.processFollowupClickedMessage(message) coVerifyOrder { @@ -596,7 +597,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() { mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt") every { selectFolder(any(), any()) } returns LightVirtualFile("/path") - spySession.preloader(userMessage, messenger) + spySession.preloader(messenger) controller.processFollowupClickedMessage(message) coVerifyOrder { @@ -633,7 +634,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() { mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt") every { selectFolder(any(), any()) } returns folder - spySession.preloader(userMessage, messenger) + spySession.preloader(messenger) controller.processFollowupClickedMessage(message) coVerify { diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/PrepareCodeGenerationStateTest.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/PrepareCodeGenerationStateTest.kt index 49aa4d906e3..773c75548fd 100644 --- a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/PrepareCodeGenerationStateTest.kt +++ b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/PrepareCodeGenerationStateTest.kt @@ -87,7 +87,7 @@ class PrepareCodeGenerationStateTest : FeatureDevTestBase() { val repoZipResult = ZipCreationResult(mockFile, testChecksumSha, testContentLength) val action = SessionStateAction("test-task", userMessage) - whenever(repoContext.getProjectZip()).thenReturn(repoZipResult) + whenever(repoContext.getProjectZip(false)).thenReturn(repoZipResult) every { featureDevService.createUploadUrl(any(), any(), any(), any()) } returns exampleCreateUploadUrlResponse runTest { @@ -95,6 +95,6 @@ class PrepareCodeGenerationStateTest : FeatureDevTestBase() { assertThat(actual.nextState).isInstanceOf(PrepareCodeGenerationState::class.java) } assertThat(prepareCodeGenerationState.phase).isEqualTo(SessionStatePhase.CODEGEN) - verify(repoContext, times(1)).getProjectZip() + verify(repoContext, times(1)).getProjectZip(false) } } diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionTest.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionTest.kt index 070abddb978..550058231c0 100644 --- a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionTest.kt +++ b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionTest.kt @@ -58,7 +58,7 @@ class SessionTest : FeatureDevTestBase() { fun `test preloader`() = runTest { whenever(featureDevClient.createTaskAssistConversation()).thenReturn(exampleCreateTaskAssistConversationResponse) - session.preloader(userMessage, messenger) + session.preloader(messenger) assertThat(session.conversationId).isEqualTo(testConversationId) assertThat(session.sessionState).isInstanceOf(PrepareCodeGenerationState::class.java) verify(featureDevClient, times(1)).createTaskAssistConversation() diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt index 8aaf55a7171..8951d04aed0 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt @@ -113,6 +113,29 @@ class CodeWhispererConfigurable(private val project: Project) : } } + group(message("aws.settings.codewhisperer.allow_q_dev_build_test_commands")) { + row { + val settings = codeWhispererSettings.getAutoBuildFeatureConfiguration() + for ((key) in settings) { + checkBox(key).apply { + connect.subscribe( + ToolkitConnectionManagerListener.TOPIC, + object : ToolkitConnectionManagerListener { + override fun activeConnectionChanged(newConnection: ToolkitConnection?) { + enabled(isCodeWhispererEnabled(project)) + } + } + ) + + bindSelected( + getter = { codeWhispererSettings.isAutoBuildFeatureEnabled(key) }, + setter = { newValue -> codeWhispererSettings.toggleAutoBuildFeature(key, newValue) } + ) + } + } + } + } + group(message("aws.settings.codewhisperer.group.q_chat")) { row { checkBox(message("aws.settings.codewhisperer.project_context")).apply { diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/CodeWhispererSettings.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/CodeWhispererSettings.kt index 2176259dd29..4916d241f55 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/CodeWhispererSettings.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/CodeWhispererSettings.kt @@ -26,6 +26,16 @@ class CodeWhispererSettings : PersistentStateComponent() val intValue by map() + val projectAutoBuildConfigurationMap by map() } enum class CodeWhispererConfigurationType { diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt index 4527e819f5d..ee3a10bf700 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt @@ -21,6 +21,7 @@ import software.aws.toolkits.core.utils.putNextEntry import software.aws.toolkits.jetbrains.core.coroutines.EDT import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext import software.aws.toolkits.jetbrains.services.telemetry.ALLOWED_CODE_EXTENSIONS +import software.aws.toolkits.jetbrains.utils.isDevFile import software.aws.toolkits.resources.AwsCoreBundle import software.aws.toolkits.telemetry.AmazonqTelemetry import java.io.File @@ -47,7 +48,6 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo "\\.hg/?", "\\.rvm", "\\.git/?", - "\\.gitignore", "\\.project", "\\.gem", "/\\.idea/?", @@ -71,7 +71,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo // projectRoot: is the directory where the project is located when selected to open a project. val projectRoot = project.guessProjectDir() ?: error("Cannot guess base directory for project ${project.name}") - // selectedSourceFolder": is the directory selected in replacement of the root, this happens when the project is too big to bundle for uploading. + // selectedSourceFolder: is the directory selected in replacement of the root, this happens when the project is too big to bundle for uploading. private var _selectedSourceFolder = projectRoot private var ignorePatternsWithGitIgnore = emptyList() private val gitIgnoreFile = File(selectedSourceFolder.path, ".gitignore") @@ -80,10 +80,18 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo ignorePatternsWithGitIgnore = (ignorePatterns + parseGitIgnore().map { Regex(it) }).toList() } - fun getProjectZip(): ZipCreationResult { + // This function checks for existence of `devfile.yaml` in customer's repository, currently only `devfile.yaml` is supported for this feature. + fun checkForDevFile(): Boolean { + val devFile = File(projectRoot.path, "/devfile.yaml") + return devFile.exists() + } + + fun getWorkspaceRoot(): String = projectRoot.path + + fun getProjectZip(isAutoBuildFeatureEnabled: Boolean?): ZipCreationResult { val zippedProject = runBlocking { withBackgroundProgress(project, AwsCoreBundle.message("amazonqFeatureDev.placeholder.generating_code")) { - zipFiles(selectedSourceFolder) + zipFiles(selectedSourceFolder, isAutoBuildFeatureEnabled) } } val checkSum256: String = Base64.getEncoder().encodeToString(DigestUtils.sha256(FileInputStream(zippedProject))) @@ -117,7 +125,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo return deferredResults.any { it.await() } } - suspend fun zipFiles(projectRoot: VirtualFile): File = withContext(getCoroutineBgContext()) { + suspend fun zipFiles(projectRoot: VirtualFile, isAutoBuildFeatureEnabled: Boolean?): File = withContext(getCoroutineBgContext()) { val files = mutableListOf() val ignoredExtensionMap = mutableMapOf().withDefault { 0L } var totalSize: Long = 0 @@ -127,11 +135,18 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo object : VirtualFileVisitor() { override fun visitFile(file: VirtualFile): Boolean { val isFileIgnoredByExtension = runBlocking { ignoreFileByExtension(file) } + // if `isAutoBuildFeatureEnabled` is false, then filter devfile + val isFilterDevFile = if (isAutoBuildFeatureEnabled == true) false else isDevFile(file) if (isFileIgnoredByExtension) { val extension = file.extension.orEmpty() ignoredExtensionMap[extension] = (ignoredExtensionMap[extension] ?: 0) + 1 return false } + + if (isFilterDevFile) { + return false + } + val isFileIgnoredByPattern = runBlocking { ignoreFile(file.name) } if (isFileIgnoredByPattern) { return false diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/TelemetryUtils.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/TelemetryUtils.kt index 0d73d8fd435..f94ac462c0f 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/TelemetryUtils.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/TelemetryUtils.kt @@ -20,6 +20,7 @@ val ALLOWED_CODE_EXTENSIONS = setOf( "cbl", "cc", "cfc", + "cfg", "cfm", "cjs", "clj", @@ -30,6 +31,7 @@ val ALLOWED_CODE_EXTENSIONS = setOf( "cob", "cobra", "coffee", + "config", "cpp", "cpy", "cr", @@ -44,6 +46,7 @@ val ALLOWED_CODE_EXTENSIONS = setOf( "e", "el", "elm", + "env", "erl", "ex", "exs", @@ -59,6 +62,7 @@ val ALLOWED_CODE_EXTENSIONS = setOf( "fsi", "fsx", "gd", + "gitignore", "go", "gql", "graphql", @@ -78,6 +82,7 @@ val ALLOWED_CODE_EXTENSIONS = setOf( "html", "hy", "idl", + "ini", "io", "jar", "java", @@ -91,6 +96,7 @@ val ALLOWED_CODE_EXTENSIONS = setOf( "lgt", "lhs", "lisp", + "lock", "logtalk", "lsp", "lua", @@ -175,14 +181,17 @@ val ALLOWED_CODE_EXTENSIONS = setOf( "ss", "st", "sv", + "svg", "swift", "t", "tcl", "tf", + "toml", "trigger", "ts", "tsx", "tu", + "txt", "v", "vala", "vapi", diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/utils/DevFileUtils.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/utils/DevFileUtils.kt new file mode 100644 index 00000000000..9218620edb7 --- /dev/null +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/utils/DevFileUtils.kt @@ -0,0 +1,8 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.utils + +import com.intellij.openapi.vfs.VirtualFile + +fun isDevFile(file: VirtualFile): Boolean = file.name.matches(Regex("devfile\\.ya?ml", RegexOption.IGNORE_CASE)) diff --git a/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/telemetry/OpenedFileTypeMetricsTest.kt b/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/telemetry/OpenedFileTypeMetricsTest.kt index c5afffb3c28..01ff16f5c6e 100644 --- a/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/telemetry/OpenedFileTypeMetricsTest.kt +++ b/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/telemetry/OpenedFileTypeMetricsTest.kt @@ -30,7 +30,7 @@ class OpenedFileTypeMetricsTest { @Test fun `test addToExistingTelemetryBatch with disallowed extension`() { - service.addToExistingTelemetryBatch("txt") + service.addToExistingTelemetryBatch("mp4") assertThat(service.getOpenedFileTypes()).isEmpty() } } diff --git a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties index c4cbf4cd2f2..657ba754ef9 100644 --- a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties +++ b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties @@ -42,7 +42,10 @@ action.q.openchat.text=Open Chat Panel amazonqChat.project_context.index_in_progress=By the way, I'm still indexing this project for full context from your workspace. I may have a better response in a few minutes when it's complete if you'd like to try again then. amazonqFeatureDev.chat_message.ask_for_new_task=What new task would you like to work on? amazonqFeatureDev.chat_message.closed_session=Okay, I've ended this chat session. You can open a new tab to chat or start another workflow. +amazonqFeatureDev.chat_message.devFileInRepository=I noticed that your repository has a `devfile.yaml`. Would you like me to use the devfile to build and test your project as I generate code?\n\nFor more information on using devfiles to improve code generation, see the [Amazon Q Developer documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/software-dev.html). +amazonqFeatureDev.chat_message.generate_dev_file=For future tasks in this project, I can create a devfile to build and test code as I generate it. This can improve the quality of generated code. To allow me to create a devfile, choose **Generate devfile to build code**. amazonqFeatureDev.chat_message.requesting_changes=Requesting changes ... +amazonqFeatureDev.chat_message.setting_updated=I've updated your settings so I can run code and test commands based on your devfile for this project. You can update this setting under **Amazon Q: Allow Q /dev to run code and test commands**. amazonqFeatureDev.chat_message.start_code_generation=Okay, I'll generate code for that. This might take a few minutes.\n\nYou can navigate away from this chat, but please keep this tab open. I'll notify you when I'm done. amazonqFeatureDev.chat_message.start_code_generation_retry=Okay, I'll generate new code. This might take a few minutes.\n\nYou can navigate away from this chat, but please keep this tab open. I'll notify you when I'm done. amazonqFeatureDev.chat_message.uploading_code=Uploading code... @@ -79,8 +82,11 @@ amazonqFeatureDev.exception.throttling=I'm sorry, I'm experiencing high demand a amazonqFeatureDev.exception.upload_code=I'm sorry, I couldn't upload your workspace artifacts to Amazon S3 to help you with this task. You might need to allow access to the S3 bucket. For more information, see the [Amazon Q documentation](https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/security_iam_manage-access-with-policies.html#data-perimeters) or contact your network or organization administrator. amazonqFeatureDev.exception.upload_url_expiry=I'm sorry, I wasn't able to generate code. A connection timed out or became unavailable. Please try again or check the following:\n\n- Exclude non-essential files in your workspace's `.gitignore`.\n\n- Check that your network connection is stable. amazonqFeatureDev.follow_instructions_for_authentication=Follow instructions to re-authenticate ... +amazonqFeatureDev.follow_up.accept_for_project=Yes, use my devfile for this project amazonqFeatureDev.follow_up.close_session=No, thanks amazonqFeatureDev.follow_up.continue=Continue +amazonqFeatureDev.follow_up.decline_for_project=No, thanks +amazonqFeatureDev.follow_up.generate_dev_file=Generate devfile to build code amazonqFeatureDev.follow_up.incorrect_source_folder=The folder you chose isn't in your open workspace folder. You can add this folder to your workspace, or choose a folder in your open workspace. amazonqFeatureDev.follow_up.insert_all_code=Accept all changes amazonqFeatureDev.follow_up.insert_remaining_code=Accept remaining changes @@ -221,6 +227,7 @@ aws.settings.auto_update.notification_enable.tooltip=If unchecked, updates will aws.settings.auto_update.progress.message=Updating AWS plugins aws.settings.auto_update.text=Automatically install plugin updates when available aws.settings.aws_cli_settings=AWS CLI Settings +aws.settings.codewhisperer.allow_q_dev_build_test_commands=Amazon Q: Allow Q /dev to run code and test commands aws.settings.codewhisperer.automatic_import_adder=Imports recommendation aws.settings.codewhisperer.automatic_import_adder.tooltip=Amazon Q will add import statements with code suggestions when necessary aws.settings.codewhisperer.configurable.controlled_by_admin=\ Controlled by your admin From 232a273120a1843db9c3f4871732f00dd050b7a5 Mon Sep 17 00:00:00 2001 From: Karan Ahluwalia Date: Fri, 24 Jan 2025 13:30:27 -0800 Subject: [PATCH 2/5] Update file generation logic and file upload functionality for dev-execution. (#5295) #### Parameter Update for Testing: - Updated function parameters to address testing issues introduced by changes made in PR #5077. #### Enhanced Development Prompt: - Modified GENERATE_DEV_FILE_PROMPT to support running install, build, and test commands independently. #### Improved Build and Code File Upload Logic: - Updated the logic to enable uploading build files alongside code files. --- .../session/PrepareDocGenerationState.kt | 2 +- .../amazonqFeatureDev/FeatureDevConstants.kt | 2 +- .../controller/FeatureDevController.kt | 2 +- .../FeatureDevSessionContextTest.kt | 14 ++++++++------ .../codescan/CodeWhispererProjectCodeScanTest.kt | 16 ++++++++++------ .../services/amazonq/FeatureDevSessionContext.kt | 5 ++++- 6 files changed, 25 insertions(+), 16 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/session/PrepareDocGenerationState.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/session/PrepareDocGenerationState.kt index f9720cd4ce2..497126bb12d 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/session/PrepareDocGenerationState.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqDoc/session/PrepareDocGenerationState.kt @@ -39,7 +39,7 @@ class PrepareDocGenerationState( var zipFileLength: Long? = null val nextState: SessionState try { - val repoZipResult = config.repoContext.getProjectZip() + val repoZipResult = config.repoContext.getProjectZip(false) val zipFileChecksum = repoZipResult.checksum zipFileLength = repoZipResult.contentLength val fileToUpload = repoZipResult.payload diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevConstants.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevConstants.kt index 8c4312fccb4..658cdc2ed58 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevConstants.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevConstants.kt @@ -8,7 +8,7 @@ const val FEATURE_EVALUATION_PRODUCT_NAME = "FeatureDev" const val FEATURE_NAME = "Amazon Q Developer Agent for software development" @Suppress("MaxLineLength") -const val GENERATE_DEV_FILE_PROMPT = "generate a devfile in my repository. Note that you should only use devfile version 2.0.0 and the only supported command is test, so you should bundle all install, build and test commands in “test”. also you can use “public.ecr.aws/aws-mde/universal-image:latest” as universal image if you aren’t sure which image to use. here is an example for a node repository (but don't assume it's always a node project. look at the existing repository structure before generating the devfile): schemaVersion: 2.0.0 components: - name: dev container: image: public.ecr.aws/aws-mde/universal-image:latest commands: - id: test exec: component: dev commandLine: \"npm install && npm run build && npm run test\"" +const val GENERATE_DEV_FILE_PROMPT = "generate a devfile in my repository. Note that you should only use devfile version 2.0.0 and the only supported commands are install, build and test (are all optional). so you may have to bundle some commands together using '&&'. also you can use \"public.ecr.aws/aws-mde/universal-image:latest\" as universal image if you aren’t sure which image to use. here is an example for a node repository (but don't assume it's always a node project. look at the existing repository structure before generating the devfile): schemaVersion: 2.0.0 components: - name: dev container: image: public.ecr.aws/aws-mde/universal-image:latest commands: - id: install exec: component: dev commandLine: \"npm install\" - id: build exec: component: dev commandLine: \"npm run build\" - id: test exec: component: dev commandLine: \"npm run test\"" // Max number of times a user can attempt to retry a code generation request if it fails const val CODE_GENERATION_RETRY_LIMIT = 3 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 4bc86d89543..d13f03e7fce 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 @@ -736,7 +736,7 @@ class FeatureDevController( return } - session.preloader(message, messenger) + session.preloader(messenger) broadcastQEvent(QFeatureEvent.INVOCATION) when (session.sessionState.phase) { diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt index 136c4d3e433..dfebd35097c 100644 --- a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt +++ b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt @@ -55,10 +55,9 @@ class FeatureDevSessionContextTest : FeatureDevTestBase(HeavyJavaCodeInsightTest @Test fun testWithInvalidFile() { - val txtFile = mock() - whenever(txtFile.extension).thenReturn("txt") - whenever(txtFile.path).thenReturn("file.txt") - assertFalse(featureDevSessionContext.isFileExtensionAllowed(txtFile)) + val mediaFile = mock() + whenever(mediaFile.extension).thenReturn("mp4") + assertFalse(featureDevSessionContext.isFileExtensionAllowed(mediaFile)) } @Test @@ -96,10 +95,11 @@ class FeatureDevSessionContextTest : FeatureDevTestBase(HeavyJavaCodeInsightTest "License.md", "node_modules/express", "build/outputs", - "dist/bundle.js" + "dist/bundle.js", + "gradle/wrapper/gradle-wrapper.jar", ) - val zipResult = featureDevSessionContext.getProjectZip() + val zipResult = featureDevSessionContext.getProjectZip(false) val zipPath = zipResult.payload.path val zippedFiles = mutableSetOf() @@ -120,6 +120,8 @@ class FeatureDevSessionContextTest : FeatureDevTestBase(HeavyJavaCodeInsightTest "builder/GetTestBuilder.java", "settings.gradle", "build.gradle", + "gradle/wrapper/gradle-wrapper.jar", + ".gitignore", ) assertTrue(zippedFiles == expectedFiles) diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererProjectCodeScanTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererProjectCodeScanTest.kt index 47d2c5ff6e9..4bf42a83a77 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererProjectCodeScanTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/codescan/CodeWhispererProjectCodeScanTest.kt @@ -34,6 +34,7 @@ class CodeWhispererProjectCodeScanTest : CodeWhispererCodeScanTestBase(PythonCod private lateinit var testYaml: VirtualFile private lateinit var helperPy: VirtualFile private lateinit var testTf: VirtualFile + private lateinit var gitIgnore: VirtualFile private lateinit var sessionConfigSpy: CodeScanSessionConfig private lateinit var sessionConfigSpy2: CodeScanSessionConfig @@ -63,9 +64,9 @@ class CodeWhispererProjectCodeScanTest : CodeWhispererCodeScanTestBase(PythonCod fun `test createPayload`() { val payload = sessionConfigSpy.createPayload() assertNotNull(payload) - assertThat(payload.context.totalFiles).isEqualTo(10) + assertThat(payload.context.totalFiles).isEqualTo(11) - assertThat(payload.context.scannedFiles.size).isEqualTo(10) + assertThat(payload.context.scannedFiles.size).isEqualTo(11) assertThat(payload.context.scannedFiles).contains(testYaml, testTf, readMeMd, utilsJs, utilsCs, testJson, testCs, helperPy, helperCs, helpGo) assertThat(payload.context.srcPayloadSize).isEqualTo(totalSize) @@ -80,12 +81,12 @@ class CodeWhispererProjectCodeScanTest : CodeWhispererCodeScanTestBase(PythonCod filesInZip += 1 } - assertThat(filesInZip).isEqualTo(10) + assertThat(filesInZip).isEqualTo(11) } @Test fun `getProjectPayloadMetadata()`() { - getProjectPayloadMetadata(sessionConfigSpy, 10, totalSize, this.totalLines, CodewhispererLanguage.Csharp) + getProjectPayloadMetadata(sessionConfigSpy, 11, totalSize, this.totalLines, CodewhispererLanguage.Csharp) } @Test @@ -105,7 +106,7 @@ class CodeWhispererProjectCodeScanTest : CodeWhispererCodeScanTestBase(PythonCod @Test fun `e2e happy path integration test`() = runTest { - assertE2ERunsSuccessfully(sessionConfigSpy, project, totalLines, 10, totalSize, 1) + assertE2ERunsSuccessfully(sessionConfigSpy, project, totalLines, 11, totalSize, 1) } private fun setupCsharpProject() { @@ -371,7 +372,10 @@ class CodeWhispererProjectCodeScanTest : CodeWhispererCodeScanTestBase(PythonCod // Adding gitignore file and gitignore file member for testing. // The tests include the markdown file but not these two files. - projectRule.fixture.addFileToProject("/.gitignore", "node_modules\n.idea\n.vscode\n.DS_Store").virtualFile + gitIgnore = projectRule.fixture.addFileToProject("/.gitignore", "node_modules\n.idea\n.vscode\n.DS_Store").virtualFile + totalSize += gitIgnore.length + totalLines += gitIgnore.toNioPath().toFile().readLines().size + projectRule.fixture.addFileToProject("/.idea/ref", "ref: refs/heads/main") } } diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt index 2a7cc5ceece..127c524bee6 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt @@ -47,11 +47,11 @@ class RepoSizeLimitError(override val message: String) : RuntimeException(), Rep class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Long? = null) { // TODO: Need to correct this class location in the modules going further to support both amazonq and codescan. + private val requiredFilesForExecution = setOf("gradle/wrapper/gradle-wrapper.jar") private val additionalGitIgnoreRules = setOf( ".aws-sam", ".gem", ".git", - ".gitignore", ".gradle", ".hg", ".idea", @@ -136,6 +136,9 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo suspend fun ignoreFile(file: VirtualFile): Boolean = ignoreFile(file.presentableUrl) suspend fun ignoreFile(path: String): Boolean { + if (requiredFilesForExecution.any { path.endsWith(it) }) { + return false + } // this method reads like something a JS dev would write and doesn't do what the author thinks val deferredResults = ignorePatternsWithGitIgnore.map { pattern -> withContext(coroutineContext) { From ec810100f0d6576a1eec1e29c27bc7a1e599c208 Mon Sep 17 00:00:00 2001 From: Hamed Soleimani Date: Mon, 27 Jan 2025 15:57:32 -0800 Subject: [PATCH 3/5] feat: Allow including binary files when auto build is enabled (#5299) * feat: Allow including binary files when auto build is enabled * attempt fixing test failures on windows * fix windows tests --- .../FeatureDevSessionContextTest.kt | 67 ++++++++++--- .../amazonq/FeatureDevSessionContext.kt | 97 ++++++++++--------- .../jetbrains/services/amazonq/QConstants.kt | 1 + .../services/telemetry/TelemetryUtils.kt | 1 - 4 files changed, 104 insertions(+), 62 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt index dfebd35097c..f77d0ce75b7 100644 --- a/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt +++ b/plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevSessionContextTest.kt @@ -3,6 +3,7 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.testFramework.RuleChain +import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before @@ -72,7 +73,51 @@ class FeatureDevSessionContextTest : FeatureDevTestBase(HeavyJavaCodeInsightTest } @Test - fun testZipProject() { + fun testZipProjectWithoutAutoDev() { + checkZipProject( + false, + setOf( + "src/MyClass.java", + "gradlew", + "gradlew.bat", + "README.md", + "gradle/wrapper/gradle-wrapper.properties", + "builder/GetTestBuilder.java", + "settings.gradle", + "build.gradle", + ".gitignore", + ) + ) + } + + @Test + fun testZipProjectWithAutoDev() { + checkZipProject( + true, + setOf( + "src/MyClass.java", + "icons/menu.svg", + "assets/header.jpg", + "gradle/wrapper/gradle-wrapper.jar", + "gradle/wrapper/gradle-wrapper.properties", + "images/logo.png", + "builder/GetTestBuilder.java", + "gradlew", + "README.md", + ".gitignore", + "License.md", + "output.bin", + "archive.zip", + "gradlew.bat", + "license.txt", + "build.gradle", + "devfile.yaml", + "settings.gradle" + ) + ) + } + + fun checkZipProject(autoBuildEnabled: Boolean, expectedFiles: Set) { addFilesToProjectModule( ".gitignore", ".gradle/cached.jar", @@ -97,9 +142,12 @@ class FeatureDevSessionContextTest : FeatureDevTestBase(HeavyJavaCodeInsightTest "build/outputs", "dist/bundle.js", "gradle/wrapper/gradle-wrapper.jar", + "devfile.yaml", ) - val zipResult = featureDevSessionContext.getProjectZip(false) + projectRule.fixture.addFileToModule(module, "large-file.txt", "loblob".repeat(1024 * 1024)) + + val zipResult = featureDevSessionContext.getProjectZip(autoBuildEnabled) val zipPath = zipResult.payload.path val zippedFiles = mutableSetOf() @@ -111,19 +159,6 @@ class FeatureDevSessionContextTest : FeatureDevTestBase(HeavyJavaCodeInsightTest } } - val expectedFiles = setOf( - "src/MyClass.java", - "gradlew", - "gradlew.bat", - "README.md", - "gradle/wrapper/gradle-wrapper.properties", - "builder/GetTestBuilder.java", - "settings.gradle", - "build.gradle", - "gradle/wrapper/gradle-wrapper.jar", - ".gitignore", - ) - - assertTrue(zippedFiles == expectedFiles) + assertEquals(zippedFiles, expectedFiles) } } diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt index 127c524bee6..781d3dd2581 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt @@ -10,7 +10,6 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileVisitor import com.intellij.openapi.vfs.isFile import com.intellij.platform.ide.progress.withBackgroundProgress -import kotlinx.coroutines.async import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -18,6 +17,7 @@ import kotlinx.coroutines.withContext import org.apache.commons.codec.digest.DigestUtils import org.apache.commons.io.FileUtils import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext +import software.aws.toolkits.jetbrains.services.amazonq.QConstants.MAX_FILE_SIZE_BYTES import software.aws.toolkits.jetbrains.services.telemetry.ALLOWED_CODE_EXTENSIONS import software.aws.toolkits.jetbrains.utils.isDevFile import software.aws.toolkits.resources.AwsCoreBundle @@ -33,7 +33,6 @@ import java.nio.file.Paths import java.nio.file.StandardCopyOption import java.util.Base64 import java.util.UUID -import kotlin.coroutines.coroutineContext import kotlin.io.path.Path import kotlin.io.path.createParentDirectories import kotlin.io.path.getPosixFilePermissions @@ -47,8 +46,7 @@ class RepoSizeLimitError(override val message: String) : RuntimeException(), Rep class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Long? = null) { // TODO: Need to correct this class location in the modules going further to support both amazonq and codescan. - private val requiredFilesForExecution = setOf("gradle/wrapper/gradle-wrapper.jar") - private val additionalGitIgnoreRules = setOf( + private val additionalGitIgnoreFolderRules = setOf( ".aws-sam", ".gem", ".git", @@ -58,6 +56,12 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo ".project", ".rvm", ".svn", + "node_modules", + "build", + "dist", + ) + + private val additionalGitIgnoreBinaryFilesRules = setOf( "*.zip", "*.bin", "*.png", @@ -70,9 +74,6 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo "license.md", "License.md", "LICENSE.md", - "node_modules", - "build", - "dist" ) // well known source files that do not have extensions @@ -90,12 +91,17 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo // selectedSourceFolder: is the directory selected in replacement of the root, this happens when the project is too big to bundle for uploading. private var _selectedSourceFolder = projectRoot private var ignorePatternsWithGitIgnore = emptyList() + private var ignorePatternsForBinaryFiles = additionalGitIgnoreBinaryFilesRules + .map { convertGitIgnorePatternToRegex(it) } + .mapNotNull { pattern -> + runCatching { Regex(pattern) }.getOrNull() + } private val gitIgnoreFile = File(selectedSourceFolder.path, ".gitignore") init { ignorePatternsWithGitIgnore = try { buildList { - addAll(additionalGitIgnoreRules.map { convertGitIgnorePatternToRegex(it) }) + addAll(additionalGitIgnoreFolderRules.map { convertGitIgnorePatternToRegex(it) }) addAll(parseGitIgnore()) }.mapNotNull { pattern -> runCatching { Regex(pattern) }.getOrNull() @@ -130,35 +136,49 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo return ALLOWED_CODE_EXTENSIONS.contains(extension) } - private fun ignoreFileByExtension(file: VirtualFile) = - !isFileExtensionAllowed(file) + fun ignoreFile(file: VirtualFile, applyExtraBinaryFilesRules: Boolean = true): Boolean = ignoreFile(file.presentableUrl, applyExtraBinaryFilesRules) + + fun ignoreFile(path: String, applyExtraBinaryFilesRules: Boolean = true): Boolean { + val allIgnoreRules = if (applyExtraBinaryFilesRules) ignorePatternsWithGitIgnore + ignorePatternsForBinaryFiles else ignorePatternsWithGitIgnore + val matchedRules = allIgnoreRules.map { pattern -> + // avoid partial match (pattern.containsMatchIn) since it causes us matching files + // against folder patterns. (e.g. settings.gradle ignored by .gradle rule!) + // we convert the glob rules to regex, add a trailing /* to all rules and then match + // entries against them by adding a trailing /. + // TODO: Add unit tests for gitignore matching + val relative = if (path.startsWith(projectRootPath.toString())) Paths.get(path).relativeTo(projectRootPath) else path + pattern.matches("$relative/") + } + return matchedRules.any { it } + } - suspend fun ignoreFile(file: VirtualFile): Boolean = ignoreFile(file.presentableUrl) + private fun wellKnown(file: VirtualFile): Boolean = wellKnownSourceFiles.contains(file.name) - suspend fun ignoreFile(path: String): Boolean { - if (requiredFilesForExecution.any { path.endsWith(it) }) { + private fun shouldIncludeInZipFile(file: VirtualFile, isAutoBuildFeatureEnabled: Boolean): Boolean { + // large files always ignored + if (file.length > MAX_FILE_SIZE_BYTES) { return false } - // this method reads like something a JS dev would write and doesn't do what the author thinks - val deferredResults = ignorePatternsWithGitIgnore.map { pattern -> - withContext(coroutineContext) { - // avoid partial match (pattern.containsMatchIn) since it causes us matching files - // against folder patterns. (e.g. settings.gradle ignored by .gradle rule!) - // we convert the glob rules to regex, add a trailing /* to all rules and then match - // entries against them by adding a trailing /. - // TODO: Add unit tests for gitignore matching - val relative = if (path.startsWith(projectRootPath.toString())) Paths.get(path).relativeTo(projectRootPath) else path - async { pattern.matches("$relative/") } - } + + // always respect gitignore rules and remove binary files if auto build is disabled + val isFileIgnoredByPattern = ignoreFile(file, !isAutoBuildFeatureEnabled) + if (isFileIgnoredByPattern) { + return false } - // this will serially iterate over and block - // ideally we race the results https://github.com/Kotlin/kotlinx.coroutines/issues/2867 - // i.e. Promise.any(...) - return deferredResults.any { it.await() } - } + // all other files are included when auto build enabled + if (isAutoBuildFeatureEnabled) { + return true + } + + // when auto build is disabled, only include files with well known extensions and names except "devfile.yam" + if (!isDevFile(file) && (wellKnown(file) || isFileExtensionAllowed(file))) { + return true + } - fun wellKnown(file: VirtualFile): Boolean = wellKnownSourceFiles.contains(file.name) + // Any other files should not be included + return false + } suspend fun zipFiles(projectRoot: VirtualFile, isAutoBuildFeatureEnabled: Boolean?): File = withContext(getCoroutineBgContext()) { val files = mutableListOf() @@ -169,26 +189,13 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo projectRoot, object : VirtualFileVisitor() { override fun visitFile(file: VirtualFile): Boolean { - val isWellKnown = runBlocking { wellKnown(file) } - val isFileIgnoredByExtension = runBlocking { ignoreFileByExtension(file) } - // if `isAutoBuildFeatureEnabled` is false, then filter devfile - val isFilterDevFile = if (isAutoBuildFeatureEnabled == true) false else isDevFile(file) - - if (!isWellKnown && isFileIgnoredByExtension) { + val isIncluded = shouldIncludeInZipFile(file, isAutoBuildFeatureEnabled == true) + if (!isIncluded) { val extension = file.extension.orEmpty() ignoredExtensionMap[extension] = (ignoredExtensionMap[extension] ?: 0) + 1 return false } - if (isFilterDevFile) { - return false - } - - val isFileIgnoredByPattern = runBlocking { ignoreFile(file.name) } - if (isFileIgnoredByPattern) { - return false - } - if (file.isFile) { totalSize += file.length files.add(file) diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QConstants.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QConstants.kt index 3cc2bf81741..26424ecc17d 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QConstants.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QConstants.kt @@ -6,4 +6,5 @@ package software.aws.toolkits.jetbrains.services.amazonq object QConstants { const val Q_MARKETPLACE_URI = "https://aws.amazon.com/q/developer/" const val CODEWHISPERER_LOGIN_HELP_URI = "https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/auth-access.html" + const val MAX_FILE_SIZE_BYTES = 1024 * 1024 } diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/TelemetryUtils.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/TelemetryUtils.kt index b3c2e216811..c7e21874a89 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/TelemetryUtils.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/TelemetryUtils.kt @@ -88,7 +88,6 @@ val ALLOWED_CODE_EXTENSIONS = setOf( "idl", "ini", "io", - "jar", "java", "jl", "js", From 86e993ede886c79edda8ecca7a867e3d596db82f Mon Sep 17 00:00:00 2001 From: Neil Kulkarni <60868290+neilk-aws@users.noreply.github.com> Date: Tue, 28 Jan 2025 12:08:14 -0800 Subject: [PATCH 4/5] docs(amazonq): Update autobuild setting strings (#5301) Improves the formatting and strings presented to customers when the allow /dev to run code and test commands automatically. Also includes minor variable name changes to improve internal consistency. --- .../controller/FeatureDevController.kt | 2 +- .../settings/CodeWhispererConfigurable.kt | 52 +++++++------- .../CodeWhispererConfigurableTest.kt | 1 + .../CodeWhispererSettingsTest.kt | 67 +++++++++++++++++++ .../settings/CodeWhispererSettings.kt | 12 ++-- .../resources/MessagesBundle.properties | 3 +- 6 files changed, 108 insertions(+), 29 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 d13f03e7fce..fa027cbb729 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 @@ -727,7 +727,7 @@ class FeatureDevController( return } - val codeWhispererSettings = CodeWhispererSettings.getInstance().getAutoBuildFeatureConfiguration() + val codeWhispererSettings = CodeWhispererSettings.getInstance().getAutoBuildSetting() val hasDevFile = session.context.checkForDevFile() val isPromptedForAutoBuildFeature = codeWhispererSettings.containsKey(session.context.getWorkspaceRoot()) diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt index 2c02c5319f3..f26ffcea918 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt @@ -125,29 +125,6 @@ class CodeWhispererConfigurable(private val project: Project) : } } - group(message("aws.settings.codewhisperer.allow_q_dev_build_test_commands")) { - row { - val settings = codeWhispererSettings.getAutoBuildFeatureConfiguration() - for ((key) in settings) { - checkBox(key).apply { - connect.subscribe( - ToolkitConnectionManagerListener.TOPIC, - object : ToolkitConnectionManagerListener { - override fun activeConnectionChanged(newConnection: ToolkitConnection?) { - enabled(isCodeWhispererEnabled(project)) - } - } - ) - - bindSelected( - getter = { codeWhispererSettings.isAutoBuildFeatureEnabled(key) }, - setter = { newValue -> codeWhispererSettings.toggleAutoBuildFeature(key, newValue) } - ) - } - } - } - } - group(message("aws.settings.codewhisperer.group.q_chat")) { row { checkBox(message("aws.settings.codewhisperer.project_context")).apply { @@ -214,6 +191,35 @@ class CodeWhispererConfigurable(private val project: Project) : } } + val autoBuildSetting = codeWhispererSettings.getAutoBuildSetting() + if (autoBuildSetting.isNotEmpty()) { + group(message("aws.settings.codewhisperer.feature_development")) { + row { + text(message("aws.settings.codewhisperer.feature_development.allow_running_code_and_test_commands")) + } + row { + val settings = codeWhispererSettings.getAutoBuildSetting() + for ((key) in settings) { + checkBox(key).apply { + connect.subscribe( + ToolkitConnectionManagerListener.TOPIC, + object : ToolkitConnectionManagerListener { + override fun activeConnectionChanged(newConnection: ToolkitConnection?) { + enabled(isCodeWhispererEnabled(project)) + } + } + ) + + bindSelected( + getter = { codeWhispererSettings.isAutoBuildFeatureEnabled(key) }, + setter = { newValue -> codeWhispererSettings.toggleAutoBuildFeature(key, newValue) } + ) + } + } + } + } + } + group(message("aws.settings.codewhisperer.code_review")) { row { ExpandableTextField(ParametersListUtil.COLON_LINE_PARSER, ParametersListUtil.COLON_LINE_JOINER).also { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererConfigurableTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererConfigurableTest.kt index 1436f2423f5..d65d042061a 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererConfigurableTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererConfigurableTest.kt @@ -13,6 +13,7 @@ import org.mockito.kotlin.doNothing import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManagerListener import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanManager import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererConfigurable +import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings import software.aws.toolkits.resources.message import javax.swing.JCheckBox import javax.swing.JComponent diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererSettingsTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererSettingsTest.kt index 3a640c15bce..63bc70ae2b8 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererSettingsTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererSettingsTest.kt @@ -12,7 +12,9 @@ import com.intellij.openapi.wm.ToolWindowManager import com.intellij.openapi.wm.impl.status.widget.StatusBarWidgetsManager import com.intellij.testFramework.replaceService import com.intellij.testFramework.runInEdtAndWait +import com.intellij.util.xmlb.XmlSerializer import org.assertj.core.api.Assertions.assertThat +import org.jdom.output.XMLOutputter import org.junit.Before import org.junit.Ignore import org.junit.Test @@ -30,6 +32,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.status.CodeWhisper import software.aws.toolkits.jetbrains.services.codewhisperer.toolwindow.CodeWhispererCodeReferenceToolWindowFactory import software.aws.toolkits.jetbrains.settings.CodeWhispererConfiguration import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings +import software.aws.toolkits.jetbrains.utils.xmlElement import kotlin.test.fail class CodeWhispererSettingsTest : CodeWhispererTestBase() { @@ -145,6 +148,70 @@ class CodeWhispererSettingsTest : CodeWhispererTestBase() { assertThat(settingsManager.isIncludeCodeWithReference()).isEqualTo(true) } } + + @Test + fun `serialize settings to ensure backwards compatibility`() { + val element = xmlElement( + """ + + + """.trimIndent() + ) + + val settings = CodeWhispererSettings.getInstance() + settings.toggleAutoBuildFeature("project1", true) + + XmlSerializer.serializeInto(settings.state, element) + + val actual = XMLOutputter().outputString(element) + val expected = "\n" + + "" + + "" + + "" + + assertThat(actual).isEqualTo(expected) + } + + @Test + fun `deserialize empty settings to ensure backwards compatibility`() { + val element = xmlElement( + """ + + + """ + ) + val actual = XmlSerializer.deserialize(element, CodeWhispererConfiguration::class.java) + assertThat(actual.autoBuildSetting).hasSize(0) + } + + @Test + fun `deserialize existing settings to ensure backwards compatibility`() { + val element = xmlElement( + """ + + + + + """.trimIndent() + ) + val actual = XmlSerializer.deserialize(element, CodeWhispererConfiguration::class.java) + assertThat(actual.autoBuildSetting).hasSize(1) + assertThat(actual.autoBuildSetting["project1"]).isTrue() + } } class CodeWhispererSettingUnitTest { diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/CodeWhispererSettings.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/CodeWhispererSettings.kt index a07a6261010..48410b65949 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/CodeWhispererSettings.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/CodeWhispererSettings.kt @@ -25,15 +25,15 @@ class CodeWhispererSettings : PersistentStateComponent() - val projectAutoBuildConfigurationMap by map() + @get:Property + val autoBuildSetting by map() } enum class CodeWhispererConfigurationType { diff --git a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties index d41be10f419..5a08058621b 100644 --- a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties +++ b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties @@ -266,7 +266,8 @@ aws.settings.auto_update.notification_enable.tooltip=If unchecked, updates will aws.settings.auto_update.progress.message=Updating AWS plugins aws.settings.auto_update.text=Automatically install plugin updates when available aws.settings.aws_cli_settings=AWS CLI Settings -aws.settings.codewhisperer.allow_q_dev_build_test_commands=Amazon Q: Allow Q /dev to run code and test commands +aws.settings.codewhisperer.feature_development=Feature Development +aws.settings.codewhisperer.feature_development.allow_running_code_and_test_commands=Allow /dev to run code and test commands aws.settings.codewhisperer.automatic_import_adder=Imports recommendation aws.settings.codewhisperer.automatic_import_adder.tooltip=Amazon Q will add import statements with code suggestions when necessary aws.settings.codewhisperer.code_review=Code Review From 7c7afc400b4be86c8446c465efc01eb8794fa5de Mon Sep 17 00:00:00 2001 From: Richard Li Date: Tue, 28 Jan 2025 14:56:28 -0800 Subject: [PATCH 5/5] chore: fix lint errors --- .../services/codewhisperer/CodeWhispererConfigurableTest.kt | 1 - .../software/aws/toolkits/resources/MessagesBundle.properties | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererConfigurableTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererConfigurableTest.kt index d65d042061a..1436f2423f5 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererConfigurableTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererConfigurableTest.kt @@ -13,7 +13,6 @@ import org.mockito.kotlin.doNothing import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManagerListener import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeWhispererCodeScanManager import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererConfigurable -import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings import software.aws.toolkits.resources.message import javax.swing.JCheckBox import javax.swing.JComponent diff --git a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties index 5a08058621b..4b40b1cd3a1 100644 --- a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties +++ b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties @@ -266,8 +266,6 @@ aws.settings.auto_update.notification_enable.tooltip=If unchecked, updates will aws.settings.auto_update.progress.message=Updating AWS plugins aws.settings.auto_update.text=Automatically install plugin updates when available aws.settings.aws_cli_settings=AWS CLI Settings -aws.settings.codewhisperer.feature_development=Feature Development -aws.settings.codewhisperer.feature_development.allow_running_code_and_test_commands=Allow /dev to run code and test commands aws.settings.codewhisperer.automatic_import_adder=Imports recommendation aws.settings.codewhisperer.automatic_import_adder.tooltip=Amazon Q will add import statements with code suggestions when necessary aws.settings.codewhisperer.code_review=Code Review @@ -277,6 +275,8 @@ aws.settings.codewhisperer.configurable.controlled_by_admin=\ Controlled by aws.settings.codewhisperer.configurable.opt_out.title=Share Amazon Q content with AWS aws.settings.codewhisperer.configurable.opt_out.tooltip=When checked, your content processed by Amazon Q may be used for service improvement (except for content processed by the Amazon Q Developer Pro tier). Unchecking this box will cause AWS to delete any of your content used for that purpose. The information used to provide the Amazon Q service to you will not be affected. See the Service Terms for more detail. aws.settings.codewhisperer.configurable.title=Amazon Q +aws.settings.codewhisperer.feature_development=Feature Development +aws.settings.codewhisperer.feature_development.allow_running_code_and_test_commands=Allow /dev to run code and test commands aws.settings.codewhisperer.group.data_sharing=Data Sharing aws.settings.codewhisperer.group.inline_suggestions=Inline Suggestions aws.settings.codewhisperer.group.plugin_settings=Plugin Settings