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/build.gradle.kts b/plugins/amazonq/build.gradle.kts index 69357ce5dc1..39bbdd368fa 100644 --- a/plugins/amazonq/build.gradle.kts +++ b/plugins/amazonq/build.gradle.kts @@ -1,6 +1,10 @@ // Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import de.undercouch.gradle.tasks.download.Download +import org.jetbrains.intellij.platform.gradle.tasks.PrepareSandboxTask import software.aws.toolkits.gradle.changelog.tasks.GeneratePluginChangeLog plugins { @@ -8,6 +12,13 @@ plugins { id("toolkit-publish-root-conventions") id("toolkit-jvm-conventions") id("toolkit-testing") + id("de.undercouch.download") +} + +buildscript { + dependencies { + classpath(libs.bundles.jackson) + } } val changelog = tasks.register("pluginChangeLog") { @@ -47,3 +58,81 @@ tasks.check { } } } + +val downloadFlareManifest by tasks.registering(Download::class) { + src("https://aws-toolkit-language-servers.amazonaws.com/qAgenticChatServer/0/manifest.json") + dest(layout.buildDirectory.file("flare/manifest.json")) + onlyIfModified(true) + useETag(true) +} + +data class FlareManifest( + val versions: List, +) + +data class FlareVersion( + val serverVersion: String, + val thirdPartyLicenses: String, + val targets: List, +) + +data class FlareTarget( + val platform: String, + val arch: String, + val contents: List +) + +data class FlareContent( + val url: String, +) + +val downloadFlareArtifacts by tasks.registering(Download::class) { + dependsOn(downloadFlareManifest) + inputs.files(downloadFlareManifest) + + val manifestFile = downloadFlareManifest.map { it.outputFiles.first() } + val manifest = manifestFile.map { jacksonObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).readValue(it.readText(), FlareManifest::class.java) } + + // use darwin-aarch64 because its the smallest and we're going to throw away everything platform specific + val latest = manifest.map { it.versions.first() } + val latestVersion = latest.map { it.serverVersion } + val licensesUrl = latest.map { it.thirdPartyLicenses } + val darwin = latest.map { it.targets.first { target -> target.platform == "darwin" && target.arch == "arm64" } } + val contentUrls = darwin.map { it.contents.map { content -> content.url } } + + val destination = layout.buildDirectory.dir(latestVersion.map { "flare/$it" }) + outputs.dir(destination) + + src(contentUrls.zip(licensesUrl) { left, right -> left + right}) + dest(destination) + onlyIfModified(true) + useETag(true) +} + +val prepareBundledFlare by tasks.registering(Copy::class) { + dependsOn(downloadFlareArtifacts) + inputs.files(downloadFlareArtifacts) + + val dest = layout.buildDirectory.dir("tmp/extractFlare") + into(dest) + from(downloadFlareArtifacts.map { it.outputFiles.filterNot { file -> file.name.endsWith(".zip") } }) + + doLast { + copy { + into(dest) + includeEmptyDirs = false + downloadFlareArtifacts.get().outputFiles.filter { it.name.endsWith(".zip") }.forEach { + dest.get().file(it.parentFile.name).asFile.createNewFile() + from(zipTree(it)) { + include("*.js") + include("*.txt") + } + } + } + } +} + +tasks.withType().configureEach { + intoChild(intellijPlatform.projectName.map { "$it/flare" }) + .from(prepareBundledFlare) +} diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QRefreshPanelAction.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QRefreshPanelAction.kt index 84ba01c0e4c..14b551f25b6 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QRefreshPanelAction.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/QRefreshPanelAction.kt @@ -9,6 +9,9 @@ import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.project.DumbAwareAction import com.intellij.util.messages.Topic +import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService +import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.ChatCommunicationManager +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_TAB_REMOVE import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindow import software.aws.toolkits.resources.AmazonQBundle import java.util.EventListener @@ -16,6 +19,15 @@ import java.util.EventListener class QRefreshPanelAction : DumbAwareAction(AmazonQBundle.message("amazonq.refresh.panel"), null, AllIcons.Actions.Refresh) { override fun actionPerformed(e: AnActionEvent) { val project = e.project ?: return + + // Notify LSP server about all open tabs being removed + val chatManager = ChatCommunicationManager.getInstance(project) + chatManager.getAllTabIds().forEach { tabId -> + AmazonQLspService.executeIfRunning(project) { server -> + rawEndpoint.notify(CHAT_TAB_REMOVE, mapOf("tabId" to tabId)) + } + } + // recreate chat browser AmazonQToolWindow.getInstance(project).disposeAndRecreate() // recreate signin browser 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 3bdeda97713..028dca4e1d8 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.artifacts.ArtifactManager import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.AsyncChatUiListener @@ -43,6 +44,7 @@ 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.jetbrains.utils.isRunningOnRemoteBackend +import software.aws.toolkits.resources.message import java.util.concurrent.CompletableFuture import javax.swing.JButton @@ -103,6 +105,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 = if (isRunningOnRemoteBackend()) { JBLoadingPanel(null) { 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..5c0dbc67bd3 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"))) @@ -307,14 +307,19 @@ class BrowserConnector( } CHAT_TAB_ADD -> { - handleChat(AmazonQChatServer.tabAdd, node) + handleChat(AmazonQChatServer.tabAdd, node) { params, invoke -> + // Track the tab ID when a tab is added + chatCommunicationManager.addTabId(params.tabId) + invoke() + } } CHAT_TAB_REMOVE -> { handleChat(AmazonQChatServer.tabRemove, node) { params, invoke -> chatCommunicationManager.removePartialChatMessage(params.tabId) cancelInflightRequests(params.tabId) - + // Remove the tab ID from tracking when a tab is removed + chatCommunicationManager.removeTabId(params.tabId) invoke() } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/AmazonQTheme.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/AmazonQTheme.kt index 33a6e534927..bbf449eac59 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/AmazonQTheme.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/AmazonQTheme.kt @@ -16,6 +16,8 @@ data class AmazonQTheme( val defaultText: Color, val inactiveText: Color, val linkText: Color, + val lightText: Color, + val emptyText: Color, val background: Color, val border: Color, @@ -31,6 +33,8 @@ data class AmazonQTheme( val buttonBackground: Color, val secondaryButtonForeground: Color, val secondaryButtonBackground: Color, + val inputBorderFocused: Color, + val inputBorderUnfocused: Color, val info: Color, val success: Color, diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/CssVariable.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/CssVariable.kt index 997423c0300..793a75052fb 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/CssVariable.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/CssVariable.kt @@ -16,6 +16,7 @@ enum class CssVariable( TextColorAlt("--mynah-color-text-alternate"), TextColorStrong("--mynah-color-text-strong"), TextColorWeak("--mynah-color-text-weak"), + TextColorLight("--mynah-color-light"), TextColorLink("--mynah-color-text-link"), TextColorInput("--mynah-color-text-input"), TextColorDisabled("--mynah-color-text-disabled"), @@ -27,6 +28,8 @@ enum class CssVariable( ColorDeep("--mynah-color-deep"), ColorDeepReverse("--mynah-color-deep-reverse"), BorderDefault("--mynah-color-border-default"), + BorderFocused("--mynah-color-text-input-border-focused"), + BorderUnfocused("--mynah-color-text-input-border"), InputBackground("--mynah-input-bg"), SyntaxBackground("--mynah-color-syntax-bg"), diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/EditorThemeAdapter.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/EditorThemeAdapter.kt index 2ca89145e6e..a845e3c19f3 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/EditorThemeAdapter.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/EditorThemeAdapter.kt @@ -124,6 +124,10 @@ class EditorThemeAdapter { editorString = currentScheme.foregroundColor(DefaultLanguageHighlighterColors.STRING), editorProperty = currentScheme.foregroundColor(DefaultLanguageHighlighterColors.INSTANCE_FIELD), editorClassName = currentScheme.foregroundColor(DefaultLanguageHighlighterColors.CLASS_NAME), + lightText = themeColor("TextField.inactiveForeground", default = 0xA8ADBD, darkDefault = 0x5A5D63), + emptyText = themeColor("TextField.inactiveForeground", default = 0xA8ADBD, darkDefault = 0x5A5D63), + inputBorderFocused = themeColor("ActionButton.focusedBorderColor", default = 0x4682FA, darkDefault = 0x3574f0), + inputBorderUnfocused = themeColor("TextField.borderColor", default = 0xEBECF0, darkDefault = 0x4E5157), ) } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/ThemeBrowserAdapter.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/ThemeBrowserAdapter.kt index e891fa34e65..d0f43958664 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/ThemeBrowserAdapter.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/theme/ThemeBrowserAdapter.kt @@ -40,7 +40,8 @@ class ThemeBrowserAdapter { append(CssVariable.TextColorStrong, theme.textFieldForeground) append(CssVariable.TextColorInput, theme.textFieldForeground) append(CssVariable.TextColorLink, theme.linkText) - append(CssVariable.TextColorWeak, theme.inactiveText) + append(CssVariable.TextColorWeak, theme.emptyText) + append(CssVariable.TextColorLight, theme.emptyText) append(CssVariable.TextColorDisabled, theme.inactiveText) append(CssVariable.Background, bg) @@ -48,6 +49,8 @@ class ThemeBrowserAdapter { append(CssVariable.CardBackground, bg) append(CssVariable.CardBackgroundAlt, altBg) append(CssVariable.BorderDefault, theme.border) + append(CssVariable.BorderFocused, theme.inputBorderFocused) + append(CssVariable.BorderUnfocused, theme.inputBorderUnfocused) append(CssVariable.TabActive, theme.activeTab) append(CssVariable.InputBackground, inputBg) 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/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 e03521841da..4a8d97575a5 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 @@ -83,6 +83,21 @@ class CodeWhispererConfigurable(private val project: Project) : .resizableColumn() .align(Align.FILL) } + row(message("amazonqFeatureDev.placeholder.node_runtime_path")) { + val fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFileDescriptor() + fileChooserDescriptor.isForcedToUseIdeaFileChooser = true + + textFieldWithBrowseButton(fileChooserDescriptor = fileChooserDescriptor) + .bindText( + { LspSettings.getInstance().getNodeRuntimePath().orEmpty() }, + { LspSettings.getInstance().setNodeRuntimePath(it) } + ) + .applyToComponent { + emptyText.text = message("executableCommon.auto_managed") + } + .resizableColumn() + .align(Align.FILL) + } } group(message("aws.settings.codewhisperer.group.general")) { diff --git a/plugins/amazonq/shared/jetbrains-community/build.gradle.kts b/plugins/amazonq/shared/jetbrains-community/build.gradle.kts index 315356836b5..fb011ad3990 100644 --- a/plugins/amazonq/shared/jetbrains-community/build.gradle.kts +++ b/plugins/amazonq/shared/jetbrains-community/build.gradle.kts @@ -24,3 +24,12 @@ dependencies { testFixturesApi(testFixtures(project(":plugin-core:jetbrains-community"))) } + +// hack because our test structure currently doesn't make complete sense +tasks.prepareTestSandbox { + val pluginXmlJar = project(":plugin-amazonq").tasks.jar + + dependsOn(pluginXmlJar) + intoChild(intellijPlatform.projectName.map { "$it/lib" }) + .from(pluginXmlJar) +} 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/AmazonQLanguageClient.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClient.kt index 90e7eb49978..c5141c4446b 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClient.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClient.kt @@ -8,6 +8,7 @@ import org.eclipse.lsp4j.jsonrpc.services.JsonRequest import org.eclipse.lsp4j.services.LanguageClient import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.LSPAny import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_OPEN_TAB +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_OPTIONS_UPDATE_NOTIFICATION import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_SEND_CONTEXT_COMMANDS import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_SEND_UPDATE import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CopyFileParams @@ -67,4 +68,7 @@ interface AmazonQLanguageClient : LanguageClient { @JsonNotification(DID_CREATE_DIRECTORY) fun createDirectory(params: FileParams) + + @JsonNotification(CHAT_OPTIONS_UPDATE_NOTIFICATION) + fun sendChatOptionsUpdate(params: LSPAny) } 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 7b33a57eb86..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 @@ -41,6 +41,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.ChatCommun import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.FlareUiMessage import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.LSPAny import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_OPEN_TAB +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_OPTIONS_UPDATE_NOTIFICATION import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_SEND_CONTEXT_COMMANDS import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_SEND_UPDATE import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CopyFileParams @@ -149,26 +150,29 @@ class AmazonQLanguageClientImpl(private val project: Project) : AmazonQLanguageC return CompletableFuture.completedFuture(ShowDocumentResult(false)) } - // 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()) if (params.external == true) { - BrowserUtil.open(fileToOpen) + BrowserUtil.open(params.uri) return CompletableFuture.completedFuture(ShowDocumentResult(true)) } - 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" } - } - } + // 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()) + 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)) @@ -311,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, @@ -413,6 +418,16 @@ class AmazonQLanguageClientImpl(private val project: Project) : AmazonQLanguageC return refreshVfs(params.newPath) } + override fun sendChatOptionsUpdate(params: LSPAny) { + val chatManager = ChatCommunicationManager.getInstance(project) + chatManager.notifyUi( + FlareUiMessage( + command = CHAT_OPTIONS_UPDATE_NOTIFICATION, + params = params, + ) + ) + } + private fun refreshVfs(path: String) { val currPath = Paths.get(path) if (currPath.startsWith(localHistoryPath)) return 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 c2a0dd461dc..28c28100f97 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 @@ -5,16 +5,19 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp import com.google.gson.ToNumberPolicy import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.configurations.PathEnvironmentVariableUtil import com.intellij.execution.impl.ExecutionManagerImpl import com.intellij.execution.process.KillableColoredProcessHandler import com.intellij.execution.process.KillableProcessHandler import com.intellij.execution.process.ProcessEvent import com.intellij.execution.process.ProcessListener import com.intellij.execution.process.ProcessOutputType +import com.intellij.notification.NotificationAction import com.intellij.openapi.Disposable import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.components.serviceIfCreated +import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Key @@ -74,8 +77,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 @@ -86,7 +93,9 @@ import java.net.Proxy import java.net.URI import java.nio.charset.StandardCharsets import java.nio.file.Files +import java.nio.file.Path import java.util.concurrent.Future +import java.util.concurrent.TimeUnit import kotlin.time.Duration.Companion.seconds // https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/server/LSPProcessListener.java @@ -364,22 +373,30 @@ private class AmazonQServerInstance(private val project: Project, private val cs // make assumption that all requests will resolve to the same CA // also terrible assumption that default endpoint is reachable val qUri = URI(QDefaultServiceConfig.ENDPOINT) - val rtsTrustChain = TrustChainUtil.getTrustChain(qUri) - val extraCaCerts = Files.createTempFile("q-extra-ca", ".pem").apply { - writeText( - TrustChainUtil.certsToPem(rtsTrustChain) - ) + val extraCaCerts = try { + val rtsTrustChain = TrustChainUtil.getTrustChain(qUri) + + Files.createTempFile("q-extra-ca", ".pem").apply { + writeText( + TrustChainUtil.certsToPem(rtsTrustChain) + ) + } + } catch (e: Exception) { + LOG.info(e) { "Could not resolve trust chain for $qUri, skipping NODE_EXTRA_CA_CERTS" } + null } val node = if (SystemInfo.isWindows) "node.exe" else "node" - val cmd = NodeExePatcher.patch(artifact.resolve(node)) + val nodePath = getNodeRuntimePath(artifact.resolve(node)) + + val cmd = NodeExePatcher.patch(nodePath) .withParameters( LspSettings.getInstance().getArtifactPath() ?: artifact.resolve("aws-lsp-codewhisperer.js").toString(), "--stdio", "--set-credentials-encryption-key", ).withEnvironment( buildMap { - put("NODE_EXTRA_CA_CERTS", extraCaCerts.toAbsolutePath().toString()) + extraCaCerts?.let { put("NODE_EXTRA_CA_CERTS", it.toAbsolutePath().toString()) } val proxy = JdkProxyProvider.getInstance().proxySelector.select(qUri) // log if only socks proxy available @@ -492,6 +509,121 @@ private class AmazonQServerInstance(private val project: Project, private val cs } } + /** + * Resolves the path to a valid Node.js runtime in the following order of preference: + * 1. Uses the provided nodePath if it exists and is executable + * 2. Uses user-specified runtime path from LSP settings if available + * 3. Uses system Node.js if version 18+ is available + * 4. Falls back to original nodePath with a notification to configure runtime + * + * @param nodePath The initial Node.js runtime path to check, typically from the artifact directory + * @return Path The resolved Node.js runtime path to use for the LSP server + * + * Side effects: + * - Logs warnings if initial runtime path is invalid + * - Logs info when using alternative runtime path + * - Shows notification to user if no valid Node.js runtime is found + * + * Note: The function will return a path even if no valid runtime is found, but the LSP server + * 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( + "Amazon Q", + message("amazonqFeatureDev.placeholder.node_runtime_message"), + project = project, + listOf( + NotificationAction.create( + message("codewhisperer.actions.open_settings.title") + ) { _, notification -> + ShowSettingsUtil.getInstance().showSettingsDialog(project, message("aws.settings.codewhisperer.configurable.title")) + }, + NotificationAction.create( + message("codewhisperer.notification.custom.simple.button.got_it") + ) { _, notification -> notification.expire() } + ) + ) + + resolveNodeMetric(false, false) + return nodePath + } + } + + /** + * Locates node executable ≥18 in system PATH. + * Uses IntelliJ's PathEnvironmentVariableUtil to find executables. + * + * @return Path? The absolute path to node ≥18 if found, null otherwise + */ + private fun locateNodeCommand(): Path? { + val exeName = if (SystemInfo.isWindows) "node.exe" else "node" + + return PathEnvironmentVariableUtil.findAllExeFilesInPath(exeName) + .asSequence() + .map { it.toPath() } + .filter { Files.isRegularFile(it) && Files.isExecutable(it) } + .firstNotNullOfOrNull { path -> + try { + val process = ProcessBuilder(path.toString(), "--version") + .redirectErrorStream(true) + .start() + + if (!process.waitFor(5, TimeUnit.SECONDS)) { + process.destroy() + null + } else if (process.exitValue() == 0) { + val version = process.inputStream.bufferedReader().readText().trim() + val majorVersion = version.removePrefix("v").split(".")[0].toIntOrNull() + + if (majorVersion != null && majorVersion >= 18) { + path.toAbsolutePath() + } else { + LOG.debug { "Node version < 18 found at: $path (version: $version)" } + null + } + } else { + LOG.debug { "Failed to get version from node at: $path" } + null + } + } catch (e: Exception) { + LOG.debug(e) { "Failed to check version for node at: $path" } + null + } + } + } + override fun dispose() { if (!launcherFuture.isDone) { try { 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 8fc8d068348..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 @@ -16,6 +16,13 @@ import org.jetbrains.annotations.VisibleForTesting 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.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 @@ -54,36 +61,83 @@ class ArtifactManager @NonInjectable internal constructor(private val manifestFe return mutex.withLock { coroutineScope { async { - 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) + } + + 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) + } + + 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 } - throw LspException("Language server versions not found in manifest.", LspException.ErrorCode.NO_COMPATIBLE_LSP_VERSION) } - - 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) - return@async artifactPath } }.also { artifactDeferred = it 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..a93f7c64297 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>() @@ -45,6 +45,7 @@ class ChatCommunicationManager(private val cs: CoroutineScope) { private val pendingTabRequests = ConcurrentHashMap>() private val partialResultLocks = ConcurrentHashMap() private val finalResultProcessed = ConcurrentHashMap() + private val openTabs = mutableSetOf() fun setUiReady() { uiReady.complete(true) @@ -53,7 +54,7 @@ class ChatCommunicationManager(private val cs: CoroutineScope) { fun notifyUi(uiMessage: FlareUiMessage) { cs.launch { uiReady.await() - AsyncChatUiListener.notifyPartialMessageUpdate(uiMessage) + AsyncChatUiListener.notifyPartialMessageUpdate(project, uiMessage) } } @@ -80,6 +81,24 @@ class ChatCommunicationManager(private val cs: CoroutineScope) { fun removePartialChatMessage(partialResultToken: String) = chatPartialResultMap.remove(partialResultToken) + fun addTabId(tabId: String) { + synchronized(openTabs) { + openTabs.add(tabId) + } + } + + fun removeTabId(tabId: String) { + synchronized(openTabs) { + openTabs.remove(tabId) + } + } + + fun getAllTabIds(): Set { + synchronized(openTabs) { + return openTabs.toSet() + } + } + fun addSerializedChatRequest(requestId: String, result: CompletableFuture) { pendingSerializedChatRequests[requestId] = result } @@ -148,7 +167,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 +188,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/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/FlareChatCommands.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/FlareChatCommands.kt index d964435c9eb..3c734325f01 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/FlareChatCommands.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/FlareChatCommands.kt @@ -20,6 +20,7 @@ const val CHAT_INSERT_TO_CURSOR_NOTIFICATION = "aws/chat/insertToCursorPosition" const val CHAT_LINK_CLICK = "aws/chat/linkClick" const val CHAT_LIST_CONVERSATIONS = "aws/chat/listConversations" const val CHAT_OPEN_TAB = "aws/chat/openTab" +const val CHAT_OPTIONS_UPDATE_NOTIFICATION = "aws/chat/chatOptionsUpdate" const val CHAT_PROMPT_OPTION_ACKNOWLEDGED = "chatPromptOptionAcknowledged" const val CHAT_QUICK_ACTION = "aws/chat/sendChatQuickAction" const val CHAT_READY = "aws/chat/ready" diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/LspSettings.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/LspSettings.kt index 61060774e79..bc199de4917 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/LspSettings.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/LspSettings.kt @@ -25,10 +25,16 @@ class LspSettings : PersistentStateComponent { fun getArtifactPath() = state.artifactPath + fun getNodeRuntimePath() = state.nodeRuntimePath + fun setArtifactPath(artifactPath: String?) { state.artifactPath = artifactPath.nullize(nullizeSpaces = true) } + fun setNodeRuntimePath(nodeRuntimePath: String?) { + state.nodeRuntimePath = nodeRuntimePath.nullize(nullizeSpaces = true) + } + companion object { fun getInstance(): LspSettings = service() } @@ -36,4 +42,5 @@ class LspSettings : PersistentStateComponent { class LspConfiguration : BaseState() { var artifactPath by string() + var nodeRuntimePath by string() } 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/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactManagerTest.kt b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactManagerTest.kt index b97625e994e..de364834b9c 100644 --- a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactManagerTest.kt +++ b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/ArtifactManagerTest.kt @@ -3,6 +3,8 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts +import com.intellij.ide.plugins.PluginManagerCore +import com.intellij.openapi.extensions.PluginId import com.intellij.testFramework.ProjectExtension import com.intellij.util.text.SemVer import io.mockk.Runs @@ -17,7 +19,6 @@ import kotlinx.coroutines.test.runTest import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.extension.RegisterExtension import org.junit.jupiter.api.io.TempDir import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.ArtifactManager.SupportedManifestVersionRange @@ -51,29 +52,27 @@ class ArtifactManagerTest { } @Test - fun `fetch artifact fetcher throws exception if manifest is null`() = runTest { + fun `fetch artifact fetcher returns bundled if manifest is null`() = runTest { every { manifestFetcher.fetch() }.returns(null) - val exception = assertThrows { - artifactManager.fetchArtifact(projectExtension.project) - } - assertThat(exception) - .hasFieldOrPropertyWithValue("errorCode", LspException.ErrorCode.MANIFEST_FETCH_FAILED) + assertThat(artifactManager.fetchArtifact(projectExtension.project)) + .isEqualTo( + PluginManagerCore.getPlugin(PluginId.getId("amazon.q"))?.pluginPath?.resolve("flare") + ) } @Test - fun `fetch artifact does not have any valid lsp versions`() = runTest { + fun `fetch artifact does not have any valid lsp versions returns bundled`() = runTest { every { manifestFetcher.fetch() }.returns(Manifest()) every { artifactManager.getLSPVersionsFromManifestWithSpecifiedRange(any()) }.returns( ArtifactManager.LSPVersions(deListedVersions = emptyList(), inRangeVersions = emptyList()) ) - val exception = assertThrows { - artifactManager.fetchArtifact(projectExtension.project) - } - assertThat(exception) - .hasFieldOrPropertyWithValue("errorCode", LspException.ErrorCode.NO_COMPATIBLE_LSP_VERSION) + assertThat(artifactManager.fetchArtifact(projectExtension.project)) + .isEqualTo( + PluginManagerCore.getPlugin(PluginId.getId("amazon.q"))?.pluginPath?.resolve("flare") + ) } @Test diff --git a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/AwsToolkit.kt b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/AwsToolkit.kt index 883c64a9051..350dcb9442b 100644 --- a/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/AwsToolkit.kt +++ b/plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/AwsToolkit.kt @@ -4,11 +4,9 @@ package software.aws.toolkits.jetbrains import com.intellij.ide.plugins.PluginManagerCore -import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.extensions.PluginDescriptor import com.intellij.openapi.extensions.PluginId import java.nio.file.Path -import java.nio.file.Paths import java.util.EnumMap object AwsToolkit { @@ -37,12 +35,7 @@ data class PluginInfo(val id: String, val name: String) { val version: String? get() = descriptor?.version val path: Path? - get() = - if (ApplicationManager.getApplication().isUnitTestMode) { - Paths.get(System.getProperty("plugin.path")) - } else { - descriptor?.pluginPath - } + get() = descriptor?.pluginPath } enum class AwsPlugin { 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 d50132769c1..9f769d120ab 100644 --- a/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties +++ b/plugins/core/resources/resources/software/aws/toolkits/resources/MessagesBundle.properties @@ -146,6 +146,8 @@ amazonqFeatureDev.placeholder.downloading_and_extracting_lsp_artifacts=Downloadi amazonqFeatureDev.placeholder.generating_code=Generating code... amazonqFeatureDev.placeholder.lsp=LSP amazonqFeatureDev.placeholder.new_plan=Describe your task or issue in as much detail as possible +amazonqFeatureDev.placeholder.node_runtime_message=Please provide the absolute path of your node js v18+ runtime executable in Settings. Re-open IDE to apply this change. +amazonqFeatureDev.placeholder.node_runtime_path=Node Runtime Path amazonqFeatureDev.placeholder.provide_code_feedback=Provide feedback or comments amazonqFeatureDev.placeholder.select_lsp_artifact=Select LSP Artifact amazonqFeatureDev.placeholder.write_new_prompt=Write a new prompt @@ -1654,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