diff --git a/.changes/3.74.json b/.changes/3.74.json new file mode 100644 index 00000000000..ceea3211ddd --- /dev/null +++ b/.changes/3.74.json @@ -0,0 +1,11 @@ +{ + "date" : "2025-06-05", + "version" : "3.74", + "entries" : [ { + "type" : "feature", + "description" : "Agentic coding experience: Amazon Q can now write code and run shell commands on your behalf" + }, { + "type" : "bugfix", + "description" : "Support full Unicode range in inline chat panel on Windows" + } ] +} \ No newline at end of file diff --git a/.changes/next-release/bugfix-061149bd-c6ef-4c86-9f12-98e38fe3b576.json b/.changes/next-release/bugfix-061149bd-c6ef-4c86-9f12-98e38fe3b576.json deleted file mode 100644 index 3a97907cb1d..00000000000 --- a/.changes/next-release/bugfix-061149bd-c6ef-4c86-9f12-98e38fe3b576.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type" : "bugfix", - "description" : "Support full Unicode range in inline chat panel on Windows" -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e0116b680f..85632cf8a2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# _3.74_ (2025-06-05) +- **(Feature)** Agentic coding experience: Amazon Q can now write code and run shell commands on your behalf +- **(Bug Fix)** Support full Unicode range in inline chat panel on Windows + # _3.73_ (2025-05-29) - **(Bug Fix)** /transform: handle InvalidGrantException properly when polling job status diff --git a/gradle.properties b/gradle.properties index 65bf15a2f2f..6aabb46f9e0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 # Toolkit Version -toolkitVersion=3.74-SNAPSHOT +toolkitVersion=3.75-SNAPSHOT # Publish Settings publishToken= diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQPanel.kt index 25743840c89..f9a6a207ebd 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQPanel.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQPanel.kt @@ -25,6 +25,7 @@ import software.aws.toolkits.jetbrains.isDeveloperMode import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext import software.aws.toolkits.jetbrains.services.amazonq.apps.AppConnection import software.aws.toolkits.jetbrains.services.amazonq.commands.MessageTypeRegistry +import software.aws.toolkits.jetbrains.services.amazonq.isQSupportedInThisVersion import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.AsyncChatUiListener import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.FlareUiMessage @@ -41,6 +42,7 @@ import software.aws.toolkits.jetbrains.services.amazonqCodeTest.auth.isCodeTestA import software.aws.toolkits.jetbrains.services.amazonqDoc.auth.isDocAvailable import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.auth.isFeatureDevAvailable import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isCodeTransformAvailable +import software.aws.toolkits.resources.message import java.nio.file.Paths import java.util.concurrent.CompletableFuture import javax.swing.JButton @@ -102,6 +104,9 @@ class AmazonQPanel(val project: Project, private val scope: CoroutineScope) : Di webviewContainer.add(JBTextArea("JCEF not supported")) } browser.complete(null) + } else if (!isQSupportedInThisVersion()) { + webviewContainer.add(JBTextArea("${message("q.unavailable")}\n ${message("q.unavailable.node")}")) + browser.complete(null) } else { val loadingPanel = JBLoadingPanel(null, this) val wrapper = Wrapper() diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt index 9d39606ba8b..83d6a532ef1 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt @@ -224,7 +224,7 @@ class BrowserConnector( ) val serializedEnrichmentParams = serializer.objectMapper.valueToTree(enrichmentParams) - val chatParams: ObjectNode = (node as ObjectNode) + val chatParams: ObjectNode = (node.params as ObjectNode) .setAll(serializedEnrichmentParams) val tabId = requestFromUi.params.tabId @@ -235,7 +235,7 @@ class BrowserConnector( val result = AmazonQLspService.executeIfRunning(project) { server -> encryptionManager = this.encryptionManager - val encryptedParams = EncryptedChatParams(this.encryptionManager.encrypt(chatParams.params), partialResultToken) + val encryptedParams = EncryptedChatParams(this.encryptionManager.encrypt(chatParams), partialResultToken) rawEndpoint.request(SEND_CHAT_COMMAND_PROMPT, encryptedParams) as CompletableFuture } ?: (CompletableFuture.failedFuture(IllegalStateException("LSP Server not running"))) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ActionRegistrar.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ActionRegistrar.kt index b9a6efd5b48..e5e92f7544b 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ActionRegistrar.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/ActionRegistrar.kt @@ -29,7 +29,7 @@ class ActionRegistrar { fun reportMessageClick(command: EditorContextCommand, project: Project) { if (command == EditorContextCommand.GenerateUnitTests) { - AsyncChatUiListener.notifyPartialMessageUpdate(Gson().toJson(TestCommandMessage())) + AsyncChatUiListener.notifyPartialMessageUpdate(project, Gson().toJson(TestCommandMessage())) } else { // new agentic chat route ApplicationManager.getApplication().executeOnPooledThread { @@ -45,7 +45,7 @@ class ActionRegistrar { val params = SendToPromptParams(selection = codeSelection, triggerType = TriggerType.CONTEXT_MENU) uiMessage = FlareUiMessage(command = SEND_TO_PROMPT, params = params) } - AsyncChatUiListener.notifyPartialMessageUpdate(uiMessage) + AsyncChatUiListener.notifyPartialMessageUpdate(project, uiMessage) } } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/ExplainCodeIssueAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/ExplainCodeIssueAction.kt index bf744d4b569..94f572984a9 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/ExplainCodeIssueAction.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/commands/codescan/actions/ExplainCodeIssueAction.kt @@ -4,6 +4,7 @@ package software.aws.toolkits.jetbrains.services.cwc.commands.codescan.actions import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.DataKey @@ -18,7 +19,14 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SendT import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.TriggerType class ExplainCodeIssueAction : AnAction(), DumbAware { + override fun getActionUpdateThread() = ActionUpdateThread.BGT + + override fun update(e: AnActionEvent) { + e.presentation.isEnabledAndVisible = e.project != null + } + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return val issueDataKey = DataKey.create>("amazonq.codescan.explainissue") val issueContext = e.getData(issueDataKey) ?: return @@ -50,7 +58,7 @@ class ExplainCodeIssueAction : AnAction(), DumbAware { ) val uiMessage = FlareUiMessage(SEND_TO_PROMPT, params) - AsyncChatUiListener.notifyPartialMessageUpdate(uiMessage) + AsyncChatUiListener.notifyPartialMessageUpdate(project, uiMessage) } } } diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QUtils.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QUtils.kt index f6dea5c681e..55b6c46d7ef 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QUtils.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QUtils.kt @@ -3,7 +3,9 @@ package software.aws.toolkits.jetbrains.services.amazonq +import com.intellij.openapi.application.ApplicationInfo import com.intellij.openapi.project.Project +import com.intellij.openapi.util.BuildNumber import com.intellij.openapi.util.SystemInfo import software.amazon.awssdk.services.codewhispererruntime.model.IdeCategory import software.amazon.awssdk.services.codewhispererruntime.model.OperatingSystem @@ -52,3 +54,12 @@ fun codeWhispererUserContext(): UserContext = ClientMetadata.getDefault().let { .ideVersion(it.awsVersion) .build() } + +fun isQSupportedInThisVersion(): Boolean { + val currentBuild = ApplicationInfo.getInstance().build.withoutProductCode() + + return !( + currentBuild.baselineVersion == 242 && + BuildNumber.fromString("242.22855.74")?.let { currentBuild < it } == true + ) +} 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 01eda2ea2c1..577b51cf612 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 @@ -158,18 +158,21 @@ class AmazonQLanguageClientImpl(private val project: Project) : AmazonQLanguageC // The filepath sent by the server contains unicode characters which need to be // decoded for JB file handling APIs to be handle to handle file operations val fileToOpen = URLDecoder.decode(params.uri, StandardCharsets.UTF_8.name()) - ApplicationManager.getApplication().invokeLater { - try { - val virtualFile = VirtualFileManager.getInstance().findFileByUrl(fileToOpen) - ?: throw IllegalArgumentException("Cannot find file: $fileToOpen") - - FileEditorManager.getInstance(project).openFile(virtualFile, true) - } catch (e: Exception) { - LOG.warn { "Failed to show document: $fileToOpen" } - } - } + return CompletableFuture.supplyAsync( + { + try { + val virtualFile = VirtualFileManager.getInstance().refreshAndFindFileByUrl(fileToOpen) + ?: throw IllegalArgumentException("Cannot find file: $fileToOpen") - return CompletableFuture.completedFuture(ShowDocumentResult(true)) + FileEditorManager.getInstance(project).openFile(virtualFile, true) + ShowDocumentResult(true) + } catch (e: Exception) { + LOG.warn { "Failed to show document: $fileToOpen" } + ShowDocumentResult(false) + } + }, + ApplicationManager.getApplication()::invokeLater + ) } catch (e: Exception) { LOG.warn { "Error showing document" } return CompletableFuture.completedFuture(ShowDocumentResult(false)) @@ -312,6 +315,7 @@ class AmazonQLanguageClientImpl(private val project: Project) : AmazonQLanguageC override fun sendChatUpdate(params: LSPAny): CompletableFuture { AsyncChatUiListener.notifyPartialMessageUpdate( + project, FlareUiMessage( command = CHAT_SEND_UPDATE, params = params, diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt index c2f91fbbe8a..1f20fe7e265 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt @@ -78,10 +78,12 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.textdocument.TextDoc import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.WorkspaceFolderUtil.createWorkspaceFolders import software.aws.toolkits.jetbrains.services.amazonq.lsp.workspace.WorkspaceServiceHandler import software.aws.toolkits.jetbrains.services.amazonq.profile.QDefaultServiceConfig +import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata import software.aws.toolkits.jetbrains.settings.LspSettings import software.aws.toolkits.jetbrains.utils.notifyInfo import software.aws.toolkits.resources.message +import software.aws.toolkits.telemetry.Telemetry import java.io.IOException import java.io.OutputStreamWriter import java.io.PipedInputStream @@ -528,20 +530,36 @@ private class AmazonQServerInstance(private val project: Project, private val cs * may fail to start in that case. The caller should handle potential runtime initialization failures. */ private fun getNodeRuntimePath(nodePath: Path): Path { + val resolveNodeMetric = { isBundled: Boolean, success: Boolean -> + Telemetry.languageserver.setup.use { + it.id("q") + it.metadata("languageServerSetupStage", "resolveNode") + it.metadata("credentialStartUrl", getStartUrl(project)) + it.setAttribute("isBundledNode", isBundled) + it.success(success) + } + } + if (Files.exists(nodePath) && Files.isExecutable(nodePath)) { + resolveNodeMetric(true, true) return nodePath } + // use alternative node runtime if it is not found LOG.warn { "Node Runtime download failed. Fallback to user specified node runtime " } // attempt to use user provided node runtime path val nodeRuntime = LspSettings.getInstance().getNodeRuntimePath() if (!nodeRuntime.isNullOrEmpty()) { LOG.info { "Using node from $nodeRuntime " } + + resolveNodeMetric(false, true) return Path.of(nodeRuntime) } else { val localNode = locateNodeCommand() if (localNode != null) { LOG.info { "Using node from ${localNode.toAbsolutePath()}" } + + resolveNodeMetric(false, true) return localNode } notifyInfo( @@ -559,6 +577,8 @@ private class AmazonQServerInstance(private val project: Project, private val cs ) { _, notification -> notification.expire() } ) ) + + resolveNodeMetric(false, false) return nodePath } } diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt index 97f2db11fed..dc8d25235dc 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelper.kt @@ -17,7 +17,10 @@ import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.core.utils.info import software.aws.toolkits.core.utils.warn import software.aws.toolkits.jetbrains.core.saveFileFromUrl +import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl import software.aws.toolkits.resources.AwsCoreBundle +import software.aws.toolkits.telemetry.LanguageServerSetupStage +import software.aws.toolkits.telemetry.Telemetry import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths @@ -106,12 +109,12 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, } suspend fun tryDownloadLspArtifacts(project: Project, targetVersion: Version, target: VersionTarget): Path? { - val temporaryDownloadPath = Files.createTempDirectory("lsp-dl") - val downloadPath = lspArtifactsPath.resolve(targetVersion.serverVersion.toString()) + val destinationPath = lspArtifactsPath.resolve(targetVersion.serverVersion.toString()) while (currentAttempt.get() < maxDownloadAttempts) { currentAttempt.incrementAndGet() logger.info { "Attempt ${currentAttempt.get()} of $maxDownloadAttempts to download LSP artifacts" } + val temporaryDownloadPath = Files.createTempDirectory("lsp-dl") try { return withBackgroundProgress( @@ -119,20 +122,20 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, AwsCoreBundle.message("amazonqFeatureDev.placeholder.downloading_and_extracting_lsp_artifacts"), cancellable = true ) { - if (downloadLspArtifacts(temporaryDownloadPath, target) && !target.contents.isNullOrEmpty()) { - moveFilesFromSourceToDestination(temporaryDownloadPath, downloadPath) + if (downloadLspArtifacts(project, temporaryDownloadPath, target) && !target.contents.isNullOrEmpty()) { + moveFilesFromSourceToDestination(temporaryDownloadPath, destinationPath) target.contents .mapNotNull { it.filename } - .forEach { filename -> extractZipFile(downloadPath.resolve(filename), downloadPath) } - logger.info { "Successfully downloaded and moved LSP artifacts to $downloadPath" } + .forEach { filename -> extractZipFile(destinationPath.resolve(filename), destinationPath) } + logger.info { "Successfully downloaded and moved LSP artifacts to $destinationPath" } val thirdPartyLicenses = targetVersion.thirdPartyLicenses logger.info { - "Installing Amazon Q Language Server v${targetVersion.serverVersion} to: $downloadPath. " + + "Installing Amazon Q Language Server v${targetVersion.serverVersion} to: $destinationPath. " + if (thirdPartyLicenses == null) "" else "Attribution notice can be found at $thirdPartyLicenses" } - return@withBackgroundProgress downloadPath + return@withBackgroundProgress destinationPath } return@withBackgroundProgress null @@ -146,7 +149,7 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, else -> { logger.error(e) { "Failed to download/move LSP artifacts on attempt ${currentAttempt.get()}" } } } temporaryDownloadPath.toFile().deleteRecursively() - downloadPath.toFile().deleteRecursively() + destinationPath.toFile().deleteRecursively() } } logger.error { "Failed to download LSP artifacts after $maxDownloadAttempts attempts" } @@ -154,7 +157,7 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, } @VisibleForTesting - internal fun downloadLspArtifacts(downloadPath: Path, target: VersionTarget?): Boolean { + internal fun downloadLspArtifacts(project: Project, downloadPath: Path, target: VersionTarget?): Boolean { if (target == null || target.contents.isNullOrEmpty()) { logger.warn { "No target contents available for download" } return false @@ -171,7 +174,7 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, logger.warn { "No hash available for ${content.filename}" } return@forEach } - downloadAndValidateFile(content.url, filePath, contentHash) + downloadAndValidateFile(project, content.url, filePath, contentHash) } validateDownloadedFiles(downloadPath, target.contents) } catch (e: Exception) { @@ -182,18 +185,46 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, return true } - private fun downloadAndValidateFile(url: String, filePath: Path, expectedHash: String) { + private fun downloadAndValidateFile(project: Project, url: String, filePath: Path, expectedHash: String) { + val recordDownload = { runnable: () -> Unit -> + Telemetry.languageserver.setup.use { telemetry -> + telemetry.id("q") + telemetry.languageServerSetupStage(LanguageServerSetupStage.GetServer) + telemetry.metadata("credentialStartUrl", getStartUrl(project)) + telemetry.success(true) + + try { + runnable() + } catch (t: Throwable) { + telemetry.success(false) + telemetry.recordException(t) + } + } + } + try { if (!filePath.exists()) { logger.info { "Downloading file: ${filePath.fileName}" } - saveFileFromUrl(url, filePath, ProgressManager.getInstance().progressIndicator) + recordDownload { saveFileFromUrl(url, filePath, ProgressManager.getInstance().progressIndicator) } } if (!validateFileHash(filePath, expectedHash)) { logger.warn { "Hash mismatch for ${filePath.fileName}, re-downloading" } filePath.deleteIfExists() - saveFileFromUrl(url, filePath) - if (!validateFileHash(filePath, expectedHash)) { - throw LspException("Hash mismatch after re-download for ${filePath.fileName}", LspException.ErrorCode.HASH_MISMATCH) + recordDownload { saveFileFromUrl(url, filePath) } + + Telemetry.languageserver.setup.use { + it.id("q") + it.languageServerSetupStage(LanguageServerSetupStage.Validate) + it.metadata("credentialStartUrl", getStartUrl(project)) + it.success(true) + + if (!validateFileHash(filePath, expectedHash)) { + it.success(false) + + val exception = LspException("Hash mismatch after re-download for ${filePath.fileName}", LspException.ErrorCode.HASH_MISMATCH) + it.recordException(exception) + throw exception + } } } } catch (e: Exception) { diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactManager.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactManager.kt index b0a4d8e7e9a..d7970612854 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactManager.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactManager.kt @@ -19,6 +19,10 @@ import software.aws.toolkits.core.utils.info import software.aws.toolkits.core.utils.warn import software.aws.toolkits.jetbrains.AwsPlugin import software.aws.toolkits.jetbrains.AwsToolkit +import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl +import software.aws.toolkits.telemetry.LanguageServerSetupStage +import software.aws.toolkits.telemetry.MetricResult +import software.aws.toolkits.telemetry.Telemetry import java.nio.file.Path @Service @@ -57,42 +61,82 @@ class ArtifactManager @NonInjectable internal constructor(private val manifestFe return mutex.withLock { coroutineScope { async { - try { - val manifest = manifestFetcher.fetch() ?: throw LspException( - "Language Support is not available, as manifest is missing.", - LspException.ErrorCode.MANIFEST_FETCH_FAILED - ) - val lspVersions = getLSPVersionsFromManifestWithSpecifiedRange(manifest) - - artifactHelper.removeDelistedVersions(lspVersions.deListedVersions) - - if (lspVersions.inRangeVersions.isEmpty()) { - // No versions are found which are in the given range. Fallback to local lsp artifacts. - val localLspArtifacts = artifactHelper.getAllLocalLspArtifactsWithinManifestRange(DEFAULT_VERSION_RANGE) - if (localLspArtifacts.isNotEmpty()) { - return@async localLspArtifacts.first().first + Telemetry.languageserver.setup.use { all -> + all.id("q") + all.languageServerSetupStage(LanguageServerSetupStage.All) + all.metadata("credentialStartUrl", getStartUrl(project)) + all.result(MetricResult.Succeeded) + + try { + val lspVersions = Telemetry.languageserver.setup.use { telemetry -> + telemetry.id("q") + telemetry.languageServerSetupStage(LanguageServerSetupStage.GetManifest) + telemetry.metadata("credentialStartUrl", getStartUrl(project)) + + val exception = LspException( + "Language Support is not available, as manifest is missing.", + LspException.ErrorCode.MANIFEST_FETCH_FAILED + ) + telemetry.success(true) + val manifest = manifestFetcher.fetch() ?: run { + telemetry.recordException(exception) + telemetry.success(false) + throw exception + } + + getLSPVersionsFromManifestWithSpecifiedRange(manifest) } - throw LspException("Language server versions not found in manifest.", LspException.ErrorCode.NO_COMPATIBLE_LSP_VERSION) - } - val targetVersion = lspVersions.inRangeVersions.first() + artifactHelper.removeDelistedVersions(lspVersions.deListedVersions) + + if (lspVersions.inRangeVersions.isEmpty()) { + // No versions are found which are in the given range. Fallback to local lsp artifacts. + val localLspArtifacts = artifactHelper.getAllLocalLspArtifactsWithinManifestRange(DEFAULT_VERSION_RANGE) + if (localLspArtifacts.isNotEmpty()) { + return@async localLspArtifacts.first().first + } + throw LspException("Language server versions not found in manifest.", LspException.ErrorCode.NO_COMPATIBLE_LSP_VERSION) + } - // If there is an LSP Manifest with the same version - val target = getTargetFromLspManifest(targetVersion) - // Get Local LSP files and check if we can re-use existing LSP Artifacts - val artifactPath: Path = if (artifactHelper.getExistingLspArtifacts(targetVersion, target)) { - artifactHelper.getAllLocalLspArtifactsWithinManifestRange(DEFAULT_VERSION_RANGE).first().first - } else { - artifactHelper.tryDownloadLspArtifacts(project, targetVersion, target) - ?: throw LspException("Failed to download LSP artifacts", LspException.ErrorCode.DOWNLOAD_FAILED) + val targetVersion = lspVersions.inRangeVersions.first() + + // If there is an LSP Manifest with the same version + val target = getTargetFromLspManifest(targetVersion) + // Get Local LSP files and check if we can re-use existing LSP Artifacts + val artifactPath: Path = if (artifactHelper.getExistingLspArtifacts(targetVersion, target)) { + artifactHelper.getAllLocalLspArtifactsWithinManifestRange(DEFAULT_VERSION_RANGE).first().first + } else { + artifactHelper.tryDownloadLspArtifacts(project, targetVersion, target) + ?: throw LspException("Failed to download LSP artifacts", LspException.ErrorCode.DOWNLOAD_FAILED) + } + + artifactHelper.deleteOlderLspArtifacts(DEFAULT_VERSION_RANGE) + + Telemetry.languageserver.setup.use { + it.id("q") + it.languageServerSetupStage(LanguageServerSetupStage.Launch) + it.metadata("credentialStartUrl", getStartUrl(project)) + it.setAttribute("isBundledArtifact", false) + it.success(true) + } + return@async artifactPath + } catch (e: Exception) { + logger.warn(e) { "Failed to resolve assets from Flare CDN" } + val path = AwsToolkit.PLUGINS_INFO[AwsPlugin.Q]?.path?.resolve("flare") ?: error("not even bundled") + logger.info { "Falling back to bundled assets at $path" } + + all.recordException(e) + all.result(MetricResult.Failed) + + Telemetry.languageserver.setup.use { + it.id("q") + it.languageServerSetupStage(LanguageServerSetupStage.Launch) + it.metadata("credentialStartUrl", getStartUrl(project)) + it.setAttribute("isBundledArtifact", true) + it.success(false) + } + return@async path } - artifactHelper.deleteOlderLspArtifacts(DEFAULT_VERSION_RANGE) - return@async artifactPath - } catch (e: Exception) { - logger.warn(e) { "Failed to resolve assets from Flare CDN" } - val path = AwsToolkit.PLUGINS_INFO[AwsPlugin.Q]?.path?.resolve("flare") ?: error("not even bundled") - logger.info { "Falling back to bundled assets at $path" } - return@async path } } }.also { diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/AsyncChatUiListener.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/AsyncChatUiListener.kt index c6992526ec9..21d812cf075 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/AsyncChatUiListener.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/AsyncChatUiListener.kt @@ -3,10 +3,11 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat -import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.Project import com.intellij.util.messages.Topic import java.util.EventListener +@Deprecated("Why are we using a message bus for this????????") interface AsyncChatUiListener : EventListener { @Deprecated("shouldn't need this version") fun onChange(command: String) {} @@ -14,16 +15,17 @@ interface AsyncChatUiListener : EventListener { fun onChange(command: FlareUiMessage) {} companion object { - @Topic.AppLevel + @Topic.ProjectLevel val TOPIC = Topic.create("Partial chat message provider", AsyncChatUiListener::class.java) - fun notifyPartialMessageUpdate(command: FlareUiMessage) { - ApplicationManager.getApplication().messageBus.syncPublisher(TOPIC).onChange(command) + @Deprecated("Why are we using a message bus for this????????") + fun notifyPartialMessageUpdate(project: Project, command: FlareUiMessage) { + project.messageBus.syncPublisher(TOPIC).onChange(command) } @Deprecated("shouldn't need this version") - fun notifyPartialMessageUpdate(command: String) { - ApplicationManager.getApplication().messageBus.syncPublisher(TOPIC).onChange(command) + fun notifyPartialMessageUpdate(project: Project, command: String) { + project.messageBus.syncPublisher(TOPIC).onChange(command) } } } diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ChatCommunicationManager.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ChatCommunicationManager.kt index bd64d7336f8..ac7deaa4134 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ChatCommunicationManager.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ChatCommunicationManager.kt @@ -37,7 +37,7 @@ import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap @Service(Service.Level.PROJECT) -class ChatCommunicationManager(private val cs: CoroutineScope) { +class ChatCommunicationManager(private val project: Project, private val cs: CoroutineScope) { val uiReady = CompletableDeferred() private val chatPartialResultMap = ConcurrentHashMap() private val inflightRequestByTabId = ConcurrentHashMap>() @@ -53,7 +53,7 @@ class ChatCommunicationManager(private val cs: CoroutineScope) { fun notifyUi(uiMessage: FlareUiMessage) { cs.launch { uiReady.await() - AsyncChatUiListener.notifyPartialMessageUpdate(uiMessage) + AsyncChatUiListener.notifyPartialMessageUpdate(project, uiMessage) } } @@ -148,7 +148,7 @@ class ChatCommunicationManager(private val cs: CoroutineScope) { params = partialChatResult, isPartialResult = true ) - AsyncChatUiListener.notifyPartialMessageUpdate(uiMessage) + AsyncChatUiListener.notifyPartialMessageUpdate(project, uiMessage) finalResultProcessed[token] = true ChatAsyncResultManager.getInstance(project).setResult(token, partialResultMap) return @@ -169,7 +169,7 @@ class ChatCommunicationManager(private val cs: CoroutineScope) { params = partialChatResult, isPartialResult = true ) - AsyncChatUiListener.notifyPartialMessageUpdate(uiMessage) + AsyncChatUiListener.notifyPartialMessageUpdate(project, uiMessage) } } } diff --git a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelperTest.kt b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelperTest.kt index 367e2a31a97..72f3961fb58 100644 --- a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelperTest.kt +++ b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactHelperTest.kt @@ -199,7 +199,7 @@ class ArtifactHelperTest { val version = Version(serverVersion = "1.0.0") val spyArtifactHelper = spyk(artifactHelper) - every { spyArtifactHelper.downloadLspArtifacts(any(), any()) } returns false + every { spyArtifactHelper.downloadLspArtifacts(mockProject, any(), any()) } returns false assertThat(runBlocking { artifactHelper.tryDownloadLspArtifacts(mockProject, version, VersionTarget(contents = contents)) }).isEqualTo(null) } @@ -210,7 +210,7 @@ class ArtifactHelperTest { val target = VersionTarget(contents = contents) val spyArtifactHelper = spyk(artifactHelper) - every { spyArtifactHelper.downloadLspArtifacts(any(), any()) } returns true + every { spyArtifactHelper.downloadLspArtifacts(mockProject, any(), any()) } returns true mockkStatic("software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.LspUtilsKt") every { moveFilesFromSourceToDestination(any(), any()) } just Runs every { extractZipFile(any(), any()) } just Runs diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt index 623663d85db..22cf1231e9b 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/telemetry/otel/OtelBase.kt @@ -201,6 +201,9 @@ abstract class AbstractBaseSpan>(internal override fun recordException(exception: Throwable): SpanType { delegate.recordException(exception) + + setAttribute("reason", exception::class.java.canonicalName) + setAttribute("reasonDesc", exception.message) return this as SpanType } 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 0871d6258f5..9f769d120ab 100644 --- a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties +++ b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties @@ -1656,7 +1656,7 @@ q.session_configuration=Extend your IDE sessions q.session_configuration.description=Your maximum session length for Amazon Q can be extended to 90 days by your administrator. For more information, refer to How to extend the session duration for Amazon Q in the IDE in the IAM Identity Center User Guide. q.sign.in=Get Started q.ui.prompt.transform=/transform -q.unavailable=\ Not supported in v2023.2.0 +q.unavailable=\ Amazon Q Chat is not supported in IDE versions <= v2024.2.1 q.unavailable.node=Please update to the latest IDE version q.window.title=Amazon Q Chat rds.aurora=Aurora