Skip to content

Add cursor position to chat request #5562

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: feature/q-lsp-chat
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -36,6 +37,9 @@ class MessageSerializer @VisibleForTesting constructor() {

fun serialize(value: Any): String = objectMapper.writeValueAsString(value)

fun <T> deserializeChatMessages(value: JsonNode, clazz: Class<T>): T =
objectMapper.treeToValue(value, clazz)

// Provide singleton global access
companion object {
private val instance = MessageSerializer()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Browser>()

val component = panel {
row {
Expand All @@ -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()
},
)
},
Expand All @@ -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)
Expand All @@ -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())
}
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -53,19 +56,43 @@ class AmazonQToolWindow private constructor(
private val appConnections = mutableListOf<AppConnection>()

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())
}
Expand Down Expand Up @@ -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 ->
Expand All @@ -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),
Expand All @@ -135,15 +159,15 @@ 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,
)
}

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(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -94,33 +102,92 @@ class Browser(parent: Disposable) : Disposable {
activeProfile: QRegionProfile?,
): String {
val postMessageToJavaJsCode = receiveMessageQuery.inject("JSON.stringify(message)")

val jsScripts = """
<script type="text/javascript" src="$WEB_SCRIPT_URI" defer onload="init()"></script>
<script type="text/javascript" src="$webUri" defer onload="init()"></script>
<script type="text/javascript">
const init = () => {
mynahUI.createMynahUI(
amazonQChat.createChat(
{
postMessage: message => {
$postMessageToJavaJsCode
}
},
${MeetQSettings.getInstance().reinvent2024OnboardingCount < MAX_ONBOARDING_PAGE_COUNT},
${MeetQSettings.getInstance().disclaimerAcknowledged},
$isFeatureDevAvailable, // whether /dev is available
$isCodeTransformAvailable, // whether /transform is available
$isDocAvailable, // whether /doc is available
$isCodeScanAvailable, // whether /scan is available
$isCodeTestAvailable, // whether /test is available
${OBJECT_MAPPER.writeValueAsString(highlightCommand)},
"${activeProfile?.profileName.orEmpty()}"
},
{
quickActionCommands: [],
disclaimerAcknowledged: ${MeetQSettings.getInstance().disclaimerAcknowledged}
}
);
}
</script>
""".trimIndent()

addQuickActionCommands(
isCodeTransformAvailable,
isFeatureDevAvailable,
isDocAvailable,
isCodeTestAvailable,
isCodeScanAvailable,
highlightCommand,
activeProfile
)
return """
<!DOCTYPE html>
<style>
body,
html {
background-color: var(--mynah-color-bg);
color: var(--mynah-color-text-default);
height: 100vh;
width: 100%%;
overflow: hidden;
margin: 0;
padding: 0;
}
.mynah-ui-icon-plus,
.mynah-ui-icon-cancel {
-webkit-mask-size: 155% !important;
mask-size: 155% !important;
mask-position: center;
scale: 60%;
}
.code-snippet-close-button i.mynah-ui-icon-cancel,
.mynah-chat-item-card-related-content-show-more i.mynah-ui-icon-down-open {
-webkit-mask-size: 195.5% !important;
mask-size: 195.5% !important;
mask-position: center;
aspect-ratio: 1/1;
width: 15px;
height: 15px;
scale: 50%
}
.mynah-ui-icon-tabs {
-webkit-mask-size: 102% !important;
mask-size: 102% !important;
mask-position: center;
}
textarea:placeholder-shown {
line-height: 1.5rem;
}
.mynah-ui-spinner-container {
contain: layout !important;
}
.mynah-ui-spinner-container > span.mynah-ui-spinner-logo-part {
position: static !important;
will-change: transform !important;
}
.mynah-ui-spinner-container,
.mynah-ui-spinner-container > span.mynah-ui-spinner-logo-part,
.mynah-ui-spinner-container > span.mynah-ui-spinner-logo-part > .mynah-ui-spinner-logo-mask.text {
border: 0 !important;
outline: none !important;
box-shadow: none !important;
border-radius: 0 !important;
}
.mynah-ui-spinner-container > span.mynah-ui-spinner-logo-part > .mynah-ui-spinner-logo-mask.text {
will-change: transform !important;
transform: translateZ(0) !important;
}
</style>
<html>
<head>
<title>AWS Q</title>
Expand All @@ -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()
}
Expand Down
Loading
Loading