Skip to content

Commit 299daed

Browse files
authored
fix(amazonq): refactor qpanel to be less leaky to fix chat panel not available (#5660)
The Q panel leaks implementation everywhere so it is difficult to cleanup and recreate the panel
1 parent e1e787b commit 299daed

File tree

5 files changed

+148
-223
lines changed

5 files changed

+148
-223
lines changed

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ import com.intellij.ui.components.panels.Wrapper
1818
import com.intellij.ui.dsl.builder.Align
1919
import com.intellij.ui.dsl.builder.panel
2020
import com.intellij.ui.jcef.JBCefJSQuery
21+
import kotlinx.coroutines.CoroutineScope
22+
import kotlinx.coroutines.flow.distinctUntilChanged
23+
import kotlinx.coroutines.flow.launchIn
24+
import kotlinx.coroutines.flow.onEach
2125
import org.cef.CefApp
2226
import software.aws.toolkits.core.utils.debug
2327
import software.aws.toolkits.core.utils.error
@@ -40,6 +44,8 @@ import software.aws.toolkits.jetbrains.services.amazonq.profile.QProfileSwitchIn
4044
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile
4145
import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfileManager
4246
import software.aws.toolkits.jetbrains.services.amazonq.util.createBrowser
47+
import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.EditorThemeAdapter
48+
import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.ThemeBrowserAdapter
4349
import software.aws.toolkits.jetbrains.utils.isQConnected
4450
import software.aws.toolkits.jetbrains.utils.isQExpired
4551
import software.aws.toolkits.jetbrains.utils.isQWebviewsAvailable
@@ -54,7 +60,7 @@ import javax.swing.JButton
5460
import javax.swing.JComponent
5561

5662
@Service(Service.Level.PROJECT)
57-
class QWebviewPanel private constructor(val project: Project) : Disposable {
63+
class QWebviewPanel private constructor(val project: Project, private val scope: CoroutineScope) : Disposable {
5864
private val webviewContainer = Wrapper()
5965
var browser: QWebviewBrowser? = null
6066
private set
@@ -102,6 +108,14 @@ class QWebviewPanel private constructor(val project: Project) : Disposable {
102108
} else {
103109
browser = QWebviewBrowser(project, this).also {
104110
webviewContainer.add(it.component())
111+
112+
val themeBrowserAdapter = ThemeBrowserAdapter()
113+
EditorThemeAdapter().onThemeChange()
114+
.distinctUntilChanged()
115+
.onEach { theme ->
116+
themeBrowserAdapter.updateLoginThemeInBrowser(it.jcefBrowser.cefBrowser, theme)
117+
}
118+
.launchIn(scope)
105119
}
106120
}
107121
}

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

Lines changed: 126 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,10 @@ 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"))
87104
webviewContainer.add(wrapper)
88105
wrapper.setContent(loadingPanel)
89106

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

0 commit comments

Comments
 (0)