Skip to content

Commit 6caa149

Browse files
authored
feat(amazonq): Add support for Amazon Q chat on remote 242+ (#4825)
1 parent 47f2cbc commit 6caa149

File tree

39 files changed

+336
-812
lines changed

39 files changed

+336
-812
lines changed

buildSrc/src/main/kotlin/toolkit-publish-root-conventions.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType
55
import org.jetbrains.intellij.platform.gradle.tasks.PatchPluginXmlTask
6+
import org.jetbrains.intellij.platform.gradle.tasks.aware.SplitModeAware
67
import software.aws.toolkits.gradle.intellij.IdeFlavor
78
import software.aws.toolkits.gradle.intellij.toolkitIntelliJ
89

@@ -75,3 +76,8 @@ tasks.runIde {
7576
systemProperty("user.home", home)
7677
environment("HOME", home)
7778
}
79+
80+
val runSplitIde by intellijPlatformTesting.runIde.registering {
81+
splitMode = true
82+
splitModeTarget = SplitModeAware.SplitModeTarget.BACKEND
83+
}

plugins/amazonq/build.gradle.kts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType
54
import software.aws.toolkits.gradle.changelog.tasks.GeneratePluginChangeLog
6-
import software.aws.toolkits.gradle.intellij.IdeFlavor
7-
import software.aws.toolkits.gradle.intellij.IdeVersions
8-
import software.aws.toolkits.gradle.intellij.toolkitIntelliJ
95

106
plugins {
117
id("toolkit-publishing-conventions")

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

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import com.intellij.ui.components.panels.Wrapper
1616
import com.intellij.ui.dsl.builder.Align
1717
import com.intellij.ui.dsl.builder.panel
1818
import com.intellij.ui.jcef.JBCefJSQuery
19-
import org.cef.CefApp
2019
import software.aws.toolkits.core.utils.error
2120
import software.aws.toolkits.core.utils.getLogger
2221
import software.aws.toolkits.core.utils.warn
@@ -30,8 +29,8 @@ import software.aws.toolkits.jetbrains.core.credentials.sono.isSono
3029
import software.aws.toolkits.jetbrains.core.region.AwsRegionProvider
3130
import software.aws.toolkits.jetbrains.core.webview.BrowserMessage
3231
import software.aws.toolkits.jetbrains.core.webview.BrowserState
32+
import software.aws.toolkits.jetbrains.core.webview.LocalAssetJBCefRequestHandler
3333
import software.aws.toolkits.jetbrains.core.webview.LoginBrowser
34-
import software.aws.toolkits.jetbrains.core.webview.WebviewResourceHandlerFactory
3534
import software.aws.toolkits.jetbrains.isDeveloperMode
3635
import software.aws.toolkits.jetbrains.services.amazonq.util.createBrowser
3736
import software.aws.toolkits.jetbrains.utils.isQConnected
@@ -108,25 +107,14 @@ class QWebviewPanel private constructor(val project: Project) : Disposable {
108107
class QWebviewBrowser(val project: Project, private val parentDisposable: Disposable) :
109108
LoginBrowser(
110109
project,
111-
QWebviewBrowser.DOMAIN,
112-
QWebviewBrowser.WEB_SCRIPT_URI
113110
),
114111
Disposable {
115112
// TODO: confirm if we need such configuration or the default is fine
116113
override val jcefBrowser = createBrowser(parentDisposable)
117114
private val query = JBCefJSQuery.create(jcefBrowser)
115+
private val assetHandler = LocalAssetJBCefRequestHandler(jcefBrowser)
118116

119117
init {
120-
CefApp.getInstance()
121-
.registerSchemeHandlerFactory(
122-
"http",
123-
domain,
124-
WebviewResourceHandlerFactory(
125-
domain = "http://$domain/",
126-
assetUri = "/webview/assets/"
127-
),
128-
)
129-
130118
loadWebView(query)
131119

132120
query.addHandler(jcefHandler)
@@ -273,12 +261,15 @@ class QWebviewBrowser(val project: Project, private val parentDisposable: Dispos
273261
}
274262

275263
override fun loadWebView(query: JBCefJSQuery) {
276-
jcefBrowser.loadHTML(getWebviewHTML(webScriptUri, query))
264+
val webScriptUri = assetHandler.createResource(
265+
"js/getStart.js",
266+
QWebviewBrowser::class.java.getResourceAsStream("/webview/assets/js/getStart.js")
267+
)
268+
269+
jcefBrowser.loadURL(assetHandler.createResource("content.html", getWebviewHTML(webScriptUri, query)))
277270
}
278271

279272
companion object {
280273
private val LOG = getLogger<QWebviewBrowser>()
281-
private const val WEB_SCRIPT_URI = "http://webview/js/getStart.js"
282-
private const val DOMAIN = "webview"
283274
}
284275
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,20 @@ import icons.AwsIcons
1111
import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AMAZON_Q_WINDOW_ID
1212
import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindow
1313
import software.aws.toolkits.jetbrains.services.codewhisperer.util.CodeWhispererConstants.runScanKey
14-
import software.aws.toolkits.jetbrains.utils.isRunningOnRemoteBackend
1514
import software.aws.toolkits.resources.message
1615
import software.aws.toolkits.telemetry.UiTelemetry
1716

1817
class QOpenPanelAction : AnAction(message("action.q.openchat.text"), null, AwsIcons.Logos.AWS_Q) {
1918
override fun actionPerformed(e: AnActionEvent) {
20-
if (isRunningOnRemoteBackend()) return
2119
val project = e.getRequiredData(CommonDataKeys.PROJECT)
2220
UiTelemetry.click(project, "q_openChat")
2321
ToolWindowManager.getInstance(project).getToolWindow(AMAZON_Q_WINDOW_ID)?.activate(null, true)
2422
if (e.getData(runScanKey) == true) {
2523
AmazonQToolWindow.openScanTab(project)
2624
}
2725
}
26+
27+
override fun update(e: AnActionEvent) {
28+
e.presentation.isEnabled = e.getData(CommonDataKeys.PROJECT) != null
29+
}
2830
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import org.cef.browser.CefBrowser
1818
import org.cef.browser.CefFrame
1919
import org.cef.handler.CefLoadHandlerAdapter
2020
import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope
21+
import software.aws.toolkits.jetbrains.core.webview.LocalAssetJBCefRequestHandler
2122
import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindow
2223
import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.EditorThemeAdapter
2324
import software.aws.toolkits.resources.message
@@ -72,7 +73,7 @@ class QGettingStartedContent(val project: Project) : Disposable {
7273

7374
private fun loadWebView() {
7475
// load the web app
75-
jcefBrowser.loadHTML(getWebviewHTML())
76+
jcefBrowser.loadURL(LocalAssetJBCefRequestHandler(jcefBrowser).createResource("content.html", getWebviewHTML()))
7677
}
7778

7879
private fun getWebviewHTML(): String {

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
package software.aws.toolkits.jetbrains.services.amazonq.toolwindow
55

6-
import com.intellij.idea.AppMode
76
import com.intellij.openapi.Disposable
87
import com.intellij.openapi.util.Disposer
98
import com.intellij.ui.components.JBTextArea
@@ -15,6 +14,7 @@ import com.intellij.ui.dsl.builder.panel
1514
import com.intellij.ui.jcef.JBCefApp
1615
import software.aws.toolkits.jetbrains.isDeveloperMode
1716
import software.aws.toolkits.jetbrains.services.amazonq.webview.Browser
17+
import software.aws.toolkits.jetbrains.utils.isRunningOnRemoteBackend
1818
import java.awt.event.ActionListener
1919
import javax.swing.JButton
2020

@@ -66,8 +66,8 @@ class AmazonQPanel(private val parent: Disposable) {
6666
private fun init() {
6767
if (!JBCefApp.isSupported()) {
6868
// Fallback to an alternative browser-less solution
69-
if (AppMode.isRemoteDevHost()) {
70-
webviewContainer.add(JBTextArea("Amazon Q chat is not supported in remote dev environment."))
69+
if (isRunningOnRemoteBackend()) {
70+
webviewContainer.add(JBTextArea("Amazon Q chat is not supported in this remote dev environment because it lacks JCEF webview support."))
7171
} else {
7272
webviewContainer.add(JBTextArea("JCEF not supported"))
7373
}

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

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
77
import com.intellij.openapi.Disposable
88
import com.intellij.openapi.util.Disposer
99
import com.intellij.ui.jcef.JBCefJSQuery
10-
import org.cef.CefApp
10+
import software.aws.toolkits.jetbrains.core.webview.LocalAssetJBCefRequestHandler
1111
import software.aws.toolkits.jetbrains.services.amazonq.util.HighlightCommand
1212
import software.aws.toolkits.jetbrains.services.amazonq.util.createBrowser
1313
import software.aws.toolkits.jetbrains.settings.MeetQSettings
14+
import java.nio.file.Paths
1415

1516
/*
1617
Displays the web view for the Amazon Q tool window
@@ -21,6 +22,17 @@ class Browser(parent: Disposable) : Disposable {
2122

2223
val receiveMessageQuery = JBCefJSQuery.create(jcefBrowser)
2324

25+
private val assetRequestHandler = LocalAssetJBCefRequestHandler(jcefBrowser)
26+
27+
init {
28+
assetRequestHandler.addWildcardHandler("mynah") { path ->
29+
val asset = path.replaceFirst("mynah/", "/mynah-ui/assets/")
30+
Paths.get(asset).normalize().toString().replace("\\", "/").let {
31+
this::class.java.getResourceAsStream(it)
32+
}
33+
}
34+
}
35+
2436
fun init(
2537
isCodeTransformAvailable: Boolean,
2638
isFeatureDevAvailable: Boolean,
@@ -29,14 +41,6 @@ class Browser(parent: Disposable) : Disposable {
2941
isCodeTestAvailable: Boolean,
3042
highlightCommand: HighlightCommand?,
3143
) {
32-
// register the scheme handler to route http://mynah/ URIs to the resources/assets directory on classpath
33-
CefApp.getInstance()
34-
.registerSchemeHandlerFactory(
35-
"http",
36-
"mynah",
37-
AssetResourceHandler.AssetResourceHandlerFactory(),
38-
)
39-
4044
loadWebView(isCodeTransformAvailable, isFeatureDevAvailable, isDocAvailable, isCodeScanAvailable, isCodeTestAvailable, highlightCommand)
4145
}
4246

@@ -63,9 +67,13 @@ class Browser(parent: Disposable) : Disposable {
6367
// setup empty state. The message request handlers use this for storing state
6468
// that's persistent between page loads.
6569
jcefBrowser.setProperty("state", "")
70+
6671
// load the web app
67-
jcefBrowser.loadHTML(
68-
getWebviewHTML(isCodeTransformAvailable, isFeatureDevAvailable, isDocAvailable, isCodeScanAvailable, isCodeTestAvailable, highlightCommand)
72+
jcefBrowser.loadURL(
73+
assetRequestHandler.createResource(
74+
"webview/chat.html",
75+
getWebviewHTML(isCodeTransformAvailable, isFeatureDevAvailable, isDocAvailable, isCodeScanAvailable, isCodeTestAvailable, highlightCommand)
76+
)
6977
)
7078
}
7179

@@ -84,7 +92,7 @@ class Browser(parent: Disposable) : Disposable {
8492
val postMessageToJavaJsCode = receiveMessageQuery.inject("JSON.stringify(message)")
8593

8694
val jsScripts = """
87-
<script type="text/javascript" src="$WEB_SCRIPT_URI" defer onload="init()"></script>
95+
<script type="text/javascript" src="http://toolkitasset/mynah/js/mynah-ui.js" defer onload="init()"></script>
8896
<script type="text/javascript">
8997
const init = () => {
9098
mynahUI.createMynahUI(
@@ -120,7 +128,6 @@ class Browser(parent: Disposable) : Disposable {
120128
}
121129

122130
companion object {
123-
private const val WEB_SCRIPT_URI = "http://mynah/js/mynah-ui.js"
124131
private const val MAX_ONBOARDING_PAGE_COUNT = 3
125132
private val OBJECT_MAPPER = jacksonObjectMapper()
126133
}

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqCodeTest/controller/CodeTestChatController.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ class CodeTestChatController(
130130

131131
ApplicationManager.getApplication().invokeAndWait {
132132
selectionRange = ApplicationManager.getApplication().runReadAction<Range?> {
133-
val editor = FileEditorManager.getInstance(project).selectedTextEditor
133+
val editor = FileEditorManager.getInstance(project).selectedTextEditorWithRemotes.firstOrNull()
134134
editor?.let {
135135
val selectionModel = it.selectionModel
136136
val startOffset = selectionModel.selectionStart

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ class FeatureDevController(
207207

208208
withContext(EDT) {
209209
broadcastQEvent(QFeatureEvent.STARTS_EDITING)
210-
val editor: Editor = FileEditorManager.getInstance(context.project).selectedTextEditor ?: return@withContext
210+
val editor: Editor = FileEditorManager.getInstance(context.project).selectedTextEditorWithRemotes.firstOrNull() ?: return@withContext
211211

212212
val caret: Caret = editor.caretModel.primaryCaret
213213
val offset: Int = caret.offset

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/controller/ChatController.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@ class ChatController private constructor(
209209
override suspend fun processInsertCodeAtCursorPosition(message: IncomingCwcMessage.InsertCodeAtCursorPosition) {
210210
broadcastQEvent(QFeatureEvent.STARTS_EDITING)
211211
withContext(EDT) {
212-
val editor: Editor = FileEditorManager.getInstance(context.project).selectedTextEditor ?: return@withContext
212+
val editor: Editor = FileEditorManager.getInstance(context.project).selectedTextEditorWithRemotes.firstOrNull() ?: return@withContext
213213

214214
val caret: Caret = editor.caretModel.primaryCaret
215215
val offset: Int = caret.offset

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/file/FileContextExtractor.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class FileContextExtractor(private val fqnWebviewAdapter: FqnWebviewAdapter?, pr
1818
private val languageExtractor: LanguageExtractor = LanguageExtractor()
1919
suspend fun extract(): FileContext? {
2020
val editor = computeOnEdt {
21-
FileEditorManager.getInstance(project).selectedTextEditor
21+
FileEditorManager.getInstance(project).selectedTextEditorWithRemotes.firstOrNull()
2222
} ?: return null
2323

2424
val fileLanguage = computeOnEdt {

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/editor/context/focusArea/FocusAreaContextExtractor.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class FocusAreaContextExtractor(private val fqnWebviewAdapter: FqnWebviewAdapter
2626
private val languageExtractor: LanguageExtractor = LanguageExtractor()
2727
suspend fun extract(): FocusAreaContext? {
2828
val editor = computeOnEdt {
29-
FileEditorManager.getInstance(project).selectedTextEditor
29+
FileEditorManager.getInstance(project).selectedTextEditorWithRemotes.firstOrNull()
3030
} ?: return null
3131

3232
if (editor.document.text.isBlank()) return null
@@ -109,7 +109,7 @@ class FocusAreaContextExtractor(private val fqnWebviewAdapter: FqnWebviewAdapter
109109
languageExtractor.extractLanguageNameFromCurrentFile(editor)
110110
}
111111
val fileText = editor.document.text
112-
val fileName = FileEditorManager.getInstance(project).selectedFiles.first().name
112+
val fileName = editor.virtualFile.name
113113

114114
// Offset the selection range to the start of the trimmedFileText
115115
val selectionInsideTrimmedFileTextRange = codeSelectionRange.let {

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatFileListener.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class InlineChatFileListener(project: Project, private val controller: InlineCha
1818
private var selectionListener: InlineChatSelectionListener? = null
1919

2020
init {
21-
val editor = project.let { FileEditorManager.getInstance(it).selectedTextEditor }
21+
val editor = project.let { FileEditorManager.getInstance(it).selectedTextEditorWithRemotes.firstOrNull() }
2222
if (editor != null) {
2323
setupListenersForEditor(editor)
2424
currentEditor = editor

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/cwc/inline/listeners/InlineChatSelectionListener.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@
33

44
package software.aws.toolkits.jetbrains.services.cwc.inline.listeners
55

6-
import com.intellij.idea.AppMode
76
import com.intellij.openapi.Disposable
87
import com.intellij.openapi.editor.event.SelectionEvent
98
import com.intellij.openapi.editor.event.SelectionListener
109
import software.aws.toolkits.jetbrains.services.cwc.inline.InlineChatEditorHint
10+
import software.aws.toolkits.jetbrains.utils.isRunningOnRemoteBackend
1111

1212
class InlineChatSelectionListener : SelectionListener, Disposable {
1313
private val inlineChatEditorHint = InlineChatEditorHint()
1414
override fun selectionChanged(e: SelectionEvent) {
15-
if (AppMode.isRemoteDevHost()) return
15+
if (isRunningOnRemoteBackend()) return
1616
val editor = e.editor
1717
val selectionModel = editor.selectionModel
1818

plugins/amazonq/codetransform/jetbrains-community/resources/META-INF/codetransform-ext-java.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
factoryClass="software.aws.toolkits.jetbrains.services.codemodernizer.toolwindow.CodeModernizerBottomToolWindowFactory"
2121
icon="AllIcons.Actions.Preview"/>
2222
<fileEditorProvider implementation="software.aws.toolkits.jetbrains.services.codemodernizer.plan.CodeModernizerPlanEditorProvider"/>
23-
<fileEditorProvider implementation="software.aws.toolkits.jetbrains.services.codemodernizer.summary.CodeModernizerSummaryEditorProvider"/>
2423
</extensions>
2524

2625
<extensions defaultExtensionNs="amazon.q">

plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/CodeModernizerManager.kt

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ import software.aws.toolkits.jetbrains.services.codemodernizer.utils.parseXmlDep
7777
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.setDependencyVersionInPom
7878
import software.aws.toolkits.jetbrains.services.codemodernizer.utils.tryGetJdk
7979
import software.aws.toolkits.jetbrains.ui.feedback.CodeTransformFeedbackDialog
80-
import software.aws.toolkits.jetbrains.utils.isRunningOnRemoteBackend
8180
import software.aws.toolkits.jetbrains.utils.notifyStickyError
8281
import software.aws.toolkits.jetbrains.utils.notifyStickyInfo
8382
import software.aws.toolkits.resources.message
@@ -134,14 +133,6 @@ class CodeModernizerManager(private val project: Project) : PersistentStateCompo
134133

135134
fun validate(project: Project, transformationType: CodeTransformType): ValidationResult {
136135
fun validateCore(project: Project): ValidationResult {
137-
if (isRunningOnRemoteBackend()) {
138-
return ValidationResult(
139-
false,
140-
InvalidTelemetryReason(
141-
CodeTransformPreValidationError.RemoteRunProject,
142-
)
143-
)
144-
}
145136
if (!isCodeTransformAvailable(project)) {
146137
return ValidationResult(
147138
false,

plugins/amazonq/codetransform/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codemodernizer/plan/CodeModernizerPlanEditorProvider.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import com.intellij.openapi.vfs.VirtualFile
1515
import software.amazon.awssdk.services.codewhispererruntime.model.TransformationPlan
1616
import software.aws.toolkits.core.utils.debug
1717
import software.aws.toolkits.core.utils.getLogger
18-
import software.aws.toolkits.jetbrains.utils.isRunningOnRemoteBackend
1918

2019
class CodeModernizerPlanEditorProvider : FileEditorProvider, DumbAware {
2120
override fun accept(project: Project, file: VirtualFile) = file is CodeModernizerPlanVirtualFile
@@ -31,8 +30,8 @@ class CodeModernizerPlanEditorProvider : FileEditorProvider, DumbAware {
3130
val MIGRATION_PLAN_KEY = Key.create<TransformationPlan>("TRANSFORMATION_PLAN")
3231
val MODULE_NAME_KEY = Key.create<String>("MODULE_NAME")
3332
val JAVA_VERSION = Key.create<String>("JAVA_VERSION")
33+
3434
fun openEditor(project: Project, plan: TransformationPlan, module: String?, javaVersionNumber: String) {
35-
if (isRunningOnRemoteBackend()) return
3635
val virtualFile = CodeModernizerPlanVirtualFile()
3736
virtualFile.putUserData(MIGRATION_PLAN_KEY, plan)
3837
virtualFile.putUserData(MODULE_NAME_KEY, module)

0 commit comments

Comments
 (0)