@@ -20,13 +20,18 @@ import kotlinx.coroutines.channels.awaitClose
2020import kotlinx.coroutines.coroutineScope
2121import kotlinx.coroutines.flow.Flow
2222import kotlinx.coroutines.flow.callbackFlow
23+ import kotlinx.coroutines.flow.catch
2324import kotlinx.coroutines.flow.distinctUntilChanged
2425import kotlinx.coroutines.flow.launchIn
2526import kotlinx.coroutines.flow.merge
2627import kotlinx.coroutines.flow.onEach
28+ import kotlinx.coroutines.flow.timeout
29+ import kotlinx.coroutines.flow.toList
2730import kotlinx.coroutines.launch
31+ import kotlinx.coroutines.runBlocking
2832import org.cef.browser.CefBrowser
2933import org.eclipse.lsp4j.TextDocumentIdentifier
34+ import org.eclipse.lsp4j.jsonrpc.JsonRpcException
3035import org.eclipse.lsp4j.jsonrpc.ResponseErrorException
3136import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode
3237import software.aws.toolkits.core.utils.error
@@ -37,6 +42,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.apps.AppConnection
3742import software.aws.toolkits.jetbrains.services.amazonq.commands.MessageSerializer
3843import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQChatServer
3944import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService
45+ import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQServerInstanceFacade
4046import software.aws.toolkits.jetbrains.services.amazonq.lsp.JsonRpcMethod
4147import software.aws.toolkits.jetbrains.services.amazonq.lsp.JsonRpcNotification
4248import software.aws.toolkits.jetbrains.services.amazonq.lsp.JsonRpcRequest
@@ -114,6 +120,7 @@ import software.aws.toolkits.telemetry.Telemetry
114120import java.util.concurrent.CompletableFuture
115121import java.util.concurrent.CompletionException
116122import java.util.function.Function
123+ import kotlin.time.Duration.Companion.milliseconds
117124
118125class BrowserConnector (
119126 private val serializer : MessageSerializer = MessageSerializer .getInstance(),
@@ -237,43 +244,20 @@ class BrowserConnector(
237244 val chatParams: ObjectNode = (node.params as ObjectNode )
238245 .setAll(serializedEnrichmentParams)
239246
240- val tabId = requestFromUi.params.tabId
241- val partialResultToken = chatCommunicationManager.addPartialChatMessage(tabId)
242- chatCommunicationManager.registerPartialResultToken(partialResultToken)
243-
244- var encryptionManager: JwtEncryptionManager ? = null
245- val result = AmazonQLspService .executeAsyncIfRunning(project) { server ->
246- encryptionManager = this .encryptionManager
247-
248- val encryptedParams = EncryptedChatParams (this .encryptionManager.encrypt(chatParams), partialResultToken)
249- rawEndpoint.request(SEND_CHAT_COMMAND_PROMPT , encryptedParams) as CompletableFuture <String >
250- } ? : (CompletableFuture .failedFuture(IllegalStateException (" LSP Server not running" )))
251-
252- // We assume there is only one outgoing request per tab because the input is
253- // blocked when there is an outgoing request
254- chatCommunicationManager.setInflightRequestForTab(tabId, result)
255- showResult(result, partialResultToken, tabId, encryptionManager, browser)
247+ doChatRequest(requestFromUi.params.tabId, browser) { serverFacade, partialResultToken ->
248+ val encryptedParams = EncryptedChatParams (serverFacade.encryptionManager.encrypt(chatParams), partialResultToken)
249+ (serverFacade.rawEndpoint.request(SEND_CHAT_COMMAND_PROMPT , encryptedParams) as CompletableFuture <String >)
250+ }
256251 }
257252
258253 CHAT_QUICK_ACTION -> {
259- val requestFromUi = serializer.deserializeChatMessages<QuickChatActionRequest >(node)
260- val tabId = requestFromUi.params.tabId
261254 val quickActionParams = node.params ? : error(" empty payload" )
262- val partialResultToken = chatCommunicationManager.addPartialChatMessage(tabId)
263- chatCommunicationManager.registerPartialResultToken(partialResultToken)
264- var encryptionManager: JwtEncryptionManager ? = null
265- val result = AmazonQLspService .executeAsyncIfRunning(project) { server ->
266- encryptionManager = this .encryptionManager
267-
268- val encryptedParams = EncryptedQuickActionChatParams (this .encryptionManager.encrypt(quickActionParams), partialResultToken)
269- rawEndpoint.request(CHAT_QUICK_ACTION , encryptedParams) as CompletableFuture <String >
270- } ? : (CompletableFuture .failedFuture(IllegalStateException (" LSP Server not running" )))
271-
272- // We assume there is only one outgoing request per tab because the input is
273- // blocked when there is an outgoing request
274- chatCommunicationManager.setInflightRequestForTab(tabId, result)
255+ val requestFromUi = serializer.deserializeChatMessages<QuickChatActionRequest >(node)
275256
276- showResult(result, partialResultToken, tabId, encryptionManager, browser)
257+ doChatRequest(requestFromUi.params.tabId, browser) { serverFacade, partialResultToken ->
258+ val encryptedParams = EncryptedQuickActionChatParams (serverFacade.encryptionManager.encrypt(quickActionParams), partialResultToken)
259+ serverFacade.rawEndpoint.request(CHAT_QUICK_ACTION , encryptedParams) as CompletableFuture <String >
260+ }
277261 }
278262
279263 CHAT_LIST_CONVERSATIONS -> {
@@ -465,7 +449,6 @@ class BrowserConnector(
465449 AUTH_FOLLOW_UP_CLICKED -> {
466450 val message = serializer.deserializeChatMessages<AuthFollowUpClickNotification >(node)
467451 chatCommunicationManager.handleAuthFollowUpClicked(
468- project,
469452 message.params
470453 )
471454 }
@@ -564,18 +547,44 @@ class BrowserConnector(
564547 }
565548 }
566549
567- private fun showResult (
568- result : CompletableFuture <String >,
569- partialResultToken : String ,
550+ private suspend fun doChatRequest (
570551 tabId : String ,
571- encryptionManager : JwtEncryptionManager ? ,
572552 browser : Browser ,
553+ action : (AmazonQServerInstanceFacade , String ) -> CompletableFuture <String >,
573554 ) {
555+ val partialResultToken = chatCommunicationManager.addPartialChatMessage(tabId)
556+ chatCommunicationManager.registerPartialResultToken(partialResultToken)
557+ var encryptionManager: JwtEncryptionManager ? = null
558+ val result = AmazonQLspService .executeAsyncIfRunning(project) { _ ->
559+ // jank
560+ encryptionManager = this @executeAsyncIfRunning.encryptionManager
561+ action(this , partialResultToken)
562+ .handle { result, ex ->
563+ if (ex == null ) {
564+ return @handle result
565+ }
566+
567+ if (JsonRpcException .indicatesStreamClosed(ex)) {
568+ // the flow buffer will never complete so insert some arbitrary timeout until we figure out how to end the flow
569+ // after the error stream is closed and drained
570+ val errorStream = runBlocking { this @executeAsyncIfRunning.errorStream.timeout(500 .milliseconds).catch { }.toList() }
571+ throw RuntimeException (" LSP execution error. See logs for more details: ${errorStream.joinToString(separator = " " )} " , ex.cause)
572+ }
573+
574+ throw ex
575+ }
576+ } ? : (CompletableFuture .failedFuture(IllegalStateException (" LSP Server not running" )))
577+
578+ // We assume there is only one outgoing request per tab because the input is
579+ // blocked when there is an outgoing request
580+ chatCommunicationManager.setInflightRequestForTab(tabId, result)
581+
574582 result.whenComplete { value, error ->
575583 try {
576584 if (error != null ) {
577585 throw error
578586 }
587+
579588 chatCommunicationManager.removePartialChatMessage(partialResultToken)
580589 val messageToChat = ChatCommunicationManager .convertToJsonToSendToChat(
581590 SEND_CHAT_COMMAND_PROMPT ,
@@ -585,7 +594,7 @@ class BrowserConnector(
585594 )
586595 browser.postChat(messageToChat)
587596 chatCommunicationManager.removeInflightRequestForTab(tabId)
588- } catch (e : CancellationException ) {
597+ } catch (_ : CancellationException ) {
589598 LOG .warn { " Cancelled chat generation" }
590599 try {
591600 chatAsyncResultManager.createRequestId(partialResultToken)
0 commit comments