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 726ba1fb34..1309b9604f 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 396057bc9a..9f213d3d61 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 8a1d637856..6783eceb0e 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,17 +15,20 @@ 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 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 import software.aws.toolkits.jetbrains.services.amazonq.onboarding.OnboardingPageInteractionType import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager import software.aws.toolkits.jetbrains.services.amazonq.util.highlightCommand +import software.aws.toolkits.jetbrains.services.amazonq.webview.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 @@ -43,7 +46,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) @@ -53,19 +56,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.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()) } @@ -98,9 +125,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 -> @@ -118,11 +143,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), @@ -135,7 +159,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, ) } @@ -143,7 +167,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 7dd41c795f..46552c338a 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 @@ -12,11 +12,12 @@ import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile 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) @@ -48,6 +49,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 @@ -94,33 +102,92 @@ class Browser(parent: Disposable) : Disposable { activeProfile: QRegionProfile?, ): String { val postMessageToJavaJsCode = receiveMessageQuery.inject("JSON.stringify(message)") - val jsScripts = """ - + """.trimIndent() + addQuickActionCommands( + isCodeTransformAvailable, + isFeatureDevAvailable, + isDocAvailable, + isCodeTestAvailable, + isCodeScanAvailable, + highlightCommand, + activeProfile + ) return """ + AWS Q @@ -132,8 +199,28 @@ class Browser(parent: Disposable) : Disposable { """.trimIndent() } + private fun addQuickActionCommands( + isCodeTransformAvailable: Boolean, + isFeatureDevAvailable: Boolean, + isDocAvailable: Boolean, + isCodeTestAvailable: Boolean, + isCodeScanAvailable: Boolean, + highlightCommand: HighlightCommand?, + activeProfile: QRegionProfile?, + ) { + // 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 + activeProfile + } + companion object { - private const val WEB_SCRIPT_URI = "http://mynah/js/mynah-ui.js" 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 592c588550..1aa3c8fbb7 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,10 @@ package software.aws.toolkits.jetbrains.services.amazonq.webview +import com.fasterxml.jackson.databind.JsonNode 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 @@ -19,6 +21,16 @@ import kotlinx.coroutines.launch import org.cef.browser.CefBrowser 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.getCursorState +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.EncryptedChatParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SEND_CHAT_COMMAND_PROMPT +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 +38,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() + private val chatCommunicationManager = ChatCommunicationManager.getInstance(project) suspend fun connect( browser: Browser, @@ -77,7 +92,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 +141,45 @@ class BrowserConnector( browser.receiveMessageQuery.removeHandler(handler) } } + + private fun handleFlareChatMessages(browser: Browser, node: JsonNode) { + when (node.command) { + SEND_CHAT_COMMAND_PROMPT -> { + val requestFromUi = serializer.deserializeChatMessages(node, SendChatPromptRequest::class.java) + val chatPrompt = ChatPrompt( + requestFromUi.params.prompt.prompt, + requestFromUi.params.prompt.escapedPrompt, + node.command + ) + val textDocumentIdentifier = getTextDocumentIdentifier(project) + val cursorState = getCursorState(project) + + val partialResultToken = chatCommunicationManager.addPartialChatMessage(requestFromUi.params.tabId) + val chatParams = ChatParams( + requestFromUi.params.tabId, + chatPrompt, + textDocumentIdentifier, + cursorState + ) + + var encryptionManager: JwtEncryptionManager? = null + val result = AmazonQLspService.executeIfRunning(project) { server -> + encryptionManager = this.encryptionManager + encryptionManager?.encrypt(chatParams)?.let { EncryptedChatParams(it, partialResultToken) }?.let { server.sendChatPrompt(it) } + } ?: (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).orEmpty(), + 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 50b1be3626..6d4c0f171a 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,11 +9,13 @@ 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 @@ -93,4 +95,14 @@ 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) + } 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 18cf95d397..c7eefe3328 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 59658c3a87..63e944bcbb 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 8787259bf0..c6faba184a 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 0000000000..9aa538be92 --- /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 0000000000..1ee2c52fc9 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ChatCommunicationManager.kt @@ -0,0 +1,69 @@ +// 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 software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SEND_CHAT_COMMAND_PROMPT +import java.util.UUID +import java.util.concurrent.ConcurrentHashMap + +@Service(Service.Level.PROJECT) +class ChatCommunicationManager { + private 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) + + val uiMessage = convertToJsonToSendToChat( + command = SEND_CHAT_COMMAND_PROMPT, + 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 0000000000..ab7b6008df --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/ChatEditorUtils.kt @@ -0,0 +1,44 @@ +// 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.runReadAction +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.Range +import org.eclipse.lsp4j.TextDocumentIdentifier +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CursorState +import kotlin.io.path.Path + +fun getTextDocumentIdentifier(project: Project): TextDocumentIdentifier? { + val selectedEditor = FileEditorManager.getInstance(project).selectedEditor ?: return null + val filePath = Path(selectedEditor.file.path).toUri() + return TextDocumentIdentifier(filePath.toString()) +} + +fun getCursorState(project: Project): CursorState? { + return runReadAction { + val editor = FileEditorManager.getInstance(project).selectedTextEditor ?: return@runReadAction null + val selectionModel = editor.selectionModel + val document = editor.document + + // Get start position + val startOffset = selectionModel.selectionStart + val startLine = document.getLineNumber(startOffset) + val startColumn = startOffset - document.getLineStartOffset(startLine) + + // Get end position + val endOffset = selectionModel.selectionEnd + val endLine = document.getLineNumber(endOffset) + val endColumn = endOffset - document.getLineStartOffset(endLine) + + return@runReadAction CursorState( + Range( + Position(startLine, startColumn), + Position(endLine, endColumn) + ) + ) + } +} 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 0000000000..5dfc924005 --- /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 +@file:Suppress("BannedImports") +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 0000000000..5e58e41eae --- /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 0000000000..b7db9193b8 --- /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 0000000000..2baa0221f5 --- /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/FlareChatCommands.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/FlareChatCommands.kt new file mode 100644 index 0000000000..6ee3c89ff8 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/chat/FlareChatCommands.kt @@ -0,0 +1,6 @@ +// 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 + +const val SEND_CHAT_COMMAND_PROMPT = "aws/chat/sendChatPrompt" 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 0000000000..58f5bb5de8 --- /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, +)