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/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 2f0b06f1b2f..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 @@ -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 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 cead9106694..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 @@ -32,11 +32,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 @@ -77,6 +79,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 @@ -135,6 +138,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) + } } } @@ -440,20 +453,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( @@ -471,9 +502,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 @@ -485,15 +514,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) { @@ -670,6 +714,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) { @@ -682,8 +727,18 @@ class FeatureDevController( return } - session.preloader(message, messenger) + val codeWhispererSettings = CodeWhispererSettings.getInstance().getAutoBuildSetting() + val hasDevFile = session.context.checkForDevFile() + val isPromptedForAutoBuildFeature = codeWhispererSettings.containsKey(session.context.getWorkspaceRoot()) + + if (hasDevFile && !isPromptedForAutoBuildFeature) { + promptAllowQCommandsConsent(messenger, tabId) + return + } + + session.preloader(messenger) broadcastQEvent(QFeatureEvent.INVOCATION) + when (session.sessionState.phase) { SessionStatePhase.CODEGEN -> onCodeGeneration(session, message, tabId) else -> null @@ -696,6 +751,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 77c6dc094db..9d82818c4a9 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 @@ -250,6 +250,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 41a2ee2079a..b9dff5445dd 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 @@ -48,7 +49,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 @@ -96,7 +98,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 0e87db9da96..dd1bf4bd7cf 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 @@ -67,9 +67,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) @@ -79,10 +79,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)) @@ -281,8 +278,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 136c4d3e433..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 @@ -55,10 +56,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 @@ -73,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", @@ -96,10 +140,14 @@ class FeatureDevSessionContextTest : FeatureDevTestBase(HeavyJavaCodeInsightTest "License.md", "node_modules/express", "build/outputs", - "dist/bundle.js" + "dist/bundle.js", + "gradle/wrapper/gradle-wrapper.jar", + "devfile.yaml", ) - val zipResult = featureDevSessionContext.getProjectZip() + 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,17 +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", - ) - - assertTrue(zippedFiles == expectedFiles) + assertEquals(zippedFiles, expectedFiles) } } 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 dfba40646fa..9c8b5774254 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 @@ -171,7 +171,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() { every { AmazonqTelemetry.endChat(amazonqConversationId = any(), amazonqEndOfTheConversationLatency = any()) } just runs runTest { - spySession.preloader(userMessage, messenger) + spySession.preloader(messenger) controller.processFollowupClickedMessage(message) } @@ -200,7 +200,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 { @@ -255,7 +255,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() { doReturn(Unit).whenever(spySession).insertNewFiles(any()) doReturn(Unit).whenever(spySession).applyDeleteFiles(any()) - spySession.preloader(userMessage, messenger) + spySession.preloader(messenger) controller.processFollowupClickedMessage(message) mockitoVerify( @@ -290,6 +290,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")) @@ -818,7 +819,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() { mockkStatic("software.aws.toolkits.jetbrains.common.util.FileUtilsKt") every { selectFolder(any(), any()) } returns null - spySession.preloader(userMessage, messenger) + spySession.preloader(messenger) controller.processFollowupClickedMessage(message) coVerifyOrder { @@ -849,7 +850,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() { mockkStatic("software.aws.toolkits.jetbrains.common.util.FileUtilsKt") every { selectFolder(any(), any()) } returns LightVirtualFile("/path") - spySession.preloader(userMessage, messenger) + spySession.preloader(messenger) controller.processFollowupClickedMessage(message) coVerifyOrder { @@ -886,7 +887,7 @@ class FeatureDevControllerTest : FeatureDevTestBase() { mockkStatic("software.aws.toolkits.jetbrains.common.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 b7ef89ebd54..c5473036095 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 @@ -88,7 +88,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 { @@ -96,6 +96,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 5284632947a..104cd064615 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 8ee849449fc..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 @@ -191,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/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/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/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 556aa3a0c10..45b96113dcc 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 @@ -28,6 +28,16 @@ class CodeWhispererSettings : PersistentStateComponent() + + @get:Property + val autoBuildSetting 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 1005a1d9b4c..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,7 +17,9 @@ 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 import software.aws.toolkits.telemetry.AmazonqTelemetry import java.io.File @@ -32,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 @@ -46,17 +46,22 @@ 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 additionalGitIgnoreRules = setOf( + private val additionalGitIgnoreFolderRules = setOf( ".aws-sam", ".gem", ".git", - ".gitignore", ".gradle", ".hg", ".idea", ".project", ".rvm", ".svn", + "node_modules", + "build", + "dist", + ) + + private val additionalGitIgnoreBinaryFilesRules = setOf( "*.zip", "*.bin", "*.png", @@ -69,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 @@ -86,15 +88,20 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo val projectRoot = project.guessProjectDir() ?: error("Cannot guess base directory for project ${project.name}") private val projectRootPath = Paths.get(projectRoot.path) ?: error("Can not find project root path") - // 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 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() @@ -104,10 +111,18 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo } } - 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))) @@ -121,34 +136,51 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo return ALLOWED_CODE_EXTENSIONS.contains(extension) } - private fun ignoreFileByExtension(file: VirtualFile) = - !isFileExtensionAllowed(file) - - suspend fun ignoreFile(file: VirtualFile): Boolean = ignoreFile(file.presentableUrl) - - suspend fun ignoreFile(path: String): Boolean { - // 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/") } - } - } + fun ignoreFile(file: VirtualFile, applyExtraBinaryFilesRules: Boolean = true): Boolean = ignoreFile(file.presentableUrl, applyExtraBinaryFilesRules) - // 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() } + 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 } } - fun wellKnown(file: VirtualFile): Boolean = wellKnownSourceFiles.contains(file.name) + private fun wellKnown(file: VirtualFile): Boolean = wellKnownSourceFiles.contains(file.name) - suspend fun zipFiles(projectRoot: VirtualFile): File = withContext(getCoroutineBgContext()) { + private fun shouldIncludeInZipFile(file: VirtualFile, isAutoBuildFeatureEnabled: Boolean): Boolean { + // large files always ignored + if (file.length > MAX_FILE_SIZE_BYTES) { + return false + } + + // always respect gitignore rules and remove binary files if auto build is disabled + val isFileIgnoredByPattern = ignoreFile(file, !isAutoBuildFeatureEnabled) + if (isFileIgnoredByPattern) { + return false + } + + // 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 + } + + // Any other files should not be included + return false + } + + suspend fun zipFiles(projectRoot: VirtualFile, isAutoBuildFeatureEnabled: Boolean?): File = withContext(getCoroutineBgContext()) { val files = mutableListOf() val ignoredExtensionMap = mutableMapOf().withDefault { 0L } var totalSize: Long = 0 @@ -157,17 +189,12 @@ 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 (!isWellKnown && isFileIgnoredByExtension) { + val isIncluded = shouldIncludeInZipFile(file, isAutoBuildFeatureEnabled == true) + if (!isIncluded) { val extension = file.extension.orEmpty() ignoredExtensionMap[extension] = (ignoredExtensionMap[extension] ?: 0) + 1 return false } - val isFileIgnoredByPattern = runBlocking { ignoreFile(file.name) } - if (isFileIgnoredByPattern) { - return false - } if (file.isFile) { totalSize += file.length 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 81f4431f3ad..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 @@ -21,6 +21,7 @@ val ALLOWED_CODE_EXTENSIONS = setOf( "cbl", "cc", "cfc", + "cfg", "cfm", "cjs", "clj", @@ -32,6 +33,7 @@ val ALLOWED_CODE_EXTENSIONS = setOf( "cob", "cobra", "coffee", + "config", "cpp", "cpy", "cr", @@ -47,6 +49,7 @@ val ALLOWED_CODE_EXTENSIONS = setOf( "e", "el", "elm", + "env", "erl", "ex", "exs", @@ -62,6 +65,7 @@ val ALLOWED_CODE_EXTENSIONS = setOf( "fsi", "fsx", "gd", + "gitignore", "go", "gql", "gradle", @@ -82,8 +86,8 @@ val ALLOWED_CODE_EXTENSIONS = setOf( "html", "hy", "idl", + "ini", "io", - "jar", "java", "jl", "js", @@ -95,6 +99,7 @@ val ALLOWED_CODE_EXTENSIONS = setOf( "lgt", "lhs", "lisp", + "lock", "logtalk", "lsp", "lua", @@ -181,14 +186,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 0abddfef282..4b40b1cd3a1 100644 --- a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties +++ b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties @@ -79,7 +79,10 @@ amazonqDoc.session.create=Create documentation for a specific folder amazonqDoc.session.sync=Sync documentation 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... @@ -118,8 +121,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 @@ -269,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