Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type" : "bugfix",
"description" : "Amazon Q: Fix profile selection errors and EDT threading violations (#6039)"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type" : "bugfix",
"description" : "Amazon Q: Fix file change notifications not being reflected in IDE and improve error handling in chat commands"
}
Original file line number Diff line number Diff line change
Expand Up @@ -289,26 +289,12 @@ class BrowserConnector(

CHAT_LIST_CONVERSATIONS -> {
handleChat(AmazonQChatServer.listConversations, node)
.whenComplete { response, _ ->
browser.postChat(
FlareUiMessage(
command = CHAT_LIST_CONVERSATIONS,
params = response
)
)
}
.handleChatResponse(CHAT_LIST_CONVERSATIONS, browser)
}

CHAT_CONVERSATION_CLICK -> {
handleChat(AmazonQChatServer.conversationClick, node)
.whenComplete { response, _ ->
browser.postChat(
FlareUiMessage(
command = CHAT_CONVERSATION_CLICK,
params = response
)
)
}
.handleChatResponse(CHAT_CONVERSATION_CLICK, browser)
}

CHAT_FEEDBACK -> {
Expand Down Expand Up @@ -511,60 +497,25 @@ class BrowserConnector(
}
LIST_MCP_SERVERS_REQUEST_METHOD -> {
handleChat(AmazonQChatServer.listMcpServers, node)
.whenComplete { response, _ ->
browser.postChat(
FlareUiMessage(
command = LIST_MCP_SERVERS_REQUEST_METHOD,
params = response
)
)
}
.handleChatResponse(LIST_MCP_SERVERS_REQUEST_METHOD, browser)
}
MCP_SERVER_CLICK_REQUEST_METHOD -> {
handleChat(AmazonQChatServer.mcpServerClick, node)
.whenComplete { response, _ ->
browser.postChat(
FlareUiMessage(
command = MCP_SERVER_CLICK_REQUEST_METHOD,
params = response
)
)
}
.handleChatResponse(MCP_SERVER_CLICK_REQUEST_METHOD, browser)
}

OPEN_FILE_DIALOG -> {
handleChat(AmazonQChatServer.showOpenFileDialog, node)
.whenComplete { response, _ ->
browser.postChat(
FlareUiMessage(
command = OPEN_FILE_DIALOG_REQUEST_METHOD,
params = response
)
)
}
.handleChatResponse(OPEN_FILE_DIALOG_REQUEST_METHOD, browser)
}

LIST_RULES_REQUEST_METHOD -> {
handleChat(AmazonQChatServer.listRules, node)
.whenComplete { response, _ ->
browser.postChat(
FlareUiMessage(
command = LIST_RULES_REQUEST_METHOD,
params = response
)
)
}
.handleChatResponse(LIST_RULES_REQUEST_METHOD, browser)
}
RULE_CLICK_REQUEST_METHOD -> {
handleChat(AmazonQChatServer.ruleClick, node)
.whenComplete { response, _ ->
browser.postChat(
FlareUiMessage(
command = RULE_CLICK_REQUEST_METHOD,
params = response
)
)
}
.handleChatResponse(RULE_CLICK_REQUEST_METHOD, browser)
}
CHAT_PINNED_CONTEXT_ADD -> {
handleChat(AmazonQChatServer.pinnedContextAdd, node)
Expand All @@ -574,14 +525,7 @@ class BrowserConnector(
}
LIST_AVAILABLE_MODELS -> {
handleChat(AmazonQChatServer.listAvailableModels, node)
.whenComplete { response, _ ->
browser.postChat(
FlareUiMessage(
command = LIST_AVAILABLE_MODELS,
params = response
)
)
}
.handleChatResponse(LIST_AVAILABLE_MODELS, browser)
}
}
}
Expand Down Expand Up @@ -794,6 +738,32 @@ class BrowserConnector(
private val JsonNode.params
get() = get("params")

/**
* Helper function to handle chat response with proper error handling.
* Logs errors and sends response to browser only if successful.
*/
private fun <T> CompletableFuture<T>.handleChatResponse(
command: String,
browser: Browser,
) {
whenComplete { response, error ->
if (error != null) {
LOG.warn(error) { "Error handling chat command: $command" }
} else if (response != null) {
try {
browser.postChat(
FlareUiMessage(
command = command,
params = response as Any
)
)
} catch (e: Exception) {
LOG.warn(e) { "Error sending response for command: $command" }
}
}
}
}

companion object {
private val LOG = getLogger<BrowserConnector>()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -587,11 +587,11 @@ class AmazonQLanguageClientImpl(private val project: Project) : AmazonQLanguageC
val currPath = Paths.get(path)
if (currPath.startsWith(localHistoryPath)) return
try {
ApplicationManager.getApplication().executeOnPooledThread {
VfsUtil.markDirtyAndRefresh(false, true, true, currPath.toFile())
}
// Use synchronous refresh (async=false) to ensure IDE picks up changes immediately
// This is important for the agent workflow where files are modified externally
VfsUtil.markDirtyAndRefresh(false, true, true, currPath.toFile())
} catch (e: Exception) {
LOG.warn(e) { "Could not refresh file" }
LOG.warn(e) { "Could not refresh file: $path" }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat

import com.google.gson.Gson
import com.google.gson.JsonObject
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
Expand Down Expand Up @@ -203,10 +204,13 @@ class ChatCommunicationManager(private val project: Project, private val cs: Cor
project.messageBus.syncPublisher(QRegionProfileSelectedListener.TOPIC)
.onProfileSelected(project, QRegionProfileManager.getInstance().activeProfile(project))
} else {
QRegionProfileDialog(
project,
selectedProfile = null
).show()
// Wrap in runInEdt to ensure dialog is shown on Event Dispatch Thread
runInEdt {
QRegionProfileDialog(
project,
selectedProfile = null
).show()
}
}

return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.intellij.openapi.editor.LogicalPosition
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
Expand Down Expand Up @@ -98,23 +99,44 @@ object LspEditorUtil {
}

fun applyWorkspaceEdit(project: Project, edit: WorkspaceEdit) {
val modifiedFiles = mutableSetOf<VirtualFile>()

WriteCommandAction.runWriteCommandAction(project) {
edit.documentChanges?.forEach { change ->
if (change.isLeft) {
val textDocumentEdit = change.left
applyEditsToFile(project, textDocumentEdit.textDocument.uri, textDocumentEdit.edits)
applyEditsToFile(project, textDocumentEdit.textDocument.uri, textDocumentEdit.edits)?.let {
modifiedFiles.add(it)
}
}
}

edit.changes?.forEach { (uri, textEdits) ->
applyEditsToFile(project, uri, textEdits)
applyEditsToFile(project, uri, textEdits)?.let {
modifiedFiles.add(it)
}
}

// Save all modified documents to ensure changes are persisted
if (modifiedFiles.isNotEmpty()) {
val documentManager = FileDocumentManager.getInstance()
modifiedFiles.forEach { file ->
documentManager.getDocument(file)?.let { doc ->
documentManager.saveDocument(doc)
}
}
}
}

// Refresh VFS for all modified files to notify the IDE of changes
if (modifiedFiles.isNotEmpty()) {
VfsUtil.markDirtyAndRefresh(false, false, true, *modifiedFiles.toTypedArray())
}
}

private fun applyEditsToFile(project: Project, uri: String, textEdits: List<TextEdit>) {
val file = VirtualFileManager.getInstance().findFileByUrl(uri) ?: return
val document = FileDocumentManager.getInstance().getDocument(file) ?: return
private fun applyEditsToFile(project: Project, uri: String, textEdits: List<TextEdit>): VirtualFile? {
val file = VirtualFileManager.getInstance().findFileByUrl(uri) ?: return null
val document = FileDocumentManager.getInstance().getDocument(file) ?: return null
val editor = FileEditorManager.getInstance(project).getSelectedEditor(file)?.let {
if (it is com.intellij.openapi.fileEditor.TextEditor) it.editor else null
}
Expand All @@ -124,6 +146,8 @@ object LspEditorUtil {
val endOffset = calculateOffset(editor, document, textEdit.range.end)
document.replaceString(startOffset, endOffset, textEdit.newText)
}

return file
}

private fun calculateOffset(editor: Editor?, document: Document, position: Position): Int =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,21 @@ class QRegionProfileDialog(

combo.proposeModelUpdate { model ->
try {
QRegionProfileManager.getInstance().listRegionProfiles(project)?.forEach {
model.addElement(it)
} ?: error("Attempted to fetch profiles while there does not exist")

val profiles = QRegionProfileManager.getInstance().listRegionProfiles(project)
// Handle empty/null profiles gracefully instead of throwing
if (profiles.isNullOrEmpty()) {
val conn = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance()) as? AwsBearerTokenConnection
Telemetry.amazonq.didSelectProfile.use { span ->
span.source(QProfileSwitchIntent.User.value)
.amazonQProfileRegion("not-set")
.ssoRegion(conn?.region)
.credentialStartUrl(conn?.startUrl)
.result(MetricResult.Failed)
.reason("No profiles available")
}
return@proposeModelUpdate
}
profiles.forEach { model.addElement(it) }
model.selectedItem = selectedProfile
} catch (e: Exception) {
val conn = ToolkitConnectionManager.getInstance(project).activeConnectionForFeature(QConnection.getInstance()) as? AwsBearerTokenConnection
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,14 @@ class QRegionProfileManager : PersistentStateComponent<QProfileState>, Disposabl
if (mappedProfiles.size == 1) {
switchProfile(project, mappedProfiles.first(), intent = QProfileSwitchIntent.Update)
}
mappedProfiles.takeIf { it.isNotEmpty() }?.also {
connectionIdToProfileCount[connection.id] = it.size
} ?: error("You don't have access to the resource")
// Profiles are required for Q features - return null when empty so callers can handle appropriately
// (e.g., show profile selection UI, display error message, or disable profile-dependent features)
if (mappedProfiles.isEmpty()) {
LOG.debug { "No region profiles available - profiles are required for using Q features" }
return@try null
}
connectionIdToProfileCount[connection.id] = mappedProfiles.size
mappedProfiles
} catch (e: Exception) {
if (e is AccessDeniedException) {
LOG.warn { "Failed to list region profiles: ${e.message}" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,12 @@ class DefaultCodeWhispererModelConfigurator(
calculateIfIamIdentityCenterConnection(project) {
// 1. fetch all profiles, invoke fetch customizations API and get result for each profile and aggregate all the results
val profiles = QRegionProfileManager.getInstance().listRegionProfiles(project)
?: error("Attempted to fetch profiles while there does not exist")
// Profiles are required for customization features - return empty list if unavailable
// This can happen when: (1) user has no profiles configured, (2) API call fails, (3) connection not authorized
if (profiles.isNullOrEmpty()) {
LOG.debug { "No profiles available for customization listing - profiles required for this feature" }
return@calculateIfIamIdentityCenterConnection emptyList()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not expected behavior. We need a profile to proceed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens in the case of null profiles?

}

val customizations = profiles.flatMap { profile ->
runCatching {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package software.aws.toolkits.jetbrains.ui

import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.progress.ProgressIndicator
Expand Down Expand Up @@ -92,7 +93,9 @@ class AsyncComboBox<T> private constructor(
currentIndicator?.cancel()
loading.set(true)
removeAllItems()
repaint()
// Ensure repaint happens on EDT to avoid "Read access is allowed from inside read-action"
// and "Write access is allowed inside write-action only" errors when updating Swing components
ApplicationManager.getApplication().invokeLater { repaint() }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please add to the description what is the error that we get here?

val indicator = EmptyProgressIndicator(ModalityState.any()).also {
currentIndicator = it
}
Expand All @@ -105,7 +108,8 @@ class AsyncComboBox<T> private constructor(
newModel.invoke(delegatedComboBoxModel(indicator))
}.invokeOnCompletion {
loading.set(false)
repaint()
// Ensure repaint happens on EDT to avoid Swing threading errors
ApplicationManager.getApplication().invokeLater { repaint() }
}
},
indicator
Expand Down