Skip to content

feat(amazonq): client emits copyCodeToClipboard for chat copy events #5589

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

Merged
merged 22 commits into from
Apr 25, 2025
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fe7738d
Set up Flare chat connection
manodnyab Apr 9, 2025
c09368c
Merge branch 'feature/q-lsp' into manodnyb/setupChatComponentsWithFlare
manodnyab Apr 9, 2025
9d037cf
Partial chat results
manodnyab Apr 9, 2025
e259071
Merge remote-tracking branch 'origin/manodnyb/setupChatComponentsWith…
manodnyab Apr 9, 2025
bc6823b
feedback
manodnyab Apr 10, 2025
b833b6c
resolved merge conflict
manodnyab Apr 10, 2025
fb9fd25
detekt
manodnyab Apr 10, 2025
46b9a82
Merge branch 'feature/q-lsp-chat' into manodnyb/setupChatComponentsWi…
manodnyab Apr 10, 2025
06dc03d
syntax error
manodnyab Apr 10, 2025
0f16cd4
Merge remote-tracking branch 'origin/manodnyb/setupChatComponentsWith…
manodnyab Apr 11, 2025
21e4885
detekt
manodnyab Apr 11, 2025
06e0966
detekt
manodnyab Apr 11, 2025
8640432
Add quick actions to chat
manodnyab Apr 11, 2025
930fff6
merge conflicts resolved
manodnyab Apr 14, 2025
28a199e
adding commands from initialize result
manodnyab Apr 14, 2025
98a43b5
added trace logging
manodnyab Apr 14, 2025
a7cc967
emit copyCodeToClipboard for chat
samgst-amazon Apr 16, 2025
ab2100c
Merge remote-tracking branch 'origin/feature/q-lsp-chat' into samgst/…
samgst-amazon Apr 21, 2025
46c12b6
feedback
samgst-amazon Apr 21, 2025
18f1567
Merge remote-tracking branch 'origin/feature/q-lsp-chat' into samgst/…
samgst-amazon Apr 22, 2025
418ad45
Merge branch 'feature/q-lsp-chat' into samgst/q-chat-copy-to-clipboard
samgst-amazon Apr 25, 2025
5813dad
Merge branch 'feature/q-lsp-chat' into samgst/q-chat-copy-to-clipboard
samgst-amazon Apr 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.ui.components.JBLoadingPanel
import com.intellij.ui.components.JBPanelWithEmptyText
Expand All @@ -24,7 +25,7 @@ import java.awt.event.ActionListener
import java.util.concurrent.CompletableFuture
import javax.swing.JButton

class AmazonQPanel(private val parent: Disposable) {
class AmazonQPanel(private val parent: Disposable, val project: Project) {
private val webviewContainer = Wrapper()
val browser = CompletableFuture<Browser>()

Expand Down Expand Up @@ -91,7 +92,7 @@ class AmazonQPanel(private val parent: Disposable) {
loadingPanel.stopLoading()
runInEdt {
browser.complete(
Browser(parent, webUri).also {
Browser(parent, webUri, project).also {
wrapper.setContent(it.component())
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ class AmazonQToolWindow private constructor(
private val browserConnector = BrowserConnector(project = project)
private val editorThemeAdapter = EditorThemeAdapter()

private val chatPanel = AmazonQPanel(parent = this)
private val chatPanel = AmazonQPanel(parent = this, project)

val component: JComponent = chatPanel.component

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ package software.aws.toolkits.jetbrains.services.amazonq.webview

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.intellij.openapi.Disposable
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.ui.jcef.JBCefJSQuery
import org.cef.CefApp
import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService
import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.AwsServerCapabilitiesProvider
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
Expand All @@ -17,7 +20,8 @@ import java.net.URI
/*
Displays the web view for the Amazon Q tool window
*/
class Browser(parent: Disposable, private val webUri: URI) : Disposable {

class Browser(parent: Disposable, private val webUri: URI, val project: Project) : Disposable {

val jcefBrowser = createBrowser(parent)

Expand All @@ -39,8 +43,17 @@ class Browser(parent: Disposable, private val webUri: URI) : Disposable {
"mynah",
AssetResourceHandler.AssetResourceHandlerFactory(),
)

loadWebView(isCodeTransformAvailable, isFeatureDevAvailable, isDocAvailable, isCodeScanAvailable, isCodeTestAvailable, highlightCommand, activeProfile)
AmazonQLspService.getInstance(project).addServerStartedListener {
loadWebView(
isCodeTransformAvailable,
isFeatureDevAvailable,
isDocAvailable,
isCodeScanAvailable,
isCodeTestAvailable,
highlightCommand,
activeProfile
)
}
}

override fun dispose() {
Expand Down Expand Up @@ -101,6 +114,7 @@ class Browser(parent: Disposable, private val webUri: URI) : Disposable {
highlightCommand: HighlightCommand?,
activeProfile: QRegionProfile?,
): String {
val quickActionConfig = generateQuickActionConfig()
val postMessageToJavaJsCode = receiveMessageQuery.inject("JSON.stringify(message)")
val jsScripts = """
<script type="text/javascript" src="$webUri" defer onload="init()"></script>
Expand All @@ -113,7 +127,7 @@ class Browser(parent: Disposable, private val webUri: URI) : Disposable {
}
},
{
quickActionCommands: [],
quickActionCommands: $quickActionConfig,
disclaimerAcknowledged: ${MeetQSettings.getInstance().disclaimerAcknowledged}
}
);
Expand Down Expand Up @@ -220,6 +234,10 @@ class Browser(parent: Disposable, private val webUri: URI) : Disposable {
activeProfile
}

private fun generateQuickActionConfig() = AwsServerCapabilitiesProvider.getInstance(project).getChatOptions().quickActions.quickActionsCommandGroups
.let { OBJECT_MAPPER.writeValueAsString(it) }
?: "[]"

companion object {
private const val MAX_ONBOARDING_PAGE_COUNT = 3
private val OBJECT_MAPPER = jacksonObjectMapper()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.future.await

Check warning on line 20 in plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused import directive

Unused import directive
import kotlinx.coroutines.launch
import org.cef.browser.CefBrowser
import org.eclipse.lsp4j.Position
Expand All @@ -27,10 +28,15 @@
import software.aws.toolkits.jetbrains.services.amazonq.lsp.encryption.JwtEncryptionManager
import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.ChatCommunicationManager
import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.getTextDocumentIdentifier
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_COPY_CODE_TO_CLIPBOARD
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CHAT_QUICK_ACTION
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ChatParams
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ChatPrompt
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CopyCodeToClipboardNotification
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CursorState
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.EncryptedChatParams
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.EncryptedQuickActionChatParams
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.QuickChatActionRequest
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SEND_CHAT_COMMAND_PROMPT
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.SendChatPromptRequest
import software.aws.toolkits.jetbrains.services.amazonq.util.command
Expand Down Expand Up @@ -167,32 +173,65 @@
)
)

val partialResultToken = chatCommunicationManager.addPartialChatMessage(requestFromUi.params.tabId)
val chatParams = ChatParams(
requestFromUi.params.tabId,
chatPrompt,
textDocumentIdentifier,
cursorState
)

val tabId = requestFromUi.params.tabId
val partialResultToken = chatCommunicationManager.addPartialChatMessage(tabId)

var encryptionManager: JwtEncryptionManager? = null
val result = AmazonQLspService.executeIfRunning(project) { server ->
encryptionManager = this.encryptionManager
encryptionManager?.encrypt(chatParams)?.let { EncryptedChatParams(it, partialResultToken) }?.let { server.sendChatPrompt(it) }
} ?: (CompletableFuture.failedFuture(IllegalStateException("LSP Server not running")))
showResult(result, partialResultToken, tabId, encryptionManager, browser)
}
CHAT_QUICK_ACTION -> {
val requestFromUi = serializer.deserializeChatMessages(node, QuickChatActionRequest::class.java)
val tabId = requestFromUi.params.tabId
val quickActionParams = requestFromUi.params
val partialResultToken = chatCommunicationManager.addPartialChatMessage(tabId)
var encryptionManager: JwtEncryptionManager? = null
val result = AmazonQLspService.executeIfRunning(project) { server ->
encryptionManager = this.encryptionManager
encryptionManager?.encrypt(quickActionParams)?.let {
EncryptedQuickActionChatParams(it, partialResultToken)
}?.let {
server.sendQuickAction(it)
}
} ?: (CompletableFuture.failedFuture(IllegalStateException("LSP Server not running")))

result.whenComplete {
value, error ->
chatCommunicationManager.removePartialChatMessage(partialResultToken)
val messageToChat = ChatCommunicationManager.convertToJsonToSendToChat(
node.command,
requestFromUi.params.tabId,
encryptionManager?.decrypt(value).orEmpty(),
isPartialResult = false
)
browser.postChat(messageToChat)
}
showResult(result, partialResultToken, tabId, encryptionManager, browser)
}
CHAT_COPY_CODE_TO_CLIPBOARD -> {
val requestFromUi = serializer.deserializeChatMessages(node, CopyCodeToClipboardNotification::class.java)

Check warning on line 211 in plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt

View check run for this annotation

Codecov / codecov/patch

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt#L211

Added line #L211 was not covered by tests
AmazonQLspService.executeIfRunning(project) { server ->
server.copyCodeToClipboard(requestFromUi.params)
} ?: CompletableFuture.failedFuture<Unit>(IllegalStateException("LSP Server not running"))

Check notice on line 214 in plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unnecessary type argument

Remove explicit type arguments

Check warning on line 214 in plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt

View check run for this annotation

Codecov / codecov/patch

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt#L213-L214

Added lines #L213 - L214 were not covered by tests
}
}
}

private fun showResult(
result: CompletableFuture<String>,
partialResultToken: String,
tabId: String,
encryptionManager: JwtEncryptionManager?,
browser: Browser,
) {
result.whenComplete { value, error ->
chatCommunicationManager.removePartialChatMessage(partialResultToken)
val messageToChat = ChatCommunicationManager.convertToJsonToSendToChat(
SEND_CHAT_COMMAND_PROMPT,
tabId,
encryptionManager?.decrypt(value).orEmpty(),
isPartialResult = false
)
browser.postChat(messageToChat)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import org.eclipse.lsp4j.jsonrpc.services.JsonRequest
import org.eclipse.lsp4j.services.LanguageServer
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.GetConfigurationFromServerParams
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.LspServerConfigurations
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CopyCodeToClipboardParams
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.EncryptedChatParams
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.EncryptedQuickActionChatParams
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.UpdateCredentialsPayload
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.dependencies.DidChangeDependencyPathsParams
import java.util.concurrent.CompletableFuture
Expand All @@ -33,4 +35,10 @@ interface AmazonQLanguageServer : LanguageServer {

@JsonRequest("aws/chat/sendChatPrompt")
fun sendChatPrompt(params: EncryptedChatParams): CompletableFuture<String>

@JsonRequest("aws/chat/sendChatQuickAction")
fun sendQuickAction(params: EncryptedQuickActionChatParams): CompletableFuture<String>

@JsonNotification("aws/chat/copyCodeToClipboard")
fun copyCodeToClipboard(params: CopyCodeToClipboardParams): CompletableFuture<Unit>
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@
import org.eclipse.lsp4j.TextDocumentClientCapabilities
import org.eclipse.lsp4j.WorkspaceClientCapabilities
import org.eclipse.lsp4j.jsonrpc.Launcher
import org.eclipse.lsp4j.launch.LSPLauncher
import org.eclipse.lsp4j.jsonrpc.Launcher.Builder
import org.eclipse.lsp4j.jsonrpc.MessageConsumer
import org.eclipse.lsp4j.jsonrpc.messages.Message
import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage
import org.slf4j.event.Level
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.info
Expand All @@ -50,6 +53,9 @@
import software.aws.toolkits.jetbrains.services.amazonq.lsp.auth.DefaultAuthCredentialsService
import software.aws.toolkits.jetbrains.services.amazonq.lsp.dependencies.DefaultModuleDependenciesService
import software.aws.toolkits.jetbrains.services.amazonq.lsp.encryption.JwtEncryptionManager
import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.AmazonQLspTypeAdapterFactory
import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.AwsExtendedInitializeResult
import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.AwsServerCapabilitiesProvider
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.createExtendedClientMetadata
import software.aws.toolkits.jetbrains.services.amazonq.lsp.textdocument.TextDocumentServiceHandler
import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.WorkspaceFolderUtil.createWorkspaceFolders
Expand Down Expand Up @@ -101,8 +107,12 @@

@Service(Service.Level.PROJECT)
class AmazonQLspService(private val project: Project, private val cs: CoroutineScope) : Disposable {
private val serverStartedListener = mutableListOf<AmazonQServerStartedListener>()
fun addServerStartedListener(listener: AmazonQServerStartedListener) = serverStartedListener.add(listener)
fun notifyServerStarted() = serverStartedListener.forEach { it() }

private var instance: Deferred<AmazonQServerInstance>
val capabilities

Check notice on line 115 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Function or property has platform type

Declaration has type inferred from a platform call, which can lead to unchecked nullability issues. Specify type explicitly as nullable or non-nullable.

Check warning on line 115 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused symbol

Property "capabilities" is never used
get() = instance.getCompleted().initializeResult.getCompleted().capabilities
val encryptionManager
get() = instance.getCompleted().encryptionManager
Expand Down Expand Up @@ -141,7 +151,7 @@
override fun dispose() {
}

suspend fun restart() = mutex.withLock {

Check warning on line 154 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused symbol

Function "restart" is never used
// stop if running
instance.let {
if (it.isActive) {
Expand Down Expand Up @@ -195,7 +205,7 @@
}
}

private class AmazonQServerInstance(private val project: Project, private val cs: CoroutineScope) : Disposable {

Check warning on line 208 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Constructor parameter is never used as a property

Constructor parameter is never used as a property
val encryptionManager = JwtEncryptionManager()

private val launcher: Launcher<AmazonQLanguageServer>
Expand Down Expand Up @@ -265,7 +275,23 @@
launcherHandler.addProcessListener(inputWrapper)
launcherHandler.startNotify()

launcher = LSPLauncher.Builder<AmazonQLanguageServer>()
class AmazonQServerBuilder : Builder<AmazonQLanguageServer>() {
private val customMessageTracer = MessageTracer()

override fun wrapMessageConsumer(consumer: MessageConsumer?): MessageConsumer =
super.wrapMessageConsumer { message ->
customMessageTracer.trace("INCOMING", message)
if (message is ResponseMessage && message.result is AwsExtendedInitializeResult) {
val result = message.result as AwsExtendedInitializeResult
AwsServerCapabilitiesProvider.getInstance(project).setAwsServerCapabilities(result.getAwsServerCapabilities())
AmazonQLspService.getInstance(project).notifyServerStarted()
}
consumer?.consume(message)
customMessageTracer.trace("PROCESSED", message)
}
}

launcher = AmazonQServerBuilder()
.setLocalService(AmazonQLanguageClientImpl(project))
.setRemoteInterface(AmazonQLanguageServer::class.java)
.configureGson {
Expand All @@ -274,6 +300,7 @@

// otherwise Gson treats all numbers as double which causes deser issues
it.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
it.registerTypeAdapterFactory(AmazonQLspTypeAdapterFactory())
}.traceMessages(
PrintWriter(
object : StringWriter() {
Expand All @@ -298,7 +325,7 @@

val initializeResult = try {
withTimeout(5.seconds) {
languageServer.initialize(createInitializeParams()).await()

Check warning on line 328 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Usage of redundant or deprecated syntax or deprecated symbols

'await(): T' is deprecated. Please migrate to using kotlinx.coroutines.future.await instead. The deprecation level is going to be changed to ERROR in as soon as there's no more usages in the monorepo.
}
} catch (_: TimeoutCancellationException) {
LOG.warn { "LSP initialization timed out" }
Expand Down Expand Up @@ -348,7 +375,21 @@
}
}

class MessageTracer {
private val traceLogger = LOG.atLevel(if (isDeveloperMode()) Level.INFO else Level.DEBUG)

fun trace(direction: String, message: Message) {
traceLogger.log {
buildString {
append("$direction: ")
append(message.toString())
}
}
}
}
companion object {
private val LOG = getLogger<AmazonQServerInstance>()
}
}

typealias AmazonQServerStartedListener = () -> Unit
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
@file:Suppress("BannedImports")
package software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat

import com.google.gson.Gson
import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import org.eclipse.lsp4j.InitializeResult
import java.io.IOException

class AmazonQLspTypeAdapterFactory : TypeAdapterFactory {
override fun <T : Any?> create(gson: Gson, type: com.google.gson.reflect.TypeToken<T>): TypeAdapter<T>? {
if (type.rawType === InitializeResult::class.java) {
val delegate: TypeAdapter<InitializeResult?> = gson.getDelegateAdapter(this, type) as TypeAdapter<InitializeResult?>

return object : TypeAdapter<InitializeResult>() {
@Throws(IOException::class)
override fun write(out: JsonWriter?, value: InitializeResult?) {
delegate.write(out, value)
}

@Throws(IOException::class)
override fun read(`in`: JsonReader?): InitializeResult =
gson.fromJson(`in`, AwsExtendedInitializeResult::class.java)
} as TypeAdapter<T>
}
return null
}
}

class AwsExtendedInitializeResult : InitializeResult() {
private var awsServerCapabilities: AwsServerCapabilities? = null

fun getAwsServerCapabilities(): AwsServerCapabilities? = awsServerCapabilities

fun setAwsServerCapabilities(awsServerCapabilities: AwsServerCapabilities?) {

Check warning on line 39 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/flareChat/AmazonQLspTypeAdapterFactory.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused symbol

Function "setAwsServerCapabilities" is never used
this.awsServerCapabilities = awsServerCapabilities
}
}
Loading
Loading