From fe7738d799f0a5cf105a669df7b0d87089ed2f90 Mon Sep 17 00:00:00 2001 From: Manodnya Bhoite Date: Wed, 9 Apr 2025 12:07:57 -0700 Subject: [PATCH 01/13] Set up Flare chat connection --- .../amazonq/toolwindow/AmazonQToolWindow.kt | 12 +- .../services/amazonq/webview/Browser.kt | 106 ++++++++++++++++-- .../amazonq/webview/BrowserConnector.kt | 73 +++++++++++- .../amazonq/lsp/AmazonQLanguageClientImpl.kt | 14 +++ .../amazonq/lsp/AmazonQLanguageServer.kt | 4 + .../services/amazonq/lsp/AmazonQLspService.kt | 4 +- .../amazonq/lsp/artifacts/ArtifactHelper.kt | 10 ++ .../lsp/flareChat/AsyncChatUiListener.kt | 21 ++++ .../lsp/flareChat/ChatCommunicationManager.kt | 70 ++++++++++++ .../amazonq/lsp/flareChat/ChatEditorUtils.kt | 15 +++ .../flareChat/ProgressNotificationUtils.kt | 30 +++++ .../amazonq/lsp/model/aws/chat/ChatPrompt.kt | 26 +++++ .../amazonq/lsp/model/aws/chat/ChatResult.kt | 54 +++++++++ .../amazonq/lsp/model/aws/chat/CursorState.kt | 10 ++ .../lsp/model/aws/chat/SendChatPrompt.kt | 18 +++ 15 files changed, 452 insertions(+), 15 deletions(-) create mode 100644 plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/AsyncChatUiListener.kt create mode 100644 plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ChatCommunicationManager.kt create mode 100644 plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ChatEditorUtils.kt create mode 100644 plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ProgressNotificationUtils.kt create mode 100644 plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/ChatPrompt.kt create mode 100644 plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/ChatResult.kt create mode 100644 plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/CursorState.kt create mode 100644 plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/SendChatPrompt.kt diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt index 15a533d1191..0a6ddd40229 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt @@ -20,6 +20,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel 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.lsp.flareChat.AsyncChatUiListener import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage import software.aws.toolkits.jetbrains.services.amazonq.messages.MessageConnector import software.aws.toolkits.jetbrains.services.amazonq.onboarding.OnboardingPageInteraction @@ -42,7 +43,7 @@ class AmazonQToolWindow private constructor( private val scope: CoroutineScope, ) : Disposable { private val appSource = AppSource() - private val browserConnector = BrowserConnector() + private val browserConnector = BrowserConnector(project = project) private val editorThemeAdapter = EditorThemeAdapter() private val chatPanel = AmazonQPanel(parent = this) @@ -55,6 +56,15 @@ class AmazonQToolWindow private constructor( initConnections() connectUi() connectApps() + + project.messageBus.connect().subscribe( + AsyncChatUiListener.TOPIC, + object : AsyncChatUiListener { + override fun onChange(message: String) { + chatPanel.browser?.postChat(message) + } + } + ) } fun disposeAndRecreate() { diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt index cc39985dd36..cd375e3ed60 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt @@ -8,6 +8,7 @@ import com.intellij.openapi.Disposable import com.intellij.openapi.util.Disposer import com.intellij.ui.jcef.JBCefJSQuery import org.cef.CefApp +import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.ArtifactHelper import software.aws.toolkits.jetbrains.services.amazonq.util.HighlightCommand import software.aws.toolkits.jetbrains.services.amazonq.util.createBrowser import software.aws.toolkits.jetbrains.settings.MeetQSettings @@ -46,6 +47,13 @@ class Browser(parent: Disposable) : Disposable { fun component() = jcefBrowser.component + fun postChat(message: String) { + jcefBrowser + .cefBrowser + .executeJavaScript("window.postMessage($message)", jcefBrowser.cefBrowser.url, 0) + } + + // TODO: Remove this once chat has been integrated with agents fun post(message: String) = jcefBrowser .cefBrowser @@ -82,32 +90,86 @@ class Browser(parent: Disposable) : Disposable { highlightCommand: HighlightCommand?, ): String { val postMessageToJavaJsCode = receiveMessageQuery.inject("JSON.stringify(message)") - val jsScripts = """ """.trimIndent() + addQuickActionCommands(isCodeTransformAvailable, isFeatureDevAvailable, isDocAvailable, isCodeTestAvailable, isCodeScanAvailable, highlightCommand) return """ + AWS Q @@ -119,8 +181,28 @@ class Browser(parent: Disposable) : Disposable { """.trimIndent() } + fun addQuickActionCommands( + isCodeTransformAvailable: Boolean, + isFeatureDevAvailable: Boolean, + isDocAvailable: Boolean, + isCodeTestAvailable: Boolean, + isCodeScanAvailable: Boolean, + highlightCommand: HighlightCommand?, + ) { + // TODO: Remove this once chat has been integrated with agents. This is added temporarily to keep detekt happy. + isCodeScanAvailable + isCodeTestAvailable + isDocAvailable + isFeatureDevAvailable + isCodeTransformAvailable + MAX_ONBOARDING_PAGE_COUNT + OBJECT_MAPPER + highlightCommand + } + companion object { - private const val WEB_SCRIPT_URI = "http://mynah/js/mynah-ui.js" + // TODO: Switch this to respect the overriden paths too + private val WEB_SCRIPT_URI = ArtifactHelper().getLatestLocalLspArtifact().resolve("amazonq-ui.js").toUri() private const val MAX_ONBOARDING_PAGE_COUNT = 3 private val OBJECT_MAPPER = jacksonObjectMapper() } 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 592c588550e..8c90b65b9ec 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 @@ -3,8 +3,11 @@ package software.aws.toolkits.jetbrains.services.amazonq.webview +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.intellij.ide.BrowserUtil import com.intellij.ide.util.RunOnceUtil +import com.intellij.openapi.project.Project import com.intellij.ui.jcef.JBCefJSQuery.Response import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.channels.awaitClose @@ -17,8 +20,19 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.cef.browser.CefBrowser +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.Range import software.aws.toolkits.jetbrains.services.amazonq.apps.AppConnection import software.aws.toolkits.jetbrains.services.amazonq.commands.MessageSerializer +import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService +import software.aws.toolkits.jetbrains.services.amazonq.lsp.encryption.JwtEncryptionManager +import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.ChatCommunicationManager +import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.getTextDocumentIdentifier +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ChatParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ChatPrompt +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CursorState +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.EncryptedChatParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SendChatPromptRequest import software.aws.toolkits.jetbrains.services.amazonq.util.command import software.aws.toolkits.jetbrains.services.amazonq.util.tabType import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.AmazonQTheme @@ -26,13 +40,16 @@ import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.ThemeBrows import software.aws.toolkits.jetbrains.settings.MeetQSettings import software.aws.toolkits.telemetry.MetricResult import software.aws.toolkits.telemetry.Telemetry +import java.util.concurrent.CompletableFuture import java.util.function.Function class BrowserConnector( private val serializer: MessageSerializer = MessageSerializer.getInstance(), private val themeBrowserAdapter: ThemeBrowserAdapter = ThemeBrowserAdapter(), + private val project: Project, ) { var uiReady = CompletableDeferred() + val chatCommunicationManager = ChatCommunicationManager.getInstance(project) suspend fun connect( browser: Browser, @@ -77,7 +94,10 @@ class BrowserConnector( } } - val tabType = node.tabType ?: return@onEach + val tabType = node.tabType + if (tabType == null) { + handleFlareChatMessages(browser, node) + } connections.filter { connection -> connection.app.tabTypes.contains(tabType) }.forEach { connection -> launch { val message = serializer.deserialize(node, connection.messageTypeRegistry) @@ -123,4 +143,55 @@ class BrowserConnector( browser.receiveMessageQuery.removeHandler(handler) } } + + private suspend fun handleFlareChatMessages(browser: Browser, node: JsonNode) { + when (node.command) { + "aws/chat/sendChatPrompt" -> { + val requestFromUi = jacksonObjectMapper().readValue(node.toString(), SendChatPromptRequest::class.java) + val chatPrompt = ChatPrompt( + requestFromUi.params.prompt.prompt, + requestFromUi.params.prompt.escapedPrompt, + node.command + ) + val textDocumentIdentifier = getTextDocumentIdentifier(project) + val cursorState = CursorState( + Range( + Position( + 0, + 0 + ), + Position( + 1, + 1 + ) + ) + ) + val chatParams = ChatParams( + requestFromUi.params.tabId, + chatPrompt, + textDocumentIdentifier, + cursorState + ) + val partialResultToken = chatCommunicationManager.addPartialChatMessage(requestFromUi.params.tabId) + + var encryptionManager: JwtEncryptionManager? = null + val result = AmazonQLspService.executeIfRunning(project) { server -> + encryptionManager = this.encryptionManager + server.sendChatPrompt(EncryptedChatParams(encryptionManager!!.encrypt(chatParams))) + } ?: (CompletableFuture.failedFuture(IllegalStateException("LSP Server not running"))) + + result.whenComplete { + value, error -> + chatCommunicationManager.removePartialChatMessage(partialResultToken) + val messageToChat = ChatCommunicationManager.convertToJsonToSendToChat( + node.command, + requestFromUi.params.tabId, + encryptionManager?.decrypt(value) ?: "", + isPartialResult = false + ) + browser.postChat(messageToChat) + } + } + } + } } 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 50b1be3626d..9b85030c82c 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 @@ -9,14 +9,17 @@ import org.eclipse.lsp4j.ConfigurationParams import org.eclipse.lsp4j.MessageActionItem import org.eclipse.lsp4j.MessageParams import org.eclipse.lsp4j.MessageType +import org.eclipse.lsp4j.ProgressParams import org.eclipse.lsp4j.PublishDiagnosticsParams import org.eclipse.lsp4j.ShowMessageRequestParams import software.aws.toolkits.jetbrains.core.credentials.AwsBearerTokenConnection import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnectionManager import software.aws.toolkits.jetbrains.core.credentials.pinning.QConnection +import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.ChatCommunicationManager import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.ConnectionMetadata import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.SsoProfileData import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings +import software.aws.toolkits.jetbrains.utils.notifyInfo import java.util.concurrent.CompletableFuture /** @@ -93,4 +96,15 @@ class AmazonQLanguageClientImpl(private val project: Project) : AmazonQLanguageC } ) } + + override fun notifyProgress(params: ProgressParams?) { + if (params == null) return + val chatCommunicationManager = ChatCommunicationManager.getInstance(project) + try { + chatCommunicationManager.handlePartialResultProgressNotification(project, params) + notifyInfo("hello") + } catch (e: Exception) { + error("cannot handle partial chat") + } + } } diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageServer.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageServer.kt index 18cf95d3973..c7eefe33284 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageServer.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageServer.kt @@ -9,6 +9,7 @@ import org.eclipse.lsp4j.jsonrpc.services.JsonRequest import org.eclipse.lsp4j.services.LanguageServer import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.GetConfigurationFromServerParams import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.LspServerConfigurations +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.EncryptedChatParams import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.UpdateCredentialsPayload import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.dependencies.DidChangeDependencyPathsParams import java.util.concurrent.CompletableFuture @@ -29,4 +30,7 @@ interface AmazonQLanguageServer : LanguageServer { @JsonRequest("aws/getConfigurationFromServer") fun getConfigurationFromServer(params: GetConfigurationFromServerParams): CompletableFuture + + @JsonRequest("aws/chat/sendChatPrompt") + fun sendChatPrompt(params: EncryptedChatParams): CompletableFuture } 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 59658c3a878..63e944bcbbf 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 @@ -104,6 +104,8 @@ class AmazonQLspService(private val project: Project, private val cs: CoroutineS private var instance: Deferred val capabilities get() = instance.getCompleted().initializeResult.getCompleted().capabilities + val encryptionManager + get() = instance.getCompleted().encryptionManager // dont allow lsp commands if server is restarting private val mutex = Mutex(false) @@ -194,7 +196,7 @@ class AmazonQLspService(private val project: Project, private val cs: CoroutineS } private class AmazonQServerInstance(private val project: Project, private val cs: CoroutineScope) : Disposable { - private val encryptionManager = JwtEncryptionManager() + val encryptionManager = JwtEncryptionManager() private val launcher: Launcher 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 8787259bf08..c6faba184a5 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 @@ -79,6 +79,16 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH, .sortedByDescending { (_, semVer) -> semVer } } + fun getLatestLocalLspArtifact(): Path { + val localFolders = getSubFolders(lspArtifactsPath) + return localFolders.map { localFolder -> + localFolder to SemVer.parseFromText(localFolder.fileName.toString()) + } + .sortedByDescending { (_, semVer) -> semVer } + .first() + .first + } + fun getExistingLspArtifacts(versions: List, target: ManifestManager.VersionTarget?): Boolean { if (versions.isEmpty() || target?.contents == null) return false 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 new file mode 100644 index 00000000000..9aa538be924 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/AsyncChatUiListener.kt @@ -0,0 +1,21 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.util.messages.Topic +import java.util.EventListener + +interface AsyncChatUiListener : EventListener { + fun onChange(message: String) {} + + companion object { + @Topic.AppLevel + val TOPIC = Topic.create("Partial chat message provider", AsyncChatUiListener::class.java) + + fun notifyPartialMessageUpdate(message: String) { + ApplicationManager.getApplication().messageBus.syncPublisher(TOPIC).onChange(message) + } + } +} 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 new file mode 100644 index 00000000000..ee47b5a1fe8 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ChatCommunicationManager.kt @@ -0,0 +1,70 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat + +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import org.eclipse.lsp4j.ProgressParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService +import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.ProgressNotificationUtils.getObject +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +@Service(Service.Level.PROJECT) +class ChatCommunicationManager { + val chatPartialResultMap = ConcurrentHashMap() + private fun getPartialChatMessage(partialResultToken: String): String = + chatPartialResultMap.getValue(partialResultToken) + + fun addPartialChatMessage(tabId: String): String { + val partialResultToken: String = UUID.randomUUID().toString() + chatPartialResultMap[partialResultToken] = tabId + return partialResultToken + } + + fun removePartialChatMessage(partialResultToken: String) = + chatPartialResultMap.remove(partialResultToken) + + fun handlePartialResultProgressNotification(project: Project, params: ProgressParams) { + val token = ProgressNotificationUtils.getToken(params) + val tabId = getPartialChatMessage(token) + if (tabId == null || tabId.isEmpty()) { + return + } + if (params.value.isLeft || params.value.right == null) { + error( + "Error handling partial result notification: expected value of type Object" + ) + } + + val encryptedPartialChatResult = getObject(params, String::class.java) + if (encryptedPartialChatResult != null) { + val partialChatResult = AmazonQLspService.getInstance(project).encryptionManager.decrypt(encryptedPartialChatResult) +// if (partialChatResult.body.isEmpty()) { +// return +// } + val uiMessage = convertToJsonToSendToChat( + command = "aws/chat/sendChatPrompt", + tabId = tabId, + params = partialChatResult, + isPartialResult = true + ) + AsyncChatUiListener.notifyPartialMessageUpdate(uiMessage) + } + } + companion object { + fun getInstance(project: Project) = project.service() + + fun convertToJsonToSendToChat(command: String, tabId: String, params: String, isPartialResult: Boolean): String = + """ + { + "command":"$command", + "tabId": "$tabId", + "params": $params, + "isPartialResult": $isPartialResult + } + """.trimIndent() + } +} diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ChatEditorUtils.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ChatEditorUtils.kt new file mode 100644 index 00000000000..bc70f113c0c --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ChatEditorUtils.kt @@ -0,0 +1,15 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat + +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import org.eclipse.lsp4j.TextDocumentIdentifier +import kotlin.io.path.Path + +fun getTextDocumentIdentifier(project: Project): TextDocumentIdentifier { + val selectedEditor = FileEditorManager.getInstance(project).selectedEditor + val filePath = Path(selectedEditor?.file?.path ?: "").toUri() + return TextDocumentIdentifier(filePath.toString()) +} diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ProgressNotificationUtils.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ProgressNotificationUtils.kt new file mode 100644 index 00000000000..e9f5590ba2f --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ProgressNotificationUtils.kt @@ -0,0 +1,30 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat + +import com.google.gson.Gson +import com.google.gson.JsonElement +import org.eclipse.lsp4j.ProgressParams + +object ProgressNotificationUtils { + fun getToken(params: ProgressParams): String { + val token = if (params.token.isLeft) { + params.token.left + } else { + params.token.right.toString() + } + + return token + } + + fun getObject(params: ProgressParams, cls: Class?): T? { + val objct = params.value.right as? JsonElement ?: return null + + val gson = Gson() + val element: JsonElement = objct + val obj: T = gson.fromJson(element, cls) + + return obj + } +} diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/ChatPrompt.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/ChatPrompt.kt new file mode 100644 index 00000000000..d63490fa05e --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/ChatPrompt.kt @@ -0,0 +1,26 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat + +data class ChatPrompt( + val prompt: String, + val escapedPrompt: String, + val command: String, +) + +data class SendChatPromptRequest( + val command: String, + val params: MidChatPrompt +) + +data class MidChatPrompt( + val prompt: InnerChatPrompt, + val tabId: String +) + +data class InnerChatPrompt( + val prompt: String, + val escapedPrompt: String, + val context: List +) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/ChatResult.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/ChatResult.kt new file mode 100644 index 00000000000..b7db9193b82 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/ChatResult.kt @@ -0,0 +1,54 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat + +data class ChatResult( + val body: String, + val messageId: String, + val canBeVoted: Boolean, + val relatedContent: RelatedContent, + val followUp: FollowUp, + val codeReference: List, +) + +data class RelatedContent( + val title: String, + val content: List, +) + +data class FollowUp( + val text: String, + val options: List, +) + +data class ReferenceTrackerInformation( + val licenseName: String, + val repository: String, + val url: String, + val recommendationContentSpan: RecommendationContentSpan, + val information: String, +) + +data class SourceLink( + val title: String, + val url: String, + val body: String, +) + +data class ChatItemAction( + val pillText: String, + val prompt: String, + val disabled: Boolean, + val description: String, + val type: String, +) + +data class RecommendationContentSpan( + val start: Int, + val end: Int, +) + +data class EncryptedChatResult( + val message: String, +) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/CursorState.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/CursorState.kt new file mode 100644 index 00000000000..2baa0221f50 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/CursorState.kt @@ -0,0 +1,10 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat + +import org.eclipse.lsp4j.Range + +data class CursorState( + val range: Range, +) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/SendChatPrompt.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/SendChatPrompt.kt new file mode 100644 index 00000000000..4eb32d622d5 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/SendChatPrompt.kt @@ -0,0 +1,18 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat + +import org.eclipse.lsp4j.TextDocumentIdentifier + +data class ChatParams( + val tabId: String, + val prompt: ChatPrompt, + val textDocument: TextDocumentIdentifier, + val cursorState: CursorState, +) + +data class EncryptedChatParams( + val message: String, + val partialResultToken: String? = null, +) From 9d037cfba828de055efafca83cec513f5e111373 Mon Sep 17 00:00:00 2001 From: Manodnya Bhoite Date: Wed, 9 Apr 2025 13:46:31 -0700 Subject: [PATCH 02/13] Partial chat results --- .../services/amazonq/toolwindow/AmazonQToolWindow.kt | 4 +++- .../jetbrains/services/amazonq/webview/BrowserConnector.kt | 7 ++++--- .../services/amazonq/lsp/AmazonQLanguageClientImpl.kt | 3 +-- .../services/amazonq/lsp/model/aws/chat/SendChatPrompt.kt | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt index 0a6ddd40229..0e5ac5acb93 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt @@ -61,7 +61,9 @@ class AmazonQToolWindow private constructor( AsyncChatUiListener.TOPIC, object : AsyncChatUiListener { override fun onChange(message: String) { - chatPanel.browser?.postChat(message) + runInEdt { + chatPanel.browser?.postChat(message) + } } } ) 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 8c90b65b9ec..8f1c5216176 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 @@ -166,18 +166,19 @@ class BrowserConnector( ) ) ) + + val partialResultToken = chatCommunicationManager.addPartialChatMessage(requestFromUi.params.tabId) val chatParams = ChatParams( requestFromUi.params.tabId, chatPrompt, textDocumentIdentifier, cursorState - ) - val partialResultToken = chatCommunicationManager.addPartialChatMessage(requestFromUi.params.tabId) + ) var encryptionManager: JwtEncryptionManager? = null val result = AmazonQLspService.executeIfRunning(project) { server -> encryptionManager = this.encryptionManager - server.sendChatPrompt(EncryptedChatParams(encryptionManager!!.encrypt(chatParams))) + server.sendChatPrompt(EncryptedChatParams(encryptionManager!!.encrypt(chatParams), partialResultToken)) } ?: (CompletableFuture.failedFuture(IllegalStateException("LSP Server not running"))) result.whenComplete { 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 9b85030c82c..e3b98ccfff3 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 @@ -102,9 +102,8 @@ class AmazonQLanguageClientImpl(private val project: Project) : AmazonQLanguageC val chatCommunicationManager = ChatCommunicationManager.getInstance(project) try { chatCommunicationManager.handlePartialResultProgressNotification(project, params) - notifyInfo("hello") } catch (e: Exception) { - error("cannot handle partial chat") + error("Cannot handle partial chat") } } } diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/SendChatPrompt.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/SendChatPrompt.kt index 4eb32d622d5..f7668712860 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/SendChatPrompt.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/SendChatPrompt.kt @@ -9,7 +9,7 @@ data class ChatParams( val tabId: String, val prompt: ChatPrompt, val textDocument: TextDocumentIdentifier, - val cursorState: CursorState, + val cursorState: CursorState ) data class EncryptedChatParams( From 6348f5e4e35fc70790b3212e5e651fa883d1de62 Mon Sep 17 00:00:00 2001 From: samgst-amazon Date: Thu, 10 Apr 2025 14:31:19 -0700 Subject: [PATCH 03/13] add endChat message --- .../services/amazonq/webview/BrowserConnector.kt | 13 +++++++++++++ .../services/amazonq/lsp/AmazonQLanguageServer.kt | 4 ++++ .../services/amazonq/lsp/model/aws/chat/EndChat.kt | 12 ++++++++++++ .../amazonq/lsp/model/aws/chat/LspCommands.kt | 8 ++++++++ 4 files changed, 37 insertions(+) create mode 100644 plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/EndChat.kt create mode 100644 plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/LspCommands.kt 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 8f1c5216176..e83e8f65e87 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 @@ -32,6 +32,8 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ChatP import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ChatPrompt import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CursorState import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.EncryptedChatParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.EndChatParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.LspCommands import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SendChatPromptRequest import software.aws.toolkits.jetbrains.services.amazonq.util.command import software.aws.toolkits.jetbrains.services.amazonq.util.tabType @@ -193,6 +195,17 @@ class BrowserConnector( browser.postChat(messageToChat) } } + + LspCommands.END_CHAT -> { + val requestFromUi = jacksonObjectMapper().readValue(node.toString(), EndChatParams::class.java) + + val endChatParams = EndChatParams(requestFromUi.tabId) + val result = AmazonQLspService.executeIfRunning(project) { server -> + server.endChat(endChatParams) + } ?: CompletableFuture.failedFuture(IllegalStateException("LSP Server not running")) + + println(result) + } } } } diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageServer.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageServer.kt index c7eefe33284..47437253a16 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageServer.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageServer.kt @@ -10,6 +10,7 @@ import org.eclipse.lsp4j.services.LanguageServer import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.GetConfigurationFromServerParams import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.LspServerConfigurations import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.EncryptedChatParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.EndChatParams import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.UpdateCredentialsPayload import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.dependencies.DidChangeDependencyPathsParams import java.util.concurrent.CompletableFuture @@ -33,4 +34,7 @@ interface AmazonQLanguageServer : LanguageServer { @JsonRequest("aws/chat/sendChatPrompt") fun sendChatPrompt(params: EncryptedChatParams): CompletableFuture + + @JsonRequest("aws/chat/endChat") + fun endChat(params: EndChatParams) : CompletableFuture } diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/EndChat.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/EndChat.kt new file mode 100644 index 00000000000..df717394afb --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/EndChat.kt @@ -0,0 +1,12 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat + +data class EndChatParams( + val tabId: String +) + +data class EndChatResult( + val status: Boolean +) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/LspCommands.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/LspCommands.kt new file mode 100644 index 00000000000..4030d10c587 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/LspCommands.kt @@ -0,0 +1,8 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat + +object LspCommands{ + const val END_CHAT = "aws/chat/endChat" +} From bc6823be9e6b188abd9b76b2e566665adeae3a3f Mon Sep 17 00:00:00 2001 From: Manodnya Bhoite Date: Thu, 10 Apr 2025 15:55:28 -0700 Subject: [PATCH 04/13] feedback --- .../amazonq/commands/MessageSerializer.kt | 4 ++ .../amazonq/toolwindow/AmazonQPanel.kt | 35 ++++++++++++---- .../amazonq/toolwindow/AmazonQToolWindow.kt | 42 ++++++++++++------- .../services/amazonq/webview/Browser.kt | 8 ++-- .../amazonq/webview/BrowserConnector.kt | 10 ++--- .../amazonq/lsp/AmazonQLanguageClientImpl.kt | 1 - .../lsp/flareChat/ChatCommunicationManager.kt | 7 ++-- .../flareChat/ProgressNotificationUtils.kt | 2 +- .../amazonq/lsp/model/aws/chat/ChatPrompt.kt | 6 +-- .../lsp/model/aws/chat/FlareChatCommands.kt | 6 +++ .../lsp/model/aws/chat/SendChatPrompt.kt | 2 +- 11 files changed, 81 insertions(+), 42 deletions(-) create mode 100644 plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/FlareChatCommands.kt diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/commands/MessageSerializer.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/commands/MessageSerializer.kt index 726ba1fb34c..1309b9604f1 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/commands/MessageSerializer.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/commands/MessageSerializer.kt @@ -12,6 +12,7 @@ import com.fasterxml.jackson.databind.MapperFeature import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.fasterxml.jackson.module.kotlin.treeToValue import org.jetbrains.annotations.VisibleForTesting import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage import software.aws.toolkits.jetbrains.services.amazonq.messages.UnknownMessageType @@ -36,6 +37,9 @@ class MessageSerializer @VisibleForTesting constructor() { fun serialize(value: Any): String = objectMapper.writeValueAsString(value) + fun deserializeChatMessages(value: JsonNode, clazz: Class): T = + objectMapper.treeToValue(value, clazz) + // Provide singleton global access companion object { private val instance = MessageSerializer() 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 396057bc9a9..9f213d3d610 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 @@ -5,7 +5,11 @@ package software.aws.toolkits.jetbrains.services.amazonq.toolwindow import com.intellij.idea.AppMode import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.runInEdt import com.intellij.openapi.util.Disposer +import com.intellij.ui.components.JBLoadingPanel +import com.intellij.ui.components.JBPanelWithEmptyText import com.intellij.ui.components.JBTextArea import com.intellij.ui.components.panels.Wrapper import com.intellij.ui.dsl.builder.Align @@ -14,14 +18,15 @@ import com.intellij.ui.dsl.builder.AlignY import com.intellij.ui.dsl.builder.panel import com.intellij.ui.jcef.JBCefApp import software.aws.toolkits.jetbrains.isDeveloperMode +import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.ArtifactHelper import software.aws.toolkits.jetbrains.services.amazonq.webview.Browser import java.awt.event.ActionListener +import java.util.concurrent.CompletableFuture import javax.swing.JButton class AmazonQPanel(private val parent: Disposable) { private val webviewContainer = Wrapper() - var browser: Browser? = null - private set + val browser = CompletableFuture() val component = panel { row { @@ -39,7 +44,7 @@ class AmazonQPanel(private val parent: Disposable) { // Code to be executed when the button is clicked // Add your logic here - browser?.jcefBrowser?.openDevtools() + browser.get().jcefBrowser.openDevtools() }, ) }, @@ -56,7 +61,7 @@ class AmazonQPanel(private val parent: Disposable) { fun disposeAndRecreate() { webviewContainer.removeAll() - val toDispose = browser + val toDispose = browser.get() init() if (toDispose != null) { Disposer.dispose(toDispose) @@ -71,10 +76,26 @@ class AmazonQPanel(private val parent: Disposable) { } else { webviewContainer.add(JBTextArea("JCEF not supported")) } - browser = null + browser.complete(null) } else { - browser = Browser(parent).also { - webviewContainer.add(it.component()) + val loadingPanel = JBLoadingPanel(null, parent, 0) + val wrapper = Wrapper() + loadingPanel.startLoading() + + loadingPanel.add(JBPanelWithEmptyText().withEmptyText("Wait for chat to be ready")) + webviewContainer.add(wrapper) + wrapper.setContent(loadingPanel) + + ApplicationManager.getApplication().executeOnPooledThread { + val webUri = ArtifactHelper().getLatestLocalLspArtifact().resolve("amazonq-ui.js").toUri() + loadingPanel.stopLoading() + runInEdt { + browser.complete( + Browser(parent, webUri).also { + wrapper.setContent(it.component()) + } + ) + } } } } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt index 0e5ac5acb93..1f59cbc7a5f 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt @@ -15,6 +15,7 @@ import com.intellij.openapi.util.Disposer import com.intellij.openapi.wm.ToolWindowManager import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.future.await import kotlinx.coroutines.launch import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext @@ -26,6 +27,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.messages.MessageConnecto import software.aws.toolkits.jetbrains.services.amazonq.onboarding.OnboardingPageInteraction import software.aws.toolkits.jetbrains.services.amazonq.onboarding.OnboardingPageInteractionType import software.aws.toolkits.jetbrains.services.amazonq.util.highlightCommand +import software.aws.toolkits.jetbrains.services.amazonq.webview.Browser import software.aws.toolkits.jetbrains.services.amazonq.webview.BrowserConnector import software.aws.toolkits.jetbrains.services.amazonq.webview.FqnWebviewAdapter import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.EditorThemeAdapter @@ -53,30 +55,43 @@ class AmazonQToolWindow private constructor( private val appConnections = mutableListOf() init { - initConnections() - connectUi() - connectApps() + prepareBrowser() + + scope.launch { + chatPanel.browser.await() + } project.messageBus.connect().subscribe( AsyncChatUiListener.TOPIC, object : AsyncChatUiListener { override fun onChange(message: String) { runInEdt { - chatPanel.browser?.postChat(message) + chatPanel.browser.get()?.postChat(message) } } } ) } + private fun prepareBrowser() { + chatPanel.browser.whenComplete { browser, ex -> + if (ex != null) { + return@whenComplete + } + + initConnections() + connectUi(browser) + connectApps(browser) + } + } + fun disposeAndRecreate() { browserConnector.uiReady = CompletableDeferred() + chatPanel.disposeAndRecreate() appConnections.clear() - initConnections() - connectUi() - connectApps() + prepareBrowser() ApplicationManager.getApplication().messageBus.syncPublisher(LafManagerListener.TOPIC).lookAndFeelChanged(LafManager.getInstance()) } @@ -109,9 +124,7 @@ class AmazonQToolWindow private constructor( } } - private fun connectApps() { - val browser = chatPanel.browser ?: return - + private fun connectApps(browser: Browser) { val fqnWebviewAdapter = FqnWebviewAdapter(browser.jcefBrowser, browserConnector) appConnections.forEach { connection -> @@ -129,11 +142,10 @@ class AmazonQToolWindow private constructor( } } - private fun connectUi() { - val chatBrowser = chatPanel.browser ?: return + private fun connectUi(browser: Browser) { val loginBrowser = QWebviewPanel.getInstance(project).browser ?: return - chatBrowser.init( + browser.init( isCodeTransformAvailable = isCodeTransformAvailable(project), isFeatureDevAvailable = isFeatureDevAvailable(project), isCodeScanAvailable = isCodeScanAvailable(project), @@ -145,7 +157,7 @@ class AmazonQToolWindow private constructor( scope.launch { // Pipe messages from the UI to the relevant apps and vice versa browserConnector.connect( - browser = chatBrowser, + browser = browser, connections = appConnections, ) } @@ -153,7 +165,7 @@ class AmazonQToolWindow private constructor( scope.launch { // Update the theme in the UI when the IDE theme changes browserConnector.connectTheme( - chatBrowser = chatBrowser.jcefBrowser.cefBrowser, + chatBrowser = browser.jcefBrowser.cefBrowser, loginBrowser = loginBrowser.jcefBrowser.cefBrowser, themeSource = editorThemeAdapter.onThemeChange(), ) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt index cd375e3ed60..a2a42b8b746 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt @@ -8,15 +8,15 @@ import com.intellij.openapi.Disposable import com.intellij.openapi.util.Disposer import com.intellij.ui.jcef.JBCefJSQuery import org.cef.CefApp -import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.ArtifactHelper import software.aws.toolkits.jetbrains.services.amazonq.util.HighlightCommand import software.aws.toolkits.jetbrains.services.amazonq.util.createBrowser import software.aws.toolkits.jetbrains.settings.MeetQSettings +import java.net.URI /* Displays the web view for the Amazon Q tool window */ -class Browser(parent: Disposable) : Disposable { +class Browser(parent: Disposable, private val webUri: URI) : Disposable { val jcefBrowser = createBrowser(parent) @@ -91,7 +91,7 @@ class Browser(parent: Disposable) : Disposable { ): String { val postMessageToJavaJsCode = receiveMessageQuery.inject("JSON.stringify(message)") val jsScripts = """ - + """.trimIndent() - addQuickActionCommands(isCodeTransformAvailable, isFeatureDevAvailable, isDocAvailable, isCodeTestAvailable, isCodeScanAvailable, highlightCommand) + addQuickActionCommands( + isCodeTransformAvailable, + isFeatureDevAvailable, + isDocAvailable, + isCodeTestAvailable, + isCodeScanAvailable, + highlightCommand, + activeProfile + ) return """