Skip to content

Commit f2b56ea

Browse files
committed
Revert "Revert "fix(amazonq): support displaying customizations across all profiles o…" (aws#5612)"
This reverts commit dfdc784.
1 parent 0b592d4 commit f2b56ea

File tree

11 files changed

+121
-75
lines changed

11 files changed

+121
-75
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type" : "feature",
3+
"description" : "Amazon Q: Support selecting customizations across all Q profiles with automatic profile switching for enterprise users"
4+
}

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/credentials/CodeWhispererClientAdaptor.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import software.amazon.awssdk.services.codewhispererruntime.model.UserIntent
3838
import software.aws.toolkits.core.utils.debug
3939
import software.aws.toolkits.core.utils.getLogger
4040
import software.aws.toolkits.jetbrains.services.amazonq.codeWhispererUserContext
41+
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
4142
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
4243
import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererCustomization
4344
import software.aws.toolkits.jetbrains.services.codewhisperer.language.CodeWhispererProgrammingLanguage
@@ -78,7 +79,7 @@ interface CodeWhispererClientAdaptor {
7879

7980
fun getCodeFixJob(request: GetCodeFixJobRequest): GetCodeFixJobResponse
8081

81-
fun listAvailableCustomizations(): List<CodeWhispererCustomization>
82+
fun listAvailableCustomizations(profile: QRegionProfile): List<CodeWhispererCustomization>
8283

8384
fun startTestGeneration(uploadId: String, targetCode: List<TargetCode>, userInput: String): StartTestGenerationResponse
8485

@@ -282,9 +283,9 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW
282283
override fun getCodeFixJob(request: GetCodeFixJobRequest): GetCodeFixJobResponse = bearerClient().getCodeFixJob(request)
283284

284285
// DO NOT directly use this method to fetch customizations, use wrapper [CodeWhispererModelConfigurator.listCustomization()] instead
285-
override fun listAvailableCustomizations(): List<CodeWhispererCustomization> =
286-
bearerClient().listAvailableCustomizationsPaginator(
287-
ListAvailableCustomizationsRequest.builder().profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn).build()
286+
override fun listAvailableCustomizations(profile: QRegionProfile): List<CodeWhispererCustomization> =
287+
QRegionProfileManager.getInstance().getQClient<CodeWhispererRuntimeClient>(project, profile).listAvailableCustomizationsPaginator(
288+
ListAvailableCustomizationsRequest.builder().profileArn(profile.arn).build()
288289
)
289290
.stream()
290291
.toList()
@@ -298,7 +299,8 @@ open class CodeWhispererClientAdaptorImpl(override val project: Project) : CodeW
298299
CodeWhispererCustomization(
299300
arn = it.arn(),
300301
name = it.name(),
301-
description = it.description()
302+
description = it.description(),
303+
profile = profile
302304
)
303305
}
304306
}

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererCustomizationDialog.kt

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ import software.amazon.awssdk.arns.Arn
2626
import software.aws.toolkits.core.utils.debug
2727
import software.aws.toolkits.core.utils.getLogger
2828
import software.aws.toolkits.core.utils.tryOrNull
29+
import software.aws.toolkits.jetbrains.services.amazonq.profile.QProfileSwitchIntent
30+
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
31+
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
2932
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.Q_CUSTOM_LEARN_MORE_URI
3033
import software.aws.toolkits.jetbrains.ui.AsyncComboBox
3134
import software.aws.toolkits.jetbrains.utils.notifyInfo
@@ -34,7 +37,7 @@ import javax.swing.JComponent
3437
import javax.swing.JList
3538

3639
private val NoDataToDisplay = CustomizationUiItem(
37-
CodeWhispererCustomization("", message("codewhisperer.custom.dialog.option.no_data"), ""),
40+
CodeWhispererCustomization("", message("codewhisperer.custom.dialog.option.no_data"), "", QRegionProfile("", "")),
3841
false,
3942
false
4043
)
@@ -106,6 +109,15 @@ class CodeWhispererCustomizationDialog(
106109
RadioButtonOption.Customization -> run {
107110
CodeWhispererModelConfigurator.getInstance().switchCustomization(project, modal.selectedCustomization?.customization)
108111
notifyCustomizationIsSelected(project, modal.selectedCustomization)
112+
// Switch profile if it doesn't match the customization's profile.
113+
// Customizations are profile-scoped and must be used under the correct context.
114+
if (modal.selectedCustomization?.customization?.profile?.arn != QRegionProfileManager.getInstance().activeProfile(project)?.arn) {
115+
QRegionProfileManager.getInstance().switchProfile(
116+
project,
117+
modal.selectedCustomization?.customization?.profile,
118+
QProfileSwitchIntent.Customization
119+
)
120+
}
109121
}
110122
}
111123

@@ -179,12 +191,14 @@ class CodeWhispererCustomizationDialog(
179191
proposeModelUpdate { model ->
180192
val activeCustomization = CodeWhispererModelConfigurator.getInstance().activeCustomization(project)
181193
val unsorted = myCustomizations ?: CodeWhispererModelConfigurator.getInstance().listCustomizations(project).orEmpty()
182-
183-
val sorted = activeCustomization?.let {
184-
unsorted.putPickedUpFront(setOf(it))
185-
} ?: run {
186-
unsorted.sortedBy { it.customization.name }
187-
}
194+
val activeProfile = QRegionProfileManager.getInstance().activeProfile(project)
195+
// Group customizations by profile name (active profile first, then alphabetical), with the active customization on top
196+
val sorted = unsorted.sortedWith(
197+
compareBy<CustomizationUiItem> { it.customization.profile?.profileName != activeProfile?.profileName }
198+
.thenBy { it.customization.profile?.profileName.orEmpty() }
199+
.thenBy { it.customization.name }
200+
)
201+
.let { list -> activeCustomization?.let { list.putPickedUpFront(setOf(it)) } ?: list }
188202

189203
if (
190204
sorted.isNotEmpty() &&
@@ -259,6 +273,10 @@ private object CustomizationRenderer : ColoredListCellRenderer<CustomizationUiIt
259273
}
260274
}
261275

276+
if (it.customization.profile?.profileName?.isNotEmpty() == true) {
277+
append(" [${it.customization.profile?.profileName}]", SimpleTextAttributes.REGULAR_ATTRIBUTES)
278+
}
279+
262280
if (it.isNew) {
263281
append(" New", SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES)
264282
}

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/customization/CodeWhispererModelConfigurator.kt

Lines changed: 30 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ import software.aws.toolkits.core.utils.getLogger
2323
import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService
2424
import software.aws.toolkits.jetbrains.services.amazonq.calculateIfIamIdentityCenterConnection
2525
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
26+
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
2627
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileSelectedListener
2728
import software.aws.toolkits.jetbrains.services.codewhisperer.credentials.CodeWhispererClientAdaptor
28-
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants
2929
import software.aws.toolkits.jetbrains.utils.notifyInfo
3030
import software.aws.toolkits.jetbrains.utils.notifyWarn
3131
import software.aws.toolkits.jetbrains.utils.pluginAwareExecuteOnPooledThread
@@ -108,25 +108,24 @@ class DefaultCodeWhispererModelConfigurator : CodeWhispererModelConfigurator, Pe
108108
@RequiresBackgroundThread
109109
override fun listCustomizations(project: Project, passive: Boolean): List<CustomizationUiItem>? =
110110
calculateIfIamIdentityCenterConnection(project) {
111-
// 1. invoke API and get result
112-
val listAvailableCustomizationsResult = try {
113-
CodeWhispererClientAdaptor.getInstance(project).listAvailableCustomizations()
114-
} catch (e: Exception) {
115-
val requestId = (e as? CodeWhispererRuntimeException)?.requestId()
116-
val logMessage = if (CodeWhispererConstants.Customization.noAccessToCustomizationExceptionPredicate(e)) {
117-
// TODO: not required for non GP users
118-
"ListAvailableCustomizations: connection ${it.id} is not allowlisted, requestId: ${requestId.orEmpty()}"
119-
} else {
120-
"ListAvailableCustomizations: failed due to unknown error ${e.message}, requestId: ${requestId.orEmpty()}"
121-
}
122-
123-
LOG.debug { logMessage }
124-
null
111+
// 1. fetch all profiles, invoke fetch customizations API and get result for each profile and aggregate all the results
112+
val listAvailableProfilesResult = QRegionProfileManager.getInstance().listRegionProfiles(project)
113+
?: error("Attempted to fetch profiles while there does not exist")
114+
115+
val aggregatedCustomizations = listAvailableProfilesResult.flatMap { profile ->
116+
runCatching {
117+
CodeWhispererClientAdaptor.getInstance(project).listAvailableCustomizations(profile)
118+
}.onFailure { e ->
119+
val requestId = (e as? CodeWhispererRuntimeException)?.requestId()
120+
val logMessage = "ListAvailableCustomizations: failed due to unknown error ${e.message}, " +
121+
"requestId: ${requestId.orEmpty()}, profileName: ${profile.profileName}"
122+
LOG.debug { logMessage }
123+
}.getOrDefault(emptyList())
125124
}
126125

127126
// 2. get diff
128127
val previousCustomizationsShapshot = connectionToCustomizationsShownLastTime.getOrElse(it.id) { emptyList() }
129-
val diff = listAvailableCustomizationsResult?.filterNot { customization -> previousCustomizationsShapshot.contains(customization.arn) }?.toSet()
128+
val diff = aggregatedCustomizations.filterNot { customization -> previousCustomizationsShapshot.contains(customization.arn) }.toSet()
130129

131130
// 3 if passive,
132131
// (1) update allowlisting
@@ -135,42 +134,36 @@ class DefaultCodeWhispererModelConfigurator : CodeWhispererModelConfigurator, Pe
135134
// if not passive,
136135
// (1) update the customization list snapshot (seen by users last time) if it will be displayed
137136
if (passive) {
138-
connectionIdToIsAllowlisted[it.id] = listAvailableCustomizationsResult != null
139-
if (diff?.isNotEmpty() == true && !hasShownNewCustomizationNotification.getAndSet(true)) {
137+
connectionIdToIsAllowlisted[it.id] = aggregatedCustomizations.isNotEmpty()
138+
if (diff.isNotEmpty() && !hasShownNewCustomizationNotification.getAndSet(true)) {
140139
notifyNewCustomization(project)
141140
}
142141
} else {
143-
listAvailableCustomizationsResult?.let { customizations ->
144-
connectionToCustomizationsShownLastTime[it.id] = customizations.map { customization -> customization.arn }.toMutableList()
145-
}
142+
connectionToCustomizationsShownLastTime[it.id] = aggregatedCustomizations.map { customization -> customization.arn }.toMutableList()
146143
}
147144

148145
// 4. invalidate selected customization if
149146
// (1) the API call failed
150147
// (2) the selected customization is not in the resultset of API call
148+
// (3) the existing q region profile associated with the selected customization does not match the currently active profile
151149
activeCustomization(project)?.let { activeCustom ->
152-
if (listAvailableCustomizationsResult == null) {
150+
if (aggregatedCustomizations.isEmpty()) {
153151
invalidateSelectedAndNotify(project)
154-
} else if (!listAvailableCustomizationsResult.any { latestCustom -> latestCustom.arn == activeCustom.arn }) {
152+
} else if (!aggregatedCustomizations.any { latestCustom -> latestCustom.arn == activeCustom.arn } ||
153+
(activeCustom.profile != null && activeCustom.profile != QRegionProfileManager.getInstance().activeProfile(project))
154+
) {
155155
invalidateSelectedAndNotify(project)
156156
}
157157
}
158158

159159
// 5. transform result to UI items and return
160-
val customizationUiItems = if (diff != null) {
161-
listAvailableCustomizationsResult.let { customizations ->
162-
val nameToCount = customizations.groupingBy { customization -> customization.name }.eachCount()
163-
164-
customizations.map { customization ->
165-
CustomizationUiItem(
166-
customization,
167-
isNew = diff.contains(customization),
168-
shouldPrefixAccountId = (nameToCount[customization.name] ?: 0) > 1
169-
)
170-
}
171-
}
172-
} else {
173-
null
160+
val nameToCount = aggregatedCustomizations.groupingBy { customization -> customization.name }.eachCount()
161+
val customizationUiItems = aggregatedCustomizations.map { customization ->
162+
CustomizationUiItem(
163+
customization,
164+
isNew = diff.contains(customization),
165+
shouldPrefixAccountId = (nameToCount[customization.name] ?: 0) > 1
166+
)
174167
}
175168
connectionToCustomizationUiItems[it.id] = customizationUiItems
176169

plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererClientAdaptorTest.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
6565
import software.aws.toolkits.jetbrains.core.credentials.sono.Q_SCOPES
6666
import software.aws.toolkits.jetbrains.core.credentials.sono.SONO_REGION
6767
import software.aws.toolkits.jetbrains.services.amazonq.FEATURE_EVALUATION_PRODUCT_NAME
68+
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
6869
import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.metadata
6970
import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonRequest
7071
import software.aws.toolkits.jetbrains.services.codewhisperer.CodeWhispererTestUtil.pythonResponseWithToken
@@ -189,13 +190,13 @@ class CodeWhispererClientAdaptorTest {
189190
on { client.listAvailableCustomizationsPaginator(any<ListAvailableCustomizationsRequest>()) } doReturn sdkIterable
190191
}
191192

192-
val actual = sut.listAvailableCustomizations()
193+
val actual = sut.listAvailableCustomizations(QRegionProfile("fake_profile", "fake arn"))
193194
assertThat(actual).hasSize(3)
194195
assertThat(actual).isEqualTo(
195196
listOf(
196-
CodeWhispererCustomization(name = "custom-1", arn = "arn-1"),
197-
CodeWhispererCustomization(name = "custom-2", arn = "arn-2"),
198-
CodeWhispererCustomization(name = "custom-3", arn = "arn-3")
197+
CodeWhispererCustomization(name = "custom-1", arn = "arn-1", profile = QRegionProfile("fake_profile", "fake arn")),
198+
CodeWhispererCustomization(name = "custom-2", arn = "arn-2", profile = QRegionProfile("fake_profile", "fake arn")),
199+
CodeWhispererCustomization(name = "custom-3", arn = "arn-3", profile = QRegionProfile("fake_profile", "fake arn"))
199200
)
200201
)
201202
}

plugins/amazonq/codewhisperer/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/codewhisperer/CodeWhispererFeatureConfigServiceTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
3535
import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
3636
import software.aws.toolkits.jetbrains.core.credentials.sono.SONO_URL
3737
import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService
38+
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
3839
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
3940
import kotlin.reflect.full.memberFunctions
4041
import kotlin.test.Test
@@ -78,7 +79,7 @@ class CodeWhispererFeatureConfigServiceTest {
7879

7980
projectRule.project.replaceService(
8081
QRegionProfileManager::class.java,
81-
mock<QRegionProfileManager> { on { getQClient(any<Project>(), eq(CodeWhispererRuntimeClient::class)) } doReturn mockClient },
82+
mock<QRegionProfileManager> { on { getQClient(any<Project>(), eq(QRegionProfile()), eq(CodeWhispererRuntimeClient::class)) } doReturn mockClient },
8283
disposableRule.disposable
8384
)
8485

0 commit comments

Comments
 (0)