From 5fcc010377272734a95b5fe9c3b8fc25abf73c9b Mon Sep 17 00:00:00 2001 From: Wilson Huang Date: Tue, 12 Nov 2024 15:35:19 -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 | 120 +++++++++++++++--- .../messages/FeatureDevMessage.kt | 3 + .../session/PrepareCodeGenerationState.kt | 4 +- .../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 | 23 +++- .../services/telemetry/TelemetryUtils.kt | 9 ++ .../telemetry/OpenedFileTypeMetricsTest.kt | 2 +- .../resources/MessagesBundle.properties | 6 + 16 files changed, 198 insertions(+), 45 deletions(-) create mode 100644 .changes/next-release/feature-53e027cd-9c7f-4b09-bc4e-8ab2a20aba2a.json 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 0000000000..aff739da32 --- /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 b8678aead6..c38db3730a 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 84d2cd084c..502181c0f6 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 @@ -28,11 +28,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 @@ -69,6 +71,7 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.selectFol 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 @@ -121,6 +124,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) + } } } @@ -345,20 +358,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( @@ -376,7 +407,7 @@ class FeatureDevController( } } - private suspend fun newTask(tabId: String, isException: Boolean? = false) { + private suspend fun newTask(tabId: String, isException: Boolean? = false, prefilledPrompt: String? = null) { val session = getSessionInfo(tabId) val sessionLatency = System.currentTimeMillis() - session.sessionStartTime AmazonqTelemetry.endChat( @@ -387,15 +418,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) { @@ -554,6 +600,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) { @@ -566,7 +613,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) @@ -580,6 +636,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 1ad226f96b..35cd4788c3 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 @@ -239,6 +239,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 5a397919ac..62e25b9ebf 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 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 f02fc86609..2730081616 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 @@ -52,9 +52,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) @@ -64,10 +64,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)) @@ -159,8 +156,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 60d7b674c9..a46cb90886 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 8992baaa5f..bbca864db4 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 @@ -159,7 +159,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() { every { AmazonqTelemetry.endChat(amazonqConversationId = any(), amazonqEndOfTheConversationLatency = any()) } just runs runTest { - spySession.preloader(userMessage, messenger) + spySession.preloader(messenger) controller.processFollowupClickedMessage(message) } @@ -188,7 +188,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 { @@ -240,7 +240,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() { doNothing().`when`(spySession).insertChanges(any(), any(), any()) - spySession.preloader(userMessage, messenger) + spySession.preloader(messenger) controller.processFollowupClickedMessage(message) mockitoVerify( @@ -265,6 +265,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")) @@ -447,7 +448,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 { @@ -478,7 +479,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 { @@ -515,7 +516,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 49aa4d906e..773c75548f 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 12d1fd7145..8ed27d3bcc 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 @@ -57,7 +57,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 95d16c457d..b168939da7 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("Amazon Q: Allow Q /dev to run code and 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 56ac020984..f0d4b8a8b4 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 @@ -27,6 +27,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 4527e819f5..f5f0168adf 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 @@ -46,8 +46,6 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo "\\.svn", "\\.hg/?", "\\.rvm", - "\\.git/?", - "\\.gitignore", "\\.project", "\\.gem", "/\\.idea/?", @@ -71,7 +69,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 +78,17 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo ignorePatternsWithGitIgnore = (ignorePatterns + parseGitIgnore().map { Regex(it) }).toList() } - fun getProjectZip(): ZipCreationResult { + 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 +122,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 +132,17 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo object : VirtualFileVisitor() { override fun visitFile(file: VirtualFile): Boolean { val isFileIgnoredByExtension = runBlocking { ignoreFileByExtension(file) } + val isDevFile = if (isAutoBuildFeatureEnabled == true) false else file.path.contains("devfile.yaml") if (isFileIgnoredByExtension) { val extension = file.extension.orEmpty() ignoredExtensionMap[extension] = (ignoredExtensionMap[extension] ?: 0) + 1 return false } + + if (isDevFile) { + 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 0d73d8fd43..f94ac462c0 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/tst/software/aws/toolkits/jetbrains/services/telemetry/OpenedFileTypeMetricsTest.kt b/plugins/core/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/telemetry/OpenedFileTypeMetricsTest.kt index c5afffb3c2..01ff16f5c6 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 3d8ea0a76d..613314cbb2 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,7 +82,10 @@ 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.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_code=Accept code amazonqFeatureDev.follow_up.modified_source_folder=Changed source root to: {0}