From 7248f1976139b45fcc87b2edc4b9ce2588140cab Mon Sep 17 00:00:00 2001 From: Wilson Huang Date: Mon, 18 Nov 2024 13:07:49 -0500 Subject: [PATCH] feat(feature dev): Add setting to allow Q to run code and test commands --- ...-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