Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
# _3.63_ (2025-04-08)
- **(Feature)** Enterprise users can choose their preferred Amazon Q profile to improve personalization and workflow across different business regions
- **(Bug Fix)** Amazon Q /doc: close diff tab and open README file in preview mode after user accept changes

# _3.62_ (2025-04-03)
- **(Feature)** /review: automatically generate fix without clicking Generate Fix button
- **(Bug Fix)** /transform: prompt user to re-authenticate if credentials expire during transformation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ import software.aws.toolkits.core.utils.error
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
import software.aws.toolkits.jetbrains.common.session.Intent
import software.aws.toolkits.jetbrains.core.awsClient
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
import software.aws.toolkits.jetbrains.services.amazonq.clients.AmazonQStreamingClient
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
import software.aws.toolkits.jetbrains.services.amazonqDoc.FEATURE_EVALUATION_PRODUCT_NAME
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.calculateTotalLatency
import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata
Expand Down Expand Up @@ -72,7 +72,7 @@ class AmazonQCodeGenerateClient(private val project: Project) {
fun connection() = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())
?: error("Attempted to use connection while one does not exist")

fun bearerClient() = QRegionProfileManager.getInstance().getQClient<CodeWhispererRuntimeClient>(project)
fun bearerClient() = connection().getConnectionSettings().awsClient<CodeWhispererRuntimeClient>()

private val amazonQStreamingClient
get() = AmazonQStreamingClient.getInstance(project)
Expand All @@ -88,7 +88,6 @@ class AmazonQCodeGenerateClient(private val project: Project) {
}
requestBuilder.optOutPreference(getTelemetryOptOutPreference())
requestBuilder.userContext(docUserContext)
requestBuilder.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}

fun sendDocMetricData(operationName: String, result: String): SendTelemetryEventResponse =
Expand Down Expand Up @@ -119,9 +118,7 @@ class AmazonQCodeGenerateClient(private val project: Project) {
}

fun createTaskAssistConversation(): CreateTaskAssistConversationResponse = bearerClient().createTaskAssistConversation(
CreateTaskAssistConversationRequest.builder()
.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
.build()
CreateTaskAssistConversationRequest.builder().build()
)

fun createTaskAssistUploadUrl(conversationId: String, contentChecksumSha256: String, contentLength: Long): CreateUploadUrlResponse =
Expand All @@ -140,7 +137,6 @@ class AmazonQCodeGenerateClient(private val project: Project) {
)
.build()
)
.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}

fun startTaskAssistCodeGeneration(conversationId: String, uploadId: String, userMessage: String, intent: Intent): StartTaskAssistCodeGenerationResponse =
Expand All @@ -159,7 +155,6 @@ class AmazonQCodeGenerateClient(private val project: Project) {
.uploadId(uploadId)
}
.intent(intent.name)
.profileArn(QRegionProfileManager.getInstance().activeProfile(project)?.arn)
}

fun getTaskAssistCodeGeneration(conversationId: String, codeGenerationId: String): GetTaskAssistCodeGenerationResponse = bearerClient()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package software.aws.toolkits.jetbrains.services.amazonq
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
Expand Down Expand Up @@ -34,16 +33,11 @@ import software.aws.toolkits.jetbrains.core.webview.BrowserState
import software.aws.toolkits.jetbrains.core.webview.LoginBrowser
import software.aws.toolkits.jetbrains.core.webview.WebviewResourceHandlerFactory
import software.aws.toolkits.jetbrains.isDeveloperMode
import software.aws.toolkits.jetbrains.services.amazonq.profile.QProfileSwitchIntent
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.util.createBrowser
import software.aws.toolkits.jetbrains.utils.isQConnected
import software.aws.toolkits.jetbrains.utils.isQExpired
import software.aws.toolkits.jetbrains.utils.isQWebviewsAvailable
import software.aws.toolkits.telemetry.FeatureId
import software.aws.toolkits.telemetry.MetricResult
import software.aws.toolkits.telemetry.Telemetry
import software.aws.toolkits.telemetry.UiTelemetry
import software.aws.toolkits.telemetry.WebviewTelemetry
import java.awt.event.ActionListener
Expand Down Expand Up @@ -210,18 +204,6 @@ class QWebviewBrowser(val project: Project, private val parentDisposable: Dispos
UiTelemetry.click(project, signInOption)
}
}

is BrowserMessage.SwitchProfile -> {
QRegionProfileManager.getInstance().switchProfile(
project,
QRegionProfile(profileName = message.profileName, arn = message.arn),
intent = QProfileSwitchIntent.Auth
)
}

is BrowserMessage.PublishWebviewTelemetry -> {
publishTelemetry(message)
}
}
}

Expand Down Expand Up @@ -262,35 +244,13 @@ class QWebviewBrowser(val project: Project, private val parentDisposable: Dispos
}

// TODO: pass "REAUTH" if connection expires
// Perform the potentially blocking AWS call outside the EDT to fetch available region profiles.
ApplicationManager.getApplication().executeOnPooledThread {
var errorMessage: String? = null
val profiles: List<QRegionProfile> = try {
QRegionProfileManager.getInstance().listRegionProfiles(project).orEmpty()
} catch (e: Exception) {
errorMessage = e.message
LOG.warn { "Failed to call listRegionProfiles API" }
val qConn = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())
Telemetry.amazonq.didSelectProfile.use { span ->
span.source(QProfileSwitchIntent.Auth.value)
.amazonQProfileRegion(QRegionProfileManager.getInstance().activeProfile(project)?.region ?: "not-set")
.ssoRegion((qConn as? AwsBearerTokenConnection)?.region)
.credentialStartUrl((qConn as? AwsBearerTokenConnection)?.startUrl)
.result(MetricResult.Failed)
.reason(e.message)
}
emptyList()
}

val stage = if (isQExpired(project)) {
"REAUTH"
} else if (isQConnected(project) && QRegionProfileManager.getInstance().isPendingProfileSelection(project)) {
"PROFILE_SELECT"
} else {
"START"
}
val stage = if (isQExpired(project)) {
"REAUTH"
} else {
"START"
}

val jsonData = """
val jsonData = """
{
stage: '$stage',
regions: $regions,
Expand All @@ -301,16 +261,10 @@ class QWebviewBrowser(val project: Project, private val parentDisposable: Dispos
},
cancellable: ${state.browserCancellable},
feature: '${state.feature}',
existConnections: ${writeValueAsString(selectionSettings.values.map { it.currentSelection }.toList())},
profiles: ${writeValueAsString(profiles)},
errorMessage: ${errorMessage?.let { "\"$it\"" } ?: "null"}
existConnections: ${writeValueAsString(selectionSettings.values.map { it.currentSelection }.toList())}
}
""".trimIndent()

runInEdt {
executeJS("window.ideClient.prepareUi($jsonData)")
}
}
""".trimIndent()
executeJS("window.ideClient.prepareUi($jsonData)")
}

override fun loginIAM(profileName: String, accessKey: String, secretKey: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
import software.aws.toolkits.jetbrains.core.gettingstarted.emitUserState
import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
import software.aws.toolkits.jetbrains.services.amazonq.project.ProjectContextController
import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindow
import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindowFactory
Expand Down Expand Up @@ -53,9 +52,6 @@ class AmazonQStartupActivity : ProjectActivity {
CodeWhispererExplorerActionManager.getInstance().setIsFirstRestartAfterQInstall(false)
}
}

QRegionProfileManager.getInstance().validateProfile(project)

startLsp(project)
if (runOnce.get()) return
emitUserState(project)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
import software.aws.toolkits.jetbrains.services.amazonq.messages.MessageConnector
import software.aws.toolkits.jetbrains.services.amazonq.onboarding.OnboardingPageInteraction
import software.aws.toolkits.jetbrains.services.amazonq.onboarding.OnboardingPageInteractionType
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
import software.aws.toolkits.jetbrains.services.amazonq.util.highlightCommand
import software.aws.toolkits.jetbrains.services.amazonq.webview.BrowserConnector
import software.aws.toolkits.jetbrains.services.amazonq.webview.FqnWebviewAdapter
Expand Down Expand Up @@ -128,8 +127,7 @@ class AmazonQToolWindow private constructor(
isCodeScanAvailable = isCodeScanAvailable(project),
isCodeTestAvailable = isCodeTestAvailable(project),
isDocAvailable = isDocAvailable(project),
highlightCommand = highlightCommand(),
activeProfile = QRegionProfileManager.getInstance().takeIf { it.shouldDisplayProfileInfo(project) }?.activeProfile(project)
highlightCommand = highlightCommand()
)

scope.launch {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ import com.intellij.ui.components.panels.Wrapper
import com.intellij.util.ui.components.BorderLayoutPanel
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManagerListener
import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection
import software.aws.toolkits.jetbrains.core.credentials.sono.Q_SCOPES
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenAuthState
import software.aws.toolkits.jetbrains.core.credentials.sso.bearer.BearerTokenProviderListener
import software.aws.toolkits.jetbrains.core.notifications.NotificationPanel
Expand All @@ -26,9 +28,6 @@ import software.aws.toolkits.jetbrains.core.webview.BrowserState
import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel
import software.aws.toolkits.jetbrains.services.amazonq.RefreshQChatPanelButtonPressedListener
import software.aws.toolkits.jetbrains.services.amazonq.gettingstarted.openMeetQPage
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.utils.isQConnected
import software.aws.toolkits.jetbrains.utils.isQExpired
import software.aws.toolkits.jetbrains.utils.isQWebviewsAvailable
Expand Down Expand Up @@ -63,11 +62,7 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
ToolkitConnectionManagerListener.TOPIC,
object : ToolkitConnectionManagerListener {
override fun activeConnectionChanged(newConnection: ToolkitConnection?) {
ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance())?.let { qConn ->
openMeetQPage(project)
QRegionProfileManager.getInstance().validateProfile(project)
}
prepareChatContent(project, qPanel)
onConnectionChanged(project, newConnection, qPanel)
}
}
)
Expand All @@ -76,7 +71,9 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
RefreshQChatPanelButtonPressedListener.TOPIC,
object : RefreshQChatPanelButtonPressedListener {
override fun onRefresh() {
prepareChatContent(project, qPanel)
runInEdt {
prepareChatContent(project, qPanel)
}
}
}
)
Expand All @@ -86,19 +83,12 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
object : BearerTokenProviderListener {
override fun onChange(providerId: String, newScopes: List<String>?) {
if (ToolkitConnectionManager.getInstance(project).connectionStateForFeature(QConnection.getInstance()) == BearerTokenAuthState.AUTHORIZED) {
prepareChatContent(project, qPanel)
}
}
}
)
val qComponent = AmazonQToolWindow.getInstance(project).component

project.messageBus.connect(toolWindow.disposable).subscribe(
QRegionProfileSelectedListener.TOPIC,
object : QRegionProfileSelectedListener {
override fun onProfileSelected(project: Project, profile: QRegionProfile?) {
if (project.isDisposed) return
AmazonQToolWindow.getInstance(project).disposeAndRecreate()
prepareChatContent(project, qPanel)
runInEdt {
qPanel.setContent(qComponent)
}
}
}
}
)
Expand All @@ -117,21 +107,13 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {
project: Project,
qPanel: Wrapper,
) {
/**
* only render Q Chat when
* 1. There is a Q connection
* 2. Q connection is not expired
* 3. User is not pending region profile selection
*/
val component = if (isQConnected(project) && !isQExpired(project) && !QRegionProfileManager.getInstance().isPendingProfileSelection(project)) {
val component = if (isQConnected(project) && !isQExpired(project)) {
AmazonQToolWindow.getInstance(project).component
} else {
QWebviewPanel.getInstance(project).browser?.prepareBrowser(BrowserState(FeatureId.AmazonQ))
QWebviewPanel.getInstance(project).component
}
runInEdt {
qPanel.setContent(component)
}
qPanel.setContent(component)
}

override fun init(toolWindow: ToolWindow) {
Expand All @@ -152,6 +134,36 @@ class AmazonQToolWindowFactory : ToolWindowFactory, DumbAware {

override fun shouldBeAvailable(project: Project): Boolean = isQWebviewsAvailable()

private fun onConnectionChanged(project: Project, newConnection: ToolkitConnection?, qPanel: Wrapper) {
val isNewConnectionForQ = newConnection?.let {
(it as? AwsBearerTokenConnection)?.let { conn ->
val scopeShouldHave = Q_SCOPES

LOG.debug { "newConnection: ${conn.id}; scope: ${conn.scopes}; scope must-have: $scopeShouldHave" }

scopeShouldHave.all { s -> s in conn.scopes }
} ?: false
} ?: false

if (isNewConnectionForQ) {
openMeetQPage(project)
}

QWebviewPanel.getInstance(project).browser?.prepareBrowser(BrowserState(FeatureId.AmazonQ))

// isQConnected alone is not robust and there is race condition (read/update connection states)
val component = if (isNewConnectionForQ || (isQConnected(project) && !isQExpired(project))) {
LOG.debug { "returning Q-chat window; isQConnection=$isNewConnectionForQ; hasPinnedConnection=$isNewConnectionForQ" }
AmazonQToolWindow.getInstance(project).component
} else {
LOG.debug { "returning login window; no Q connection found" }
QWebviewPanel.getInstance(project).component
}
runInEdt {
qPanel.setContent(component)
}
}

companion object {
private val LOG = getLogger<AmazonQToolWindowFactory>()
const val WINDOW_ID = AMAZON_Q_WINDOW_ID
Expand Down
Loading
Loading