@@ -20,13 +20,18 @@ import kotlinx.coroutines.channels.awaitClose
20
20
import kotlinx.coroutines.coroutineScope
21
21
import kotlinx.coroutines.flow.Flow
22
22
import kotlinx.coroutines.flow.callbackFlow
23
+ import kotlinx.coroutines.flow.catch
23
24
import kotlinx.coroutines.flow.distinctUntilChanged
24
25
import kotlinx.coroutines.flow.launchIn
25
26
import kotlinx.coroutines.flow.merge
26
27
import kotlinx.coroutines.flow.onEach
28
+ import kotlinx.coroutines.flow.timeout
29
+ import kotlinx.coroutines.flow.toList
27
30
import kotlinx.coroutines.launch
31
+ import kotlinx.coroutines.runBlocking
28
32
import org.cef.browser.CefBrowser
29
33
import org.eclipse.lsp4j.TextDocumentIdentifier
34
+ import org.eclipse.lsp4j.jsonrpc.JsonRpcException
30
35
import org.eclipse.lsp4j.jsonrpc.ResponseErrorException
31
36
import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode
32
37
import software.aws.toolkits.core.utils.error
@@ -37,6 +42,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.apps.AppConnection
37
42
import software.aws.toolkits.jetbrains.services.amazonq.commands.MessageSerializer
38
43
import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQChatServer
39
44
import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService
45
+ import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQServerInstanceFacade
40
46
import software.aws.toolkits.jetbrains.services.amazonq.lsp.JsonRpcMethod
41
47
import software.aws.toolkits.jetbrains.services.amazonq.lsp.JsonRpcNotification
42
48
import software.aws.toolkits.jetbrains.services.amazonq.lsp.JsonRpcRequest
@@ -114,6 +120,7 @@ import software.aws.toolkits.telemetry.Telemetry
114
120
import java.util.concurrent.CompletableFuture
115
121
import java.util.concurrent.CompletionException
116
122
import java.util.function.Function
123
+ import kotlin.time.Duration.Companion.milliseconds
117
124
118
125
class BrowserConnector (
119
126
private val serializer : MessageSerializer = MessageSerializer .getInstance(),
@@ -237,43 +244,20 @@ class BrowserConnector(
237
244
val chatParams: ObjectNode = (node.params as ObjectNode )
238
245
.setAll(serializedEnrichmentParams)
239
246
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
+ }
256
251
}
257
252
258
253
CHAT_QUICK_ACTION -> {
259
- val requestFromUi = serializer.deserializeChatMessages<QuickChatActionRequest >(node)
260
- val tabId = requestFromUi.params.tabId
261
254
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)
275
256
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
+ }
277
261
}
278
262
279
263
CHAT_LIST_CONVERSATIONS -> {
@@ -465,7 +449,6 @@ class BrowserConnector(
465
449
AUTH_FOLLOW_UP_CLICKED -> {
466
450
val message = serializer.deserializeChatMessages<AuthFollowUpClickNotification >(node)
467
451
chatCommunicationManager.handleAuthFollowUpClicked(
468
- project,
469
452
message.params
470
453
)
471
454
}
@@ -564,18 +547,44 @@ class BrowserConnector(
564
547
}
565
548
}
566
549
567
- private fun showResult (
568
- result : CompletableFuture <String >,
569
- partialResultToken : String ,
550
+ private suspend fun doChatRequest (
570
551
tabId : String ,
571
- encryptionManager : JwtEncryptionManager ? ,
572
552
browser : Browser ,
553
+ action : (AmazonQServerInstanceFacade , String ) -> CompletableFuture <String >,
573
554
) {
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
+
574
582
result.whenComplete { value, error ->
575
583
try {
576
584
if (error != null ) {
577
585
throw error
578
586
}
587
+
579
588
chatCommunicationManager.removePartialChatMessage(partialResultToken)
580
589
val messageToChat = ChatCommunicationManager .convertToJsonToSendToChat(
581
590
SEND_CHAT_COMMAND_PROMPT ,
@@ -585,7 +594,7 @@ class BrowserConnector(
585
594
)
586
595
browser.postChat(messageToChat)
587
596
chatCommunicationManager.removeInflightRequestForTab(tabId)
588
- } catch (e : CancellationException ) {
597
+ } catch (_ : CancellationException ) {
589
598
LOG .warn { " Cancelled chat generation" }
590
599
try {
591
600
chatAsyncResultManager.createRequestId(partialResultToken)
0 commit comments