@@ -372,7 +372,11 @@ class IdeaAcpAgentViewModel(
372372 handleSessionUpdate(update)
373373 },
374374 onPermissionRequest = { toolCallUpdate, options ->
375- handlePermissionRequest(toolCallUpdate, options)
375+ // Use runBlocking to bridge suspend function to sync callback
376+ // This is called from IO dispatcher, so it won't block EDT
377+ kotlinx.coroutines.runBlocking {
378+ handlePermissionRequest(toolCallUpdate, options)
379+ }
376380 },
377381 cwd = cwd,
378382 enableFs = true ,
@@ -440,15 +444,20 @@ class IdeaAcpAgentViewModel(
440444
441445 flow.collect { event ->
442446 when (event) {
443- is Event .SessionUpdateEvent -> handleSessionUpdate(event.update)
447+ is Event .SessionUpdateEvent -> {
448+ acpLogger.info(" ACP SessionUpdate: ${event.update::class .simpleName} " )
449+ handleSessionUpdate(event.update)
450+ }
444451 is Event .PromptResponseEvent -> {
452+ acpLogger.info(" ACP PromptResponse: stopReason=${event.response.stopReason} , receivedChunks=${receivedAnyAgentChunk.get()} " )
445453 finishStreamingIfNeeded()
446454 val success = event.response.stopReason != StopReason .REFUSAL &&
447455 event.response.stopReason != StopReason .CANCELLED
448456
449457 // If no chunks were received, show helpful error
450458 if (! receivedAnyAgentChunk.get()) {
451459 val hint = " Check logs for details. Agent may have encountered an error."
460+ acpLogger.warn(" ACP ended without output. This may indicate: 1) Agent refused/failed, 2) Permission denied, 3) Agent process crashed" )
452461 renderer.renderError(" ACP ended without any message output; stopReason=${event.response.stopReason} . $hint " )
453462 }
454463
@@ -508,37 +517,57 @@ class IdeaAcpAgentViewModel(
508517 * the tool call.
509518 *
510519 * If the user cancels the dialog or if there's an error, returns a Cancelled outcome.
520+ *
521+ * IMPORTANT: This is called from IO thread but needs to show UI dialog on EDT.
522+ * We use CompletableFuture to avoid blocking the IO thread with invokeAndWait.
511523 */
512- private fun handlePermissionRequest (
524+ private suspend fun handlePermissionRequest (
513525 toolCall : SessionUpdate .ToolCallUpdate ,
514526 options : List <PermissionOption >,
515- ): RequestPermissionResponse {
527+ ): RequestPermissionResponse = withContext( Dispatchers . IO ) {
516528 try {
517- // Show permission dialog on EDT (UI thread) and wait for result
518- var selectedOption: PermissionOption ? = null
519- com.intellij.openapi.application.ApplicationManager .getApplication().invokeAndWait {
520- selectedOption = IdeaAcpPermissionDialog .show(project, toolCall, options)
529+ // Use CompletableFuture to avoid blocking IO thread
530+ val future = java.util.concurrent.CompletableFuture <PermissionOption ?>()
531+
532+ // Show dialog on EDT asynchronously
533+ com.intellij.openapi.application.ApplicationManager .getApplication().invokeLater {
534+ try {
535+ val selectedOption = IdeaAcpPermissionDialog .show(project, toolCall, options)
536+ future.complete(selectedOption)
537+ } catch (e: Exception ) {
538+ acpLogger.error { " Error showing permission dialog: ${e.message} " }
539+ e.printStackTrace()
540+ future.completeExceptionally(e)
541+ }
542+ }
543+
544+ // Wait for dialog result (non-blocking suspension)
545+ val selectedOption = try {
546+ future.get()
547+ } catch (e: Exception ) {
548+ acpLogger.error { " Failed to get permission dialog result: ${e.message} " }
549+ null
521550 }
522551
523552 // If user selected an option, return it
524553 if (selectedOption != null ) {
525554 acpLogger.info(
526- " ACP permission user selected: optionId=${selectedOption!! .optionId.value} kind=${selectedOption!! .kind} name=${selectedOption!! .name} "
555+ " ACP permission user selected: optionId=${selectedOption.optionId.value} kind=${selectedOption.kind} name=${selectedOption.name} "
527556 )
528- return RequestPermissionResponse (
529- RequestPermissionOutcome .Selected (selectedOption!! .optionId),
557+ return @withContext RequestPermissionResponse (
558+ RequestPermissionOutcome .Selected (selectedOption.optionId),
530559 JsonNull
531560 )
532561 }
533562
534563 // User cancelled the dialog
535564 acpLogger.info(" ACP permission cancelled by user (dialog closed)" )
536- return RequestPermissionResponse (RequestPermissionOutcome .Cancelled , JsonNull )
565+ return @withContext RequestPermissionResponse (RequestPermissionOutcome .Cancelled , JsonNull )
537566
538567 } catch (e: Exception ) {
539- acpLogger.error { " Error showing permission dialog : ${e.message} " }
568+ acpLogger.error { " Error in permission request handler : ${e.message} " }
540569 e.printStackTrace()
541- return RequestPermissionResponse (RequestPermissionOutcome .Cancelled , JsonNull )
570+ return @withContext RequestPermissionResponse (RequestPermissionOutcome .Cancelled , JsonNull )
542571 }
543572 }
544573
@@ -552,6 +581,25 @@ class IdeaAcpAgentViewModel(
552581 * For PlanUpdate, we additionally parse to the IDEA-specific plan model via [renderer.setPlan].
553582 */
554583 private fun handleSessionUpdate (update : SessionUpdate , source : String = "prompt") {
584+ // Log all session updates for debugging
585+ when (update) {
586+ is SessionUpdate .AgentMessageChunk -> {
587+ acpLogger.info(" ACP AgentMessageChunk received, content type: ${update.content::class .simpleName} " )
588+ }
589+ is SessionUpdate .AgentThoughtChunk -> {
590+ acpLogger.info(" ACP AgentThoughtChunk received" )
591+ }
592+ is SessionUpdate .ToolCall -> {
593+ acpLogger.info(" ACP ToolCall: ${update.title} , status=${update.status} " )
594+ }
595+ is SessionUpdate .ToolCallUpdate -> {
596+ acpLogger.info(" ACP ToolCallUpdate: ${update.title} , status=${update.status} " )
597+ }
598+ else -> {
599+ acpLogger.info(" ACP SessionUpdate: ${update::class .simpleName} " )
600+ }
601+ }
602+
555603 // Use the shared renderSessionUpdate from AcpClient for consistent rendering
556604 AcpClient .renderSessionUpdate(
557605 update = update,
0 commit comments