diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt index aabefda3365..5253812f8f3 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt @@ -22,7 +22,6 @@ import software.amazon.awssdk.services.codewhispererruntime.model.GetCodeFixJobR import software.amazon.awssdk.services.codewhispererruntime.model.GetTestGenerationResponse import software.amazon.awssdk.services.codewhispererruntime.model.IdeCategory import software.amazon.awssdk.services.codewhispererruntime.model.InlineChatUserDecision -import software.amazon.awssdk.services.codewhispererruntime.model.ListAvailableCustomizationsRequest import software.amazon.awssdk.services.codewhispererruntime.model.ListCodeAnalysisFindingsRequest import software.amazon.awssdk.services.codewhispererruntime.model.ListCodeAnalysisFindingsResponse import software.amazon.awssdk.services.codewhispererruntime.model.ListFeatureEvaluationsResponse @@ -35,8 +34,6 @@ import software.amazon.awssdk.services.codewhispererruntime.model.StartTestGener import software.amazon.awssdk.services.codewhispererruntime.model.SuggestionState import software.amazon.awssdk.services.codewhispererruntime.model.TargetCode import software.amazon.awssdk.services.codewhispererruntime.model.UserIntent -import software.aws.toolkits.core.utils.debug -import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.jetbrains.services.amazonq.codeWhispererUserContext import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererCustomization @@ -78,8 +75,6 @@ interface CodeWhispererClientAdaptor { fun getCodeFixJob(request: GetCodeFixJobRequest): GetCodeFixJobResponse - fun listAvailableCustomizations(): List - fun startTestGeneration(uploadId: String, targetCode: List, userInput: String): StartTestGenerationResponse fun getTestGeneration(jobId: String, jobGroupName: String): GetTestGenerationResponse @@ -281,28 +276,6 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW override fun getCodeFixJob(request: GetCodeFixJobRequest): GetCodeFixJobResponse = bearerClient().getCodeFixJob(request) - // DO NOT directly use this method to fetch customizations, use wrapper [CodeWhispererModelConfigurator.listCustomization()] instead - override fun listAvailableCustomizations(): List = - bearerClient().listAvailableCustomizationsPaginator( - ListAvailableCustomizationsRequest.builder().profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn).build() - ) - .stream() - .toList() - .flatMap { resp -> - LOG.debug { - "listAvailableCustomizations: requestId: ${resp.responseMetadata().requestId()}, customizations: ${ - resp.customizations().map { it.name() } - }" - } - resp.customizations().map { - CodeWhispererCustomization( - arn = it.arn(), - name = it.name(), - description = it.description() - ) - } - } - override fun startTestGeneration(uploadId: String, targetCode: List, userInput: String): StartTestGenerationResponse = bearerClient().startTestGeneration { builder -> builder.uploadId(uploadId) @@ -799,10 +772,6 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW requestBuilder.userContext(codeWhispererUserContext()) requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn) } - - companion object { - private val LOG = getLogger() - } } private fun CodewhispererSuggestionState.toCodeWhispererSdkType() = when { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererService.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererService.kt index 50a579fedd1..94cb1acc230 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererService.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererService.kt @@ -86,6 +86,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhisperer import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getTelemetryOptOutPreference import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.notifyErrorCodeWhispererUsageLimit import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.promptReAuth +import software.aws.toolkits.jetbrains.services.codewhisperer.util.CustomizationConstants import software.aws.toolkits.jetbrains.services.codewhisperer.util.FileContextProvider import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings import software.aws.toolkits.jetbrains.utils.isInjectedText @@ -333,7 +334,7 @@ class CodeWhispererService(private val cs: CoroutineScope) : Disposable { val displayMessage: String if ( - CodeWhispererConstants.Customization.invalidCustomizationExceptionPredicate(e) || + CustomizationConstants.invalidCustomizationExceptionPredicate(e) || e is ResourceNotFoundException ) { (e as CodeWhispererRuntimeException) diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererServiceNew.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererServiceNew.kt index 633a20ac0f9..bf8c0dab081 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererServiceNew.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/service/CodeWhispererServiceNew.kt @@ -83,6 +83,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhisperer import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.getTelemetryOptOutPreference import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.notifyErrorCodeWhispererUsageLimit import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererUtil.promptReAuth +import software.aws.toolkits.jetbrains.services.codewhisperer.util.CustomizationConstants import software.aws.toolkits.jetbrains.services.codewhisperer.util.FileContextProvider import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings import software.aws.toolkits.jetbrains.utils.isInjectedText @@ -360,7 +361,7 @@ class CodeWhispererServiceNew(private val cs: CoroutineScope) : Disposable { val displayMessage: String if ( - CodeWhispererConstants.Customization.invalidCustomizationExceptionPredicate(e) || + CustomizationConstants.invalidCustomizationExceptionPredicate(e) || e is ResourceNotFoundException ) { (e as CodeWhispererRuntimeException) 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 77f901950c7..e03521841da 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 @@ -11,6 +11,7 @@ import com.intellij.openapi.options.Configurable import com.intellij.openapi.options.SearchableConfigurable import com.intellij.openapi.options.ex.Settings import com.intellij.openapi.project.Project +import com.intellij.openapi.project.ProjectManager import com.intellij.openapi.ui.emptyText import com.intellij.ui.components.ActionLink import com.intellij.ui.components.fields.ExpandableTextField @@ -23,6 +24,7 @@ import com.intellij.util.concurrency.EdtExecutorService import com.intellij.util.execution.ParametersListUtil import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManagerListener +import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererLoginType import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExplorerActionManager import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.isCodeWhispererEnabled @@ -284,6 +286,20 @@ class CodeWhispererConfigurable(private val project: Project) : } } } + }.also { + val newCallbacks = it.applyCallbacks.toMutableMap() + .also { map -> + val list = map.getOrPut(null) { mutableListOf() } as MutableList<() -> Unit> + list.add { + ProjectManager.getInstance().openProjects.forEach { project -> + if (project.isDisposed) { + return@forEach + } + AmazonQLspService.didChangeConfiguration(project) + } + } + } + it.applyCallbacks = newCallbacks } companion object { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererConstants.kt b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererConstants.kt index 9e328c54b81..c37e143c534 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererConstants.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CodeWhispererConstants.kt @@ -8,8 +8,6 @@ import com.intellij.openapi.editor.markup.EffectType import com.intellij.openapi.editor.markup.TextAttributes import com.intellij.ui.JBColor import software.amazon.awssdk.regions.Region -import software.amazon.awssdk.services.codewhispererruntime.model.AccessDeniedException -import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException import software.aws.toolkits.jetbrains.services.codewhisperer.codescan.CodeScanResponse import software.aws.toolkits.jetbrains.services.codewhisperer.language.languages.CodeWhispererJava import software.aws.toolkits.telemetry.CodewhispererGettingStartedTask @@ -59,7 +57,6 @@ object CodeWhispererConstants { val scanResultsKey = DataKey.create("amazonq.codescan.result") val scanScopeKey = DataKey.create("amazonq.codescan.scope") - const val Q_CUSTOM_LEARN_MORE_URI = "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/customizations.html" const val Q_SUPPORTED_LANG_URI = "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/q-language-ide-support.html" const val CODEWHISPERER_CODE_SCAN_LEARN_MORE_URI = "https://docs.aws.amazon.com/codewhisperer/latest/userguide/security-scans.html" const val CODEWHISPERER_ONBOARDING_DOCUMENTATION_URI = "https://docs.aws.amazon.com/codewhisperer/latest/userguide/features.html" @@ -157,27 +154,6 @@ object CodeWhispererConstants { val Sigv4ClientRegion = Region.US_EAST_1 } - object Customization { - private const val noAccessToCustomizationMessage = "Your account is not authorized to use CodeWhisperer Enterprise." - private const val invalidCustomizationMessage = "You are not authorized to access" - - val noAccessToCustomizationExceptionPredicate: (e: Exception) -> Boolean = { e -> - if (e !is CodeWhispererRuntimeException) { - false - } else { - e is AccessDeniedException && (e.message?.contains(noAccessToCustomizationMessage, ignoreCase = true) ?: false) - } - } - - val invalidCustomizationExceptionPredicate: (e: Exception) -> Boolean = { e -> - if (e !is CodeWhispererRuntimeException) { - false - } else { - e is AccessDeniedException && (e.message?.contains(invalidCustomizationMessage, ignoreCase = true) ?: false) - } - } - } - object CrossFile { const val CHUNK_SIZE = 60 const val NUMBER_OF_LINE_IN_CHUNK = 50 diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererClientAdaptorTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererClientAdaptorTest.kt index 9a3a455d6e1..448d1beed8f 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererClientAdaptorTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererClientAdaptorTest.kt @@ -31,13 +31,10 @@ import software.amazon.awssdk.services.codewhispererruntime.model.CodeAnalysisSt import software.amazon.awssdk.services.codewhispererruntime.model.CompletionType import software.amazon.awssdk.services.codewhispererruntime.model.CreateUploadUrlRequest import software.amazon.awssdk.services.codewhispererruntime.model.CreateUploadUrlResponse -import software.amazon.awssdk.services.codewhispererruntime.model.Customization import software.amazon.awssdk.services.codewhispererruntime.model.GenerateCompletionsRequest import software.amazon.awssdk.services.codewhispererruntime.model.GetCodeAnalysisRequest import software.amazon.awssdk.services.codewhispererruntime.model.GetCodeAnalysisResponse import software.amazon.awssdk.services.codewhispererruntime.model.IdeCategory -import software.amazon.awssdk.services.codewhispererruntime.model.ListAvailableCustomizationsRequest -import software.amazon.awssdk.services.codewhispererruntime.model.ListAvailableCustomizationsResponse import software.amazon.awssdk.services.codewhispererruntime.model.ListCodeAnalysisFindingsRequest import software.amazon.awssdk.services.codewhispererruntime.model.ListCodeAnalysisFindingsResponse import software.amazon.awssdk.services.codewhispererruntime.model.ListFeatureEvaluationsRequest @@ -51,7 +48,6 @@ import software.amazon.awssdk.services.codewhispererruntime.model.StartCodeAnaly import software.amazon.awssdk.services.codewhispererruntime.model.StartCodeAnalysisResponse import software.amazon.awssdk.services.codewhispererruntime.model.SuggestionState import software.amazon.awssdk.services.codewhispererruntime.paginators.GenerateCompletionsIterable -import software.amazon.awssdk.services.codewhispererruntime.paginators.ListAvailableCustomizationsIterable import software.amazon.awssdk.services.ssooidc.SsoOidcClient import software.aws.toolkits.core.utils.test.aString import software.aws.toolkits.jetbrains.core.MockClientManagerRule @@ -158,48 +154,6 @@ class CodeWhispererClientAdaptorTest { .isInstanceOf(CodeWhispererRuntimeClient::class.java) } - @Test - fun `listCustomizations`() { - val sdkIterable = ListAvailableCustomizationsIterable(bearerClient, ListAvailableCustomizationsRequest.builder().build()) - val mockResponse1 = ListAvailableCustomizationsResponse.builder() - .customizations( - listOf( - Customization.builder().name("custom-1").arn("arn-1").build(), - Customization.builder().name("custom-2").arn("arn-2").build() - ) - ) - .nextToken("token-1") - .responseMetadata(metadata) - .sdkHttpResponse(sdkHttpResponse) - .build() as ListAvailableCustomizationsResponse - - val mockResponse2 = ListAvailableCustomizationsResponse.builder() - .customizations( - listOf( - Customization.builder().name("custom-3").arn("arn-3").build(), - ) - ) - .nextToken("") - .responseMetadata(metadata) - .sdkHttpResponse(sdkHttpResponse) - .build() as ListAvailableCustomizationsResponse - - bearerClient.stub { client -> - on { client.listAvailableCustomizations(any()) } doReturnConsecutively listOf(mockResponse1, mockResponse2) - on { client.listAvailableCustomizationsPaginator(any()) } doReturn sdkIterable - } - - val actual = sut.listAvailableCustomizations() - assertThat(actual).hasSize(3) - assertThat(actual).isEqualTo( - listOf( - CodeWhispererCustomization(name = "custom-1", arn = "arn-1"), - CodeWhispererCustomization(name = "custom-2", arn = "arn-2"), - CodeWhispererCustomization(name = "custom-3", arn = "arn-3") - ) - ) - } - @Test fun `generateCompletionsPaginator - bearer`() { val request = pythonRequest diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererModelConfiguratorTest.kt b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererModelConfiguratorTest.kt index 751db478b5b..cd3b0b840a5 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererModelConfiguratorTest.kt +++ b/plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererModelConfiguratorTest.kt @@ -470,7 +470,7 @@ class CodeWhispererModelConfiguratorTest { "" + "" - assertThat(actual).isEqualTo(expected) + assertThat(actual).isEqualToIgnoringWhitespace(expected) } @Test @@ -574,11 +574,6 @@ class CodeWhispererModelConfiguratorTest { assertThat(sut.activeCustomization(projectRule.project)).isEqualTo(oldCustomization) - val fakeCustomizations = listOf( - CodeWhispererCustomization("oldArn", "oldName", "oldDescription") - ) - mockClintAdaptor.stub { on { listAvailableCustomizations() } doReturn fakeCustomizations } - ApplicationManager.getApplication().messageBus .syncPublisher(QRegionProfileSelectedListener.TOPIC) .onProfileSelected(projectRule.project, null) @@ -593,10 +588,6 @@ class CodeWhispererModelConfiguratorTest { val oldCustomization = CodeWhispererCustomization("oldArn", "oldName", "oldDescription") sut.switchCustomization(projectRule.project, oldCustomization) assertThat(sut.activeCustomization(projectRule.project)).isEqualTo(oldCustomization) - val fakeCustomizations = listOf( - CodeWhispererCustomization("newArn", "newName", "newDescription") - ) - mockClintAdaptor.stub { on { listAvailableCustomizations() } doReturn fakeCustomizations } val latch = CountDownLatch(1) 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 84263c62ad2..e97d9d96b20 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 @@ -13,9 +13,7 @@ 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 io.mockk.every import io.mockk.junit4.MockKRule -import io.mockk.mockkObject import org.assertj.core.api.Assertions.assertThat import org.jdom.output.XMLOutputter import org.junit.Before @@ -28,7 +26,6 @@ import org.mockito.kotlin.spy import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import software.aws.toolkits.jetbrains.core.ToolWindowHeadlessManagerImpl -import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererLoginType import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.CodeWhispererExploreActionState import software.aws.toolkits.jetbrains.services.codewhisperer.explorer.isCodeWhispererEnabled @@ -254,18 +251,6 @@ class CodeWhispererSettingsTest : CodeWhispererTestBase() { assertThat(sut.getProjectContextIndexMaxSize()).isEqualTo(expected) } } - - @Test - fun `toggleMetricOptIn should trigger LSP didChangeConfiguration`() { - mockkObject(AmazonQLspService) - every { AmazonQLspService.didChangeConfiguration(any()) } returns Unit - settingsManager.toggleMetricOptIn(true) - settingsManager.toggleMetricOptIn(false) - - io.mockk.verify(atLeast = 2) { - AmazonQLspService.didChangeConfiguration(any()) - } - } } class CodeWhispererSettingUnitTest { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/migration/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererModelConfigurator.kt b/plugins/amazonq/shared/jetbrains-community/src/migration/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererModelConfigurator.kt similarity index 96% rename from plugins/amazonq/codewhisperer/jetbrains-community/src/migration/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererModelConfigurator.kt rename to plugins/amazonq/shared/jetbrains-community/src/migration/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererModelConfigurator.kt index b95002fe19f..4f7cae9cd55 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/migration/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererModelConfigurator.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/migration/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererModelConfigurator.kt @@ -1,4 +1,4 @@ -// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package migration.software.aws.toolkits.jetbrains.services.codewhisperer.customization diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt index 6d4c0f171ac..26e68a51890 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt @@ -5,6 +5,7 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp import com.intellij.notification.NotificationType import com.intellij.openapi.project.Project +import migration.software.aws.toolkits.jetbrains.settings.AwsSettings import org.eclipse.lsp4j.ConfigurationParams import org.eclipse.lsp4j.MessageActionItem import org.eclipse.lsp4j.MessageParams @@ -18,6 +19,7 @@ import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.ChatCommunicationManager import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.ConnectionMetadata import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.SsoProfileData +import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings import java.util.concurrent.CompletableFuture @@ -79,14 +81,31 @@ class AmazonQLanguageClientImpl(private val project: Project) : AmazonQLanguageC return CompletableFuture.completedFuture( buildList { + val qSettings = CodeWhispererSettings.getInstance() params.items.forEach { when (it.section) { AmazonQLspConstants.LSP_CW_CONFIGURATION_KEY -> { add( CodeWhispererLspConfiguration( - shouldShareData = CodeWhispererSettings.getInstance().isMetricOptIn(), - shouldShareCodeReferences = CodeWhispererSettings.getInstance().isIncludeCodeWithReference(), - shouldEnableWorkspaceContext = CodeWhispererSettings.getInstance().isWorkspaceContextEnabled() + shouldShareData = qSettings.isMetricOptIn(), + shouldShareCodeReferences = qSettings.isIncludeCodeWithReference(), + // server context + shouldEnableWorkspaceContext = qSettings.isWorkspaceContextEnabled() + ) + ) + } + AmazonQLspConstants.LSP_Q_CONFIGURATION_KEY -> { + add( + AmazonQLspConfiguration( + optOutTelemetry = AwsSettings.getInstance().isTelemetryEnabled, + customization = CodeWhispererModelConfigurator.getInstance().activeCustomization(project)?.arn, + // local context + enableLocalIndexing = qSettings.isProjectContextEnabled(), + indexWorkerThreads = qSettings.getProjectContextIndexThreadCount(), + enableGpuAcceleration = qSettings.isProjectContextGpu(), + localIndexing = LocalIndexingConfiguration( + maxIndexSizeMB = qSettings.getProjectContextIndexMaxSize() + ) ) ) } diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspConfiguration.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspConfiguration.kt new file mode 100644 index 00000000000..90a85dd9ea5 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspConfiguration.kt @@ -0,0 +1,32 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonq.lsp + +import com.google.gson.annotations.SerializedName + +data class AmazonQLspConfiguration( + @SerializedName(AmazonQLspConstants.LSP_OPT_OUT_TELEMETRY_CONFIGURATION_KEY) + val optOutTelemetry: Boolean? = null, + + @SerializedName(AmazonQLspConstants.LSP_ENABLE_TELEMETRY_EVENTS_CONFIGURATION_KEY) + val enableTelemetryEvents: Boolean? = null, + + @SerializedName(AmazonQLspConstants.LSP_CUSTOMIZATION_CONFIGURATION_KEY) + val customization: String? = null, + + val enableLocalIndexing: Boolean? = null, + + val enableGpuAcceleration: Boolean? = null, + + val indexWorkerThreads: Int? = null, + + val localIndexing: LocalIndexingConfiguration? = null, +) + +data class LocalIndexingConfiguration( + val ignoreFilePatterns: List? = null, + val maxFileSizeMB: Int? = null, + val maxIndexSizeMB: Int? = null, + val indexCacheDirPath: String? = null, +) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspConstants.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspConstants.kt index ca8fffcbb51..0c93dec0550 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspConstants.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspConstants.kt @@ -8,5 +8,9 @@ object AmazonQLspConstants { const val LSP_CW_CONFIGURATION_KEY = "aws.codeWhisperer" const val LSP_CW_OPT_OUT_KEY = "shareCodeWhispererContentWithAWS" const val LSP_CODE_REFERENCES_OPT_OUT_KEY = "includeSuggestionsWithCodeReferences" + const val LSP_Q_CONFIGURATION_KEY = "aws.q" + const val LSP_OPT_OUT_TELEMETRY_CONFIGURATION_KEY = "optOutTelemetry" + const val LSP_ENABLE_TELEMETRY_EVENTS_CONFIGURATION_KEY = "enableTelemetryEventsToDestination" + const val LSP_CUSTOMIZATION_CONFIGURATION_KEY = "customization" const val LSP_WORKSPACE_CONTEXT_ENABLED_KEY = "workspaceContext" } diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererCustomizationDialog.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererCustomizationDialog.kt similarity index 97% rename from plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererCustomizationDialog.kt rename to plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererCustomizationDialog.kt index eedff33e2c3..5726484a4b3 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererCustomizationDialog.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererCustomizationDialog.kt @@ -26,7 +26,6 @@ import software.amazon.awssdk.arns.Arn import software.aws.toolkits.core.utils.debug import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.core.utils.tryOrNull -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.Q_CUSTOM_LEARN_MORE_URI import software.aws.toolkits.jetbrains.ui.AsyncComboBox import software.aws.toolkits.jetbrains.utils.notifyInfo import software.aws.toolkits.resources.message @@ -162,7 +161,12 @@ class CodeWhispererCustomizationDialog( lateinit var customizationComment: Row indent { noCustomizationComment = row("") { - rowComment(message("codewhisperer.custom.dialog.option.customization.description.no_customization", Q_CUSTOM_LEARN_MORE_URI)) + rowComment( + message( + "codewhisperer.custom.dialog.option.customization.description.no_customization", + "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/customizations.html" + ) + ) }.visible(false) customizationComment = row("") { diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererCustomizationListener.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererCustomizationListener.kt similarity index 100% rename from plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererCustomizationListener.kt rename to plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererCustomizationListener.kt diff --git a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererModelConfigurator.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererModelConfigurator.kt similarity index 92% rename from plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererModelConfigurator.kt rename to plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererModelConfigurator.kt index 6f3a84fcbf9..a7799fc0d9b 100644 --- a/plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererModelConfigurator.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererModelConfigurator.kt @@ -17,15 +17,16 @@ import com.intellij.openapi.project.Project import com.intellij.util.concurrency.annotations.RequiresBackgroundThread import com.intellij.util.xmlb.annotations.MapAnnotation import com.intellij.util.xmlb.annotations.Property +import software.amazon.awssdk.services.codewhispererruntime.CodeWhispererRuntimeClient import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException import software.aws.toolkits.core.utils.debug import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService import software.aws.toolkits.jetbrains.services.amazonq.calculateIfIamIdentityCenterConnection import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile +import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileSelectedListener -import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor -import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants +import software.aws.toolkits.jetbrains.services.codewhisperer.util.CustomizationConstants import software.aws.toolkits.jetbrains.utils.notifyInfo import software.aws.toolkits.jetbrains.utils.notifyWarn import software.aws.toolkits.jetbrains.utils.pluginAwareExecuteOnPooledThread @@ -105,15 +106,36 @@ class DefaultCodeWhispererModelConfigurator : CodeWhispererModelConfigurator, Pe } } + /** + * DO NOT directly use this method to fetch customizations, use wrapper [listCustomizations] instead + */ + private fun listAvailableCustomizations(project: Project): List = + QRegionProfileManager.getInstance().getQClient(project) + .listAvailableCustomizationsPaginator {} + .flatMap { resp -> + LOG.debug { + "listAvailableCustomizations: requestId: ${resp.responseMetadata().requestId()}, customizations: ${ + resp.customizations().map { it.name() } + }" + } + resp.customizations().map { + CodeWhispererCustomization( + arn = it.arn(), + name = it.name(), + description = it.description() + ) + } + } + @RequiresBackgroundThread override fun listCustomizations(project: Project, passive: Boolean): List? = calculateIfIamIdentityCenterConnection(project) { // 1. invoke API and get result val listAvailableCustomizationsResult = try { - CodeWhispererClientAdaptor.getInstance(project).listAvailableCustomizations() + listAvailableCustomizations(project) } catch (e: Exception) { val requestId = (e as? CodeWhispererRuntimeException)?.requestId() - val logMessage = if (CodeWhispererConstants.Customization.noAccessToCustomizationExceptionPredicate(e)) { + val logMessage = if (CustomizationConstants.noAccessToCustomizationExceptionPredicate(e)) { // TODO: not required for non GP users "ListAvailableCustomizations: connection ${it.id} is not allowlisted, requestId: ${requestId.orEmpty()}" } else { diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CustomizationConstants.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CustomizationConstants.kt new file mode 100644 index 00000000000..ebcb4d730a3 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/util/CustomizationConstants.kt @@ -0,0 +1,28 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.codewhisperer.util + +import software.amazon.awssdk.services.codewhispererruntime.model.AccessDeniedException +import software.amazon.awssdk.services.codewhispererruntime.model.CodeWhispererRuntimeException + +object CustomizationConstants { + private const val noAccessToCustomizationMessage = "Your account is not authorized to use CodeWhisperer Enterprise." + private const val invalidCustomizationMessage = "You are not authorized to access" + + val noAccessToCustomizationExceptionPredicate: (e: Exception) -> Boolean = { e -> + if (e !is CodeWhispererRuntimeException) { + false + } else { + e is AccessDeniedException && (e.message?.contains(noAccessToCustomizationMessage, ignoreCase = true) ?: false) + } + } + + val invalidCustomizationExceptionPredicate: (e: Exception) -> Boolean = { e -> + if (e !is CodeWhispererRuntimeException) { + false + } else { + e is AccessDeniedException && (e.message?.contains(invalidCustomizationMessage, ignoreCase = true) ?: false) + } + } +} 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 2c5cac62e57..0fc7e53f5ba 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 @@ -10,9 +10,7 @@ import com.intellij.openapi.components.Service import com.intellij.openapi.components.State import com.intellij.openapi.components.Storage import com.intellij.openapi.components.service -import com.intellij.openapi.project.ProjectManager import com.intellij.util.xmlb.annotations.Property -import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService import software.aws.toolkits.jetbrains.utils.notifyInfo import software.aws.toolkits.resources.AmazonQBundle @@ -23,13 +21,6 @@ class CodeWhispererSettings : PersistentStateComponent() + every { project.service() } returns mockConnectionManager + + val telemetryEnabled = Random.nextBoolean() + val customizationArn = aString() + val workerThreads = Random.nextInt(CONTEXT_INDEX_THREADS) + val indexSize = Random.nextInt(CONTEXT_INDEX_SIZE) + val enableIndexing = Random.nextBoolean() + val enableGpu = Random.nextBoolean() + + val mockQSettings = mockk { + every { getProjectContextIndexThreadCount() } returns workerThreads + every { getProjectContextIndexMaxSize() } returns indexSize + every { isProjectContextEnabled() } returns enableIndexing + every { isProjectContextGpu() } returns enableGpu + } + mockkObject(CodeWhispererSettings.Companion) + every { CodeWhispererSettings.getInstance() } returns mockQSettings + + // Mock CodeWhispererModelConfigurator + val mockConfigurator = mockk { + every { activeCustomization(project) } returns CodeWhispererCustomization( + arn = customizationArn, + name = "name", + description = "description", + ) + } + ApplicationManager.getApplication().replaceService(CodeWhispererModelConfigurator::class.java, mockConfigurator, disposable) + + // Mock AWS Settings + AwsSettings.getInstance().isTelemetryEnabled = telemetryEnabled + + assertThat(sut.configuration(configurationParams("aws.q")).get()) + .singleElement() + .isEqualTo( + AmazonQLspConfiguration( + optOutTelemetry = telemetryEnabled, + customization = customizationArn, + enableLocalIndexing = enableIndexing, + enableGpuAcceleration = enableGpu, + indexWorkerThreads = workerThreads, + localIndexing = LocalIndexingConfiguration( + maxIndexSizeMB = indexSize, + ) + ) + ) + } + + @Test + fun `Gson serializes AmazonQLspConfiguration correctly`() { + val sut = AmazonQLspConfiguration( + optOutTelemetry = true, + enableTelemetryEvents = true, + customization = "arn", + enableLocalIndexing = true, + enableGpuAcceleration = true, + indexWorkerThreads = 123, + localIndexing = LocalIndexingConfiguration( + maxFileSizeMB = 789, + maxIndexSizeMB = 456, + indexCacheDirPath = "/a/path", + ignoreFilePatterns = listOf("ignore", "patterns") + ) + ) + + assertThat(Gson().toJson(sut)).isEqualToIgnoringWhitespace( + // language=JSON + """ + { + "optOutTelemetry": true, + "enableTelemetryEventsToDestination": true, + "customization": "arn", + "enableLocalIndexing": true, + "enableGpuAcceleration": true, + "indexWorkerThreads": 123, + "localIndexing": { + "ignoreFilePatterns": [ + "ignore", + "patterns" + ], + "maxFileSizeMB": 789, + "maxIndexSizeMB": 456, + "indexCacheDirPath": "/a/path" + } + } + """.trimIndent() + ) + } + private fun configurationParams(vararg attributes: String) = ConfigurationParams( attributes.map { ConfigurationItem().apply {