Skip to content

Commit 8a9eba1

Browse files
committed
fix: Fix permission request blocking and improve ACP logging
1 parent 612c8e7 commit 8a9eba1

File tree

2 files changed

+63
-15
lines changed

2 files changed

+63
-15
lines changed

mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/acp/IdeaAcpAgentViewModel.kt

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -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,

mpp-ui/src/jsMain/typescript/modes/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ export { ModeManager } from './ModeManager.js';
77
export type { ModeChangeEvent } from './ModeManager.js';
88
export { AgentMode, AgentModeFactory } from './AgentMode.js';
99
export { ChatMode, ChatModeFactory } from './ChatMode.js';
10-
export { CodexMode, CodexModeFactory } from './CodexMode.js';
10+
export { CodexMode, CodexModeFactory } from './CodexMode.js';

0 commit comments

Comments
 (0)