Skip to content

Commit 8d4b98f

Browse files
authored
feat(amazonQFlareChat): Set up Flare chat connection (#5545)
* Set up Flare chat connection * Partial chat results * feedback * detekt * syntax error * detekt * detekt
1 parent b247a76 commit 8d4b98f

File tree

18 files changed

+524
-39
lines changed

18 files changed

+524
-39
lines changed

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/commands/MessageSerializer.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.fasterxml.jackson.databind.MapperFeature
1212
import com.fasterxml.jackson.databind.SerializationFeature
1313
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
1414
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
15+
import com.fasterxml.jackson.module.kotlin.treeToValue
1516
import org.jetbrains.annotations.VisibleForTesting
1617
import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
1718
import software.aws.toolkits.jetbrains.services.amazonq.messages.UnknownMessageType
@@ -36,6 +37,9 @@ class MessageSerializer @VisibleForTesting constructor() {
3637

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

40+
fun <T> deserializeChatMessages(value: JsonNode, clazz: Class<T>): T =
41+
objectMapper.treeToValue(value, clazz)
42+
3943
// Provide singleton global access
4044
companion object {
4145
private val instance = MessageSerializer()

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQPanel.kt

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ package software.aws.toolkits.jetbrains.services.amazonq.toolwindow
55

66
import com.intellij.idea.AppMode
77
import com.intellij.openapi.Disposable
8+
import com.intellij.openapi.application.ApplicationManager
9+
import com.intellij.openapi.application.runInEdt
810
import com.intellij.openapi.util.Disposer
11+
import com.intellij.ui.components.JBLoadingPanel
12+
import com.intellij.ui.components.JBPanelWithEmptyText
913
import com.intellij.ui.components.JBTextArea
1014
import com.intellij.ui.components.panels.Wrapper
1115
import com.intellij.ui.dsl.builder.Align
@@ -14,14 +18,15 @@ import com.intellij.ui.dsl.builder.AlignY
1418
import com.intellij.ui.dsl.builder.panel
1519
import com.intellij.ui.jcef.JBCefApp
1620
import software.aws.toolkits.jetbrains.isDeveloperMode
21+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.ArtifactHelper
1722
import software.aws.toolkits.jetbrains.services.amazonq.webview.Browser
1823
import java.awt.event.ActionListener
24+
import java.util.concurrent.CompletableFuture
1925
import javax.swing.JButton
2026

2127
class AmazonQPanel(private val parent: Disposable) {
2228
private val webviewContainer = Wrapper()
23-
var browser: Browser? = null
24-
private set
29+
val browser = CompletableFuture<Browser>()
2530

2631
val component = panel {
2732
row {
@@ -39,7 +44,7 @@ class AmazonQPanel(private val parent: Disposable) {
3944
// Code to be executed when the button is clicked
4045
// Add your logic here
4146

42-
browser?.jcefBrowser?.openDevtools()
47+
browser.get().jcefBrowser.openDevtools()
4348
},
4449
)
4550
},
@@ -56,7 +61,7 @@ class AmazonQPanel(private val parent: Disposable) {
5661

5762
fun disposeAndRecreate() {
5863
webviewContainer.removeAll()
59-
val toDispose = browser
64+
val toDispose = browser.get()
6065
init()
6166
if (toDispose != null) {
6267
Disposer.dispose(toDispose)
@@ -71,10 +76,26 @@ class AmazonQPanel(private val parent: Disposable) {
7176
} else {
7277
webviewContainer.add(JBTextArea("JCEF not supported"))
7378
}
74-
browser = null
79+
browser.complete(null)
7580
} else {
76-
browser = Browser(parent).also {
77-
webviewContainer.add(it.component())
81+
val loadingPanel = JBLoadingPanel(null, parent, 0)
82+
val wrapper = Wrapper()
83+
loadingPanel.startLoading()
84+
85+
loadingPanel.add(JBPanelWithEmptyText().withEmptyText("Wait for chat to be ready"))
86+
webviewContainer.add(wrapper)
87+
wrapper.setContent(loadingPanel)
88+
89+
ApplicationManager.getApplication().executeOnPooledThread {
90+
val webUri = ArtifactHelper().getLatestLocalLspArtifact().resolve("amazonq-ui.js").toUri()
91+
loadingPanel.stopLoading()
92+
runInEdt {
93+
browser.complete(
94+
Browser(parent, webUri).also {
95+
wrapper.setContent(it.component())
96+
}
97+
)
98+
}
7899
}
79100
}
80101
}

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQToolWindow.kt

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,20 @@ import com.intellij.openapi.util.Disposer
1515
import com.intellij.openapi.wm.ToolWindowManager
1616
import kotlinx.coroutines.CompletableDeferred
1717
import kotlinx.coroutines.CoroutineScope
18+
import kotlinx.coroutines.future.await
1819
import kotlinx.coroutines.launch
1920
import software.aws.toolkits.jetbrains.services.amazonq.QWebviewPanel
2021
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
2122
import software.aws.toolkits.jetbrains.services.amazonq.apps.AppConnection
2223
import software.aws.toolkits.jetbrains.services.amazonq.commands.MessageTypeRegistry
24+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.AsyncChatUiListener
2325
import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
2426
import software.aws.toolkits.jetbrains.services.amazonq.messages.MessageConnector
2527
import software.aws.toolkits.jetbrains.services.amazonq.onboarding.OnboardingPageInteraction
2628
import software.aws.toolkits.jetbrains.services.amazonq.onboarding.OnboardingPageInteractionType
2729
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
2830
import software.aws.toolkits.jetbrains.services.amazonq.util.highlightCommand
31+
import software.aws.toolkits.jetbrains.services.amazonq.webview.Browser
2932
import software.aws.toolkits.jetbrains.services.amazonq.webview.BrowserConnector
3033
import software.aws.toolkits.jetbrains.services.amazonq.webview.FqnWebviewAdapter
3134
import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.EditorThemeAdapter
@@ -43,7 +46,7 @@ class AmazonQToolWindow private constructor(
4346
private val scope: CoroutineScope,
4447
) : Disposable {
4548
private val appSource = AppSource()
46-
private val browserConnector = BrowserConnector()
49+
private val browserConnector = BrowserConnector(project = project)
4750
private val editorThemeAdapter = EditorThemeAdapter()
4851

4952
private val chatPanel = AmazonQPanel(parent = this)
@@ -53,19 +56,43 @@ class AmazonQToolWindow private constructor(
5356
private val appConnections = mutableListOf<AppConnection>()
5457

5558
init {
56-
initConnections()
57-
connectUi()
58-
connectApps()
59+
prepareBrowser()
60+
61+
scope.launch {
62+
chatPanel.browser.await()
63+
}
64+
65+
project.messageBus.connect().subscribe(
66+
AsyncChatUiListener.TOPIC,
67+
object : AsyncChatUiListener {
68+
override fun onChange(message: String) {
69+
runInEdt {
70+
chatPanel.browser.get()?.postChat(message)
71+
}
72+
}
73+
}
74+
)
75+
}
76+
77+
private fun prepareBrowser() {
78+
chatPanel.browser.whenComplete { browser, ex ->
79+
if (ex != null) {
80+
return@whenComplete
81+
}
82+
83+
initConnections()
84+
connectUi(browser)
85+
connectApps(browser)
86+
}
5987
}
6088

6189
fun disposeAndRecreate() {
6290
browserConnector.uiReady = CompletableDeferred()
91+
6392
chatPanel.disposeAndRecreate()
6493

6594
appConnections.clear()
66-
initConnections()
67-
connectUi()
68-
connectApps()
95+
prepareBrowser()
6996

7097
ApplicationManager.getApplication().messageBus.syncPublisher(LafManagerListener.TOPIC).lookAndFeelChanged(LafManager.getInstance())
7198
}
@@ -98,9 +125,7 @@ class AmazonQToolWindow private constructor(
98125
}
99126
}
100127

101-
private fun connectApps() {
102-
val browser = chatPanel.browser ?: return
103-
128+
private fun connectApps(browser: Browser) {
104129
val fqnWebviewAdapter = FqnWebviewAdapter(browser.jcefBrowser, browserConnector)
105130

106131
appConnections.forEach { connection ->
@@ -118,11 +143,10 @@ class AmazonQToolWindow private constructor(
118143
}
119144
}
120145

121-
private fun connectUi() {
122-
val chatBrowser = chatPanel.browser ?: return
146+
private fun connectUi(browser: Browser) {
123147
val loginBrowser = QWebviewPanel.getInstance(project).browser ?: return
124148

125-
chatBrowser.init(
149+
browser.init(
126150
isCodeTransformAvailable = isCodeTransformAvailable(project),
127151
isFeatureDevAvailable = isFeatureDevAvailable(project),
128152
isCodeScanAvailable = isCodeScanAvailable(project),
@@ -135,15 +159,15 @@ class AmazonQToolWindow private constructor(
135159
scope.launch {
136160
// Pipe messages from the UI to the relevant apps and vice versa
137161
browserConnector.connect(
138-
browser = chatBrowser,
162+
browser = browser,
139163
connections = appConnections,
140164
)
141165
}
142166

143167
scope.launch {
144168
// Update the theme in the UI when the IDE theme changes
145169
browserConnector.connectTheme(
146-
chatBrowser = chatBrowser.jcefBrowser.cefBrowser,
170+
chatBrowser = browser.jcefBrowser.cefBrowser,
147171
loginBrowser = loginBrowser.jcefBrowser.cefBrowser,
148172
themeSource = editorThemeAdapter.onThemeChange(),
149173
)

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt

Lines changed: 102 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
1212
import software.aws.toolkits.jetbrains.services.amazonq.util.HighlightCommand
1313
import software.aws.toolkits.jetbrains.services.amazonq.util.createBrowser
1414
import software.aws.toolkits.jetbrains.settings.MeetQSettings
15+
import java.net.URI
1516

1617
/*
1718
Displays the web view for the Amazon Q tool window
1819
*/
19-
class Browser(parent: Disposable) : Disposable {
20+
class Browser(parent: Disposable, private val webUri: URI) : Disposable {
2021

2122
val jcefBrowser = createBrowser(parent)
2223

@@ -48,6 +49,13 @@ class Browser(parent: Disposable) : Disposable {
4849

4950
fun component() = jcefBrowser.component
5051

52+
fun postChat(message: String) {
53+
jcefBrowser
54+
.cefBrowser
55+
.executeJavaScript("window.postMessage($message)", jcefBrowser.cefBrowser.url, 0)
56+
}
57+
58+
// TODO: Remove this once chat has been integrated with agents
5159
fun post(message: String) =
5260
jcefBrowser
5361
.cefBrowser
@@ -94,33 +102,92 @@ class Browser(parent: Disposable) : Disposable {
94102
activeProfile: QRegionProfile?,
95103
): String {
96104
val postMessageToJavaJsCode = receiveMessageQuery.inject("JSON.stringify(message)")
97-
98105
val jsScripts = """
99-
<script type="text/javascript" src="$WEB_SCRIPT_URI" defer onload="init()"></script>
106+
<script type="text/javascript" src="$webUri" defer onload="init()"></script>
100107
<script type="text/javascript">
101108
const init = () => {
102-
mynahUI.createMynahUI(
109+
amazonQChat.createChat(
103110
{
104111
postMessage: message => {
105112
$postMessageToJavaJsCode
106113
}
107-
},
108-
${MeetQSettings.getInstance().reinvent2024OnboardingCount < MAX_ONBOARDING_PAGE_COUNT},
109-
${MeetQSettings.getInstance().disclaimerAcknowledged},
110-
$isFeatureDevAvailable, // whether /dev is available
111-
$isCodeTransformAvailable, // whether /transform is available
112-
$isDocAvailable, // whether /doc is available
113-
$isCodeScanAvailable, // whether /scan is available
114-
$isCodeTestAvailable, // whether /test is available
115-
${OBJECT_MAPPER.writeValueAsString(highlightCommand)},
116-
"${activeProfile?.profileName.orEmpty()}"
114+
},
115+
{
116+
quickActionCommands: [],
117+
disclaimerAcknowledged: ${MeetQSettings.getInstance().disclaimerAcknowledged}
118+
}
117119
);
118120
}
119121
</script>
120122
""".trimIndent()
121123

124+
addQuickActionCommands(
125+
isCodeTransformAvailable,
126+
isFeatureDevAvailable,
127+
isDocAvailable,
128+
isCodeTestAvailable,
129+
isCodeScanAvailable,
130+
highlightCommand,
131+
activeProfile
132+
)
122133
return """
123134
<!DOCTYPE html>
135+
<style>
136+
body,
137+
html {
138+
background-color: var(--mynah-color-bg);
139+
color: var(--mynah-color-text-default);
140+
height: 100vh;
141+
width: 100%%;
142+
overflow: hidden;
143+
margin: 0;
144+
padding: 0;
145+
}
146+
.mynah-ui-icon-plus,
147+
.mynah-ui-icon-cancel {
148+
-webkit-mask-size: 155% !important;
149+
mask-size: 155% !important;
150+
mask-position: center;
151+
scale: 60%;
152+
}
153+
.code-snippet-close-button i.mynah-ui-icon-cancel,
154+
.mynah-chat-item-card-related-content-show-more i.mynah-ui-icon-down-open {
155+
-webkit-mask-size: 195.5% !important;
156+
mask-size: 195.5% !important;
157+
mask-position: center;
158+
aspect-ratio: 1/1;
159+
width: 15px;
160+
height: 15px;
161+
scale: 50%
162+
}
163+
.mynah-ui-icon-tabs {
164+
-webkit-mask-size: 102% !important;
165+
mask-size: 102% !important;
166+
mask-position: center;
167+
}
168+
textarea:placeholder-shown {
169+
line-height: 1.5rem;
170+
}
171+
.mynah-ui-spinner-container {
172+
contain: layout !important;
173+
}
174+
.mynah-ui-spinner-container > span.mynah-ui-spinner-logo-part {
175+
position: static !important;
176+
will-change: transform !important;
177+
}
178+
.mynah-ui-spinner-container,
179+
.mynah-ui-spinner-container > span.mynah-ui-spinner-logo-part,
180+
.mynah-ui-spinner-container > span.mynah-ui-spinner-logo-part > .mynah-ui-spinner-logo-mask.text {
181+
border: 0 !important;
182+
outline: none !important;
183+
box-shadow: none !important;
184+
border-radius: 0 !important;
185+
}
186+
.mynah-ui-spinner-container > span.mynah-ui-spinner-logo-part > .mynah-ui-spinner-logo-mask.text {
187+
will-change: transform !important;
188+
transform: translateZ(0) !important;
189+
}
190+
</style>
124191
<html>
125192
<head>
126193
<title>AWS Q</title>
@@ -132,8 +199,28 @@ class Browser(parent: Disposable) : Disposable {
132199
""".trimIndent()
133200
}
134201

202+
private fun addQuickActionCommands(
203+
isCodeTransformAvailable: Boolean,
204+
isFeatureDevAvailable: Boolean,
205+
isDocAvailable: Boolean,
206+
isCodeTestAvailable: Boolean,
207+
isCodeScanAvailable: Boolean,
208+
highlightCommand: HighlightCommand?,
209+
activeProfile: QRegionProfile?,
210+
) {
211+
// TODO: Remove this once chat has been integrated with agents. This is added temporarily to keep detekt happy.
212+
isCodeScanAvailable
213+
isCodeTestAvailable
214+
isDocAvailable
215+
isFeatureDevAvailable
216+
isCodeTransformAvailable
217+
MAX_ONBOARDING_PAGE_COUNT
218+
OBJECT_MAPPER
219+
highlightCommand
220+
activeProfile
221+
}
222+
135223
companion object {
136-
private const val WEB_SCRIPT_URI = "http://mynah/js/mynah-ui.js"
137224
private const val MAX_ONBOARDING_PAGE_COUNT = 3
138225
private val OBJECT_MAPPER = jacksonObjectMapper()
139226
}

0 commit comments

Comments
 (0)