Skip to content

Commit 08a6937

Browse files
committed
fix(amazonq): refactor qpanel to be less leaky
The Q panel leaks implementation everywhere so it is difficult to cleanup and recreate the panel
1 parent 2fafb49 commit 08a6937

File tree

4 files changed

+134
-222
lines changed

4 files changed

+134
-222
lines changed

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

Lines changed: 127 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,57 @@ import com.intellij.openapi.application.runInEdt
1010
import com.intellij.openapi.project.Project
1111
import com.intellij.openapi.util.Disposer
1212
import com.intellij.ui.components.JBLoadingPanel
13-
import com.intellij.ui.components.JBPanelWithEmptyText
1413
import com.intellij.ui.components.JBTextArea
1514
import com.intellij.ui.components.panels.Wrapper
1615
import com.intellij.ui.dsl.builder.Align
1716
import com.intellij.ui.dsl.builder.AlignX
1817
import com.intellij.ui.dsl.builder.AlignY
1918
import com.intellij.ui.dsl.builder.panel
2019
import com.intellij.ui.jcef.JBCefApp
20+
import kotlinx.coroutines.CoroutineScope
21+
import kotlinx.coroutines.launch
2122
import software.aws.toolkits.jetbrains.isDeveloperMode
23+
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
24+
import software.aws.toolkits.jetbrains.services.amazonq.apps.AppConnection
25+
import software.aws.toolkits.jetbrains.services.amazonq.commands.MessageTypeRegistry
2226
import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.ArtifactHelper
27+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.AsyncChatUiListener
28+
import software.aws.toolkits.jetbrains.services.amazonq.messages.AmazonQMessage
29+
import software.aws.toolkits.jetbrains.services.amazonq.messages.MessageConnector
30+
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
31+
import software.aws.toolkits.jetbrains.services.amazonq.util.highlightCommand
2332
import software.aws.toolkits.jetbrains.services.amazonq.webview.Browser
24-
import java.awt.event.ActionListener
33+
import software.aws.toolkits.jetbrains.services.amazonq.webview.BrowserConnector
34+
import software.aws.toolkits.jetbrains.services.amazonq.webview.FqnWebviewAdapter
35+
import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.EditorThemeAdapter
36+
import software.aws.toolkits.jetbrains.services.amazonqCodeScan.auth.isCodeScanAvailable
37+
import software.aws.toolkits.jetbrains.services.amazonqCodeTest.auth.isCodeTestAvailable
38+
import software.aws.toolkits.jetbrains.services.amazonqDoc.auth.isDocAvailable
39+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.auth.isFeatureDevAvailable
40+
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.isCodeTransformAvailable
2541
import java.util.concurrent.CompletableFuture
2642
import javax.swing.JButton
2743

28-
class AmazonQPanel(private val parent: Disposable, val project: Project) {
44+
class AmazonQPanel(val project: Project, private val scope: CoroutineScope) : Disposable {
45+
private val browser = CompletableFuture<Browser>()
2946
private val webviewContainer = Wrapper()
30-
val browser = CompletableFuture<Browser>()
47+
private val appSource = AppSource()
48+
private val browserConnector = BrowserConnector(project = project)
49+
private val editorThemeAdapter = EditorThemeAdapter()
50+
private val appConnections = mutableListOf<AppConnection>()
51+
52+
init {
53+
project.messageBus.connect().subscribe(
54+
AsyncChatUiListener.TOPIC,
55+
object : AsyncChatUiListener {
56+
override fun onChange(message: String) {
57+
runInEdt {
58+
browser.get()?.postChat(message)
59+
}
60+
}
61+
}
62+
)
63+
}
3164

3265
val component = panel {
3366
row {
@@ -40,14 +73,12 @@ class AmazonQPanel(private val parent: Disposable, val project: Project) {
4073
row {
4174
cell(
4275
JButton("Show Web Debugger").apply {
43-
addActionListener(
44-
ActionListener {
45-
// Code to be executed when the button is clicked
46-
// Add your logic here
47-
48-
browser.get().jcefBrowser.openDevtools()
49-
},
50-
)
76+
addActionListener {
77+
// Code to be executed when the button is clicked
78+
// Add your logic here
79+
80+
browser.get().jcefBrowser.openDevtools()
81+
}
5182
},
5283
)
5384
.align(AlignX.CENTER)
@@ -57,19 +88,6 @@ class AmazonQPanel(private val parent: Disposable, val project: Project) {
5788
}
5889

5990
init {
60-
init()
61-
}
62-
63-
fun disposeAndRecreate() {
64-
webviewContainer.removeAll()
65-
val toDispose = browser.get()
66-
init()
67-
if (toDispose != null) {
68-
Disposer.dispose(toDispose)
69-
}
70-
}
71-
72-
private fun init() {
7391
if (!JBCefApp.isSupported()) {
7492
// Fallback to an alternative browser-less solution
7593
if (AppMode.isRemoteDevHost()) {
@@ -79,11 +97,11 @@ class AmazonQPanel(private val parent: Disposable, val project: Project) {
7997
}
8098
browser.complete(null)
8199
} else {
82-
val loadingPanel = JBLoadingPanel(null, parent, 0)
100+
val loadingPanel = JBLoadingPanel(null, this)
83101
val wrapper = Wrapper()
84102
loadingPanel.startLoading()
85103

86-
loadingPanel.add(JBPanelWithEmptyText().withEmptyText("Wait for chat to be ready"))
104+
// loadingPanel.add(JBPanelWithEmptyText().withEmptyText("Wait for chat to be ready"))
87105
webviewContainer.add(wrapper)
88106
wrapper.setContent(loadingPanel)
89107

@@ -92,12 +110,93 @@ class AmazonQPanel(private val parent: Disposable, val project: Project) {
92110
loadingPanel.stopLoading()
93111
runInEdt {
94112
browser.complete(
95-
Browser(parent, webUri, project).also {
113+
Browser(this, webUri, project).also {
96114
wrapper.setContent(it.component())
115+
116+
initConnections()
117+
connectUi(it)
118+
connectApps(it)
97119
}
98120
)
99121
}
100122
}
101123
}
102124
}
125+
126+
fun sendMessage(message: AmazonQMessage, tabType: String) {
127+
appConnections.filter { it.app.tabTypes.contains(tabType) }.forEach {
128+
scope.launch {
129+
it.messagesFromUiToApp.publish(message)
130+
}
131+
}
132+
}
133+
134+
fun sendMessageAppToUi(message: AmazonQMessage, tabType: String) {
135+
appConnections.filter { it.app.tabTypes.contains(tabType) }.forEach {
136+
scope.launch {
137+
it.messagesFromAppToUi.publish(message)
138+
}
139+
}
140+
}
141+
142+
private fun initConnections() {
143+
val apps = appSource.getApps(project)
144+
apps.forEach { app ->
145+
appConnections += AppConnection(
146+
app = app,
147+
messagesFromAppToUi = MessageConnector(),
148+
messagesFromUiToApp = MessageConnector(),
149+
messageTypeRegistry = MessageTypeRegistry(),
150+
)
151+
}
152+
}
153+
154+
private fun connectApps(browser: Browser) {
155+
val fqnWebviewAdapter = FqnWebviewAdapter(browser.jcefBrowser, browserConnector)
156+
157+
appConnections.forEach { connection ->
158+
val initContext = AmazonQAppInitContext(
159+
project = project,
160+
messagesFromAppToUi = connection.messagesFromAppToUi,
161+
messagesFromUiToApp = connection.messagesFromUiToApp,
162+
messageTypeRegistry = connection.messageTypeRegistry,
163+
fqnWebviewAdapter = fqnWebviewAdapter,
164+
)
165+
// Connect the app to the UI
166+
connection.app.init(initContext)
167+
// Dispose of the app when the tool window is disposed.
168+
Disposer.register(this, connection.app)
169+
}
170+
}
171+
172+
private fun connectUi(browser: Browser) {
173+
browser.init(
174+
isCodeTransformAvailable = isCodeTransformAvailable(project),
175+
isFeatureDevAvailable = isFeatureDevAvailable(project),
176+
isCodeScanAvailable = isCodeScanAvailable(project),
177+
isCodeTestAvailable = isCodeTestAvailable(project),
178+
isDocAvailable = isDocAvailable(project),
179+
highlightCommand = highlightCommand(),
180+
activeProfile = QRegionProfileManager.getInstance().takeIf { it.shouldDisplayProfileInfo(project) }?.activeProfile(project)
181+
)
182+
183+
scope.launch {
184+
// Pipe messages from the UI to the relevant apps and vice versa
185+
browserConnector.connect(
186+
browser = browser,
187+
connections = appConnections,
188+
)
189+
}
190+
191+
scope.launch {
192+
// Update the theme in the UI when the IDE theme changes
193+
browserConnector.connectTheme(
194+
chatBrowser = browser.jcefBrowser.cefBrowser,
195+
themeSource = editorThemeAdapter.onThemeChange(),
196+
)
197+
}
198+
}
199+
200+
override fun dispose() {
201+
}
103202
}

0 commit comments

Comments
 (0)