Skip to content

Commit b39b677

Browse files
committed
feat(acp): add config dialog and engine switching in IDEA plugin #536
- Add IdeaAcpConfigDialogWrapper for managing ACP agents with list, add/edit/delete, preset detection, and config.yaml persistence - Extend IdeaAgentViewModel with ACP engine support, agent loading, and switching between AutoDev LLM and ACP agents - Update SwingBottomToolbar to include ACP agents in model selector with separators and configuration option - Integrate ACP engine into IdeaAgentApp and IdeaDevInInputArea for unified message routing and UI state - Connect ACP ViewModel to shared renderer for consistent timeline output
1 parent badc8fb commit b39b677

File tree

6 files changed

+741
-13
lines changed

6 files changed

+741
-13
lines changed

mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/SwingBottomToolbar.kt

Lines changed: 112 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,20 @@ class SwingBottomToolbar(
5454
private var onConfigureClick: () -> Unit = {}
5555
private var onAddNewConfig: () -> Unit = {}
5656
private var onRefreshCopilot: () -> Unit = {}
57+
private var onAcpAgentSelect: (String) -> Unit = {} // ACP agent key
58+
private var onConfigureAcp: () -> Unit = {}
59+
private var onSwitchToAutodev: () -> Unit = {}
5760
private var isProcessing = false
5861
private var isEnhancing = false
5962
private var isRefreshingCopilot = false
6063
private var imageCount = 0
64+
65+
// Track ACP agent entries in the combo box
66+
// Format: list of (comboIndex, agentKey) for ACP items
67+
private var acpAgentEntries: List<Pair<Int, String>> = emptyList()
68+
private val ACP_SEPARATOR = "--- ACP Agents ---"
69+
private val ACP_CONFIGURE = "Configure ACP..."
70+
private var isComboUpdating = false
6171

6272
// Refresh GitHub Copilot button (only shown when Copilot is configured)
6373
private val refreshCopilotButton = JButton(AllIcons.Actions.Refresh).apply {
@@ -77,11 +87,31 @@ class SwingBottomToolbar(
7787
val leftPanel = JPanel(FlowLayout(FlowLayout.LEFT, 4, 0)).apply {
7888
isOpaque = false
7989

80-
modelComboBox.preferredSize = Dimension(150, 28)
90+
modelComboBox.preferredSize = Dimension(180, 28)
8191
modelComboBox.addActionListener {
92+
if (isComboUpdating) return@addActionListener
8293
val selectedIndex = modelComboBox.selectedIndex
83-
if (selectedIndex >= 0 && selectedIndex < availableConfigs.size) {
84-
onConfigSelect(availableConfigs[selectedIndex])
94+
val selectedItem = modelComboBox.selectedItem as? String
95+
96+
when {
97+
// "Configure ACP..." item
98+
selectedItem == ACP_CONFIGURE -> {
99+
onConfigureAcp()
100+
}
101+
// Separator (not selectable, reset)
102+
selectedItem == ACP_SEPARATOR -> {
103+
// Do nothing
104+
}
105+
// ACP agent entry
106+
acpAgentEntries.any { it.first == selectedIndex } -> {
107+
val agentKey = acpAgentEntries.first { it.first == selectedIndex }.second
108+
onAcpAgentSelect(agentKey)
109+
}
110+
// Regular LLM config
111+
selectedIndex in availableConfigs.indices -> {
112+
onSwitchToAutodev()
113+
onConfigSelect(availableConfigs[selectedIndex])
114+
}
85115
}
86116
}
87117
add(modelComboBox)
@@ -172,19 +202,81 @@ class SwingBottomToolbar(
172202

173203
fun setAvailableConfigs(configs: List<NamedModelConfig>) {
174204
availableConfigs = configs
175-
modelComboBox.removeAllItems()
176-
configs.forEach { modelComboBox.addItem(it.name) }
205+
rebuildComboBox()
206+
}
207+
208+
/**
209+
* Set available ACP agents and rebuild the combo box.
210+
*/
211+
fun setAcpAgents(agents: Map<String, cc.unitmesh.config.AcpAgentConfig>) {
212+
rebuildComboBoxWithAcp(agents)
213+
}
214+
215+
private fun rebuildComboBox() {
216+
rebuildComboBoxWithAcp(emptyMap())
217+
}
218+
219+
private fun rebuildComboBoxWithAcp(agents: Map<String, cc.unitmesh.config.AcpAgentConfig>) {
220+
isComboUpdating = true
221+
try {
222+
modelComboBox.removeAllItems()
223+
acpAgentEntries = emptyList()
224+
225+
// Add LLM configs
226+
availableConfigs.forEach { modelComboBox.addItem(it.name) }
227+
228+
// Add ACP agents section
229+
if (agents.isNotEmpty()) {
230+
val separatorIndex = modelComboBox.itemCount
231+
modelComboBox.addItem(ACP_SEPARATOR)
232+
233+
val entries = mutableListOf<Pair<Int, String>>()
234+
agents.forEach { (key, config) ->
235+
val displayName = config.name.ifBlank { key }
236+
val idx = modelComboBox.itemCount
237+
modelComboBox.addItem("ACP: $displayName")
238+
entries.add(idx to key)
239+
}
240+
acpAgentEntries = entries
241+
242+
// Add "Configure ACP..." at the end
243+
modelComboBox.addItem(ACP_CONFIGURE)
244+
}
245+
} finally {
246+
isComboUpdating = false
247+
}
177248
}
178249

179250
fun setCurrentConfigName(name: String?) {
180251
if (name != null) {
181-
val index = availableConfigs.indexOfFirst { it.name == name }
182-
if (index >= 0) {
183-
modelComboBox.selectedIndex = index
252+
isComboUpdating = true
253+
try {
254+
val index = availableConfigs.indexOfFirst { it.name == name }
255+
if (index >= 0) {
256+
modelComboBox.selectedIndex = index
257+
}
258+
} finally {
259+
isComboUpdating = false
184260
}
185261
}
186262
}
187263

264+
/**
265+
* Select an ACP agent in the combo box by key.
266+
*/
267+
fun setCurrentAcpAgent(agentKey: String?) {
268+
if (agentKey == null) return
269+
isComboUpdating = true
270+
try {
271+
val entry = acpAgentEntries.firstOrNull { it.second == agentKey }
272+
if (entry != null) {
273+
modelComboBox.selectedIndex = entry.first
274+
}
275+
} finally {
276+
isComboUpdating = false
277+
}
278+
}
279+
188280
fun setOnConfigSelect(callback: (NamedModelConfig) -> Unit) {
189281
onConfigSelect = callback
190282
}
@@ -250,5 +342,17 @@ class SwingBottomToolbar(
250342
fun setImageEnabled(enabled: Boolean) {
251343
imageButton.isEnabled = enabled
252344
}
345+
346+
fun setOnAcpAgentSelect(callback: (String) -> Unit) {
347+
onAcpAgentSelect = callback
348+
}
349+
350+
fun setOnConfigureAcp(callback: () -> Unit) {
351+
onConfigureAcp = callback
352+
}
353+
354+
fun setOnSwitchToAutodev(callback: () -> Unit) {
355+
onSwitchToAutodev = callback
356+
}
253357
}
254358

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

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ import cc.unitmesh.devins.idea.toolwindow.codereview.IdeaCodeReviewContent
1515
import cc.unitmesh.devins.idea.toolwindow.codereview.IdeaCodeReviewViewModel
1616
import cc.unitmesh.devins.idea.components.header.IdeaAgentTabsHeader
1717
import cc.unitmesh.devins.idea.components.IdeaVerticalResizableSplitPane
18+
import cc.unitmesh.config.AcpAgentConfig
1819
import cc.unitmesh.devins.idea.toolwindow.acp.IdeaAcpAgentContent
1920
import cc.unitmesh.devins.idea.toolwindow.acp.IdeaAcpAgentViewModel
21+
import cc.unitmesh.devins.idea.toolwindow.acp.IdeaAcpConfigDialogWrapper
2022
import cc.unitmesh.devins.idea.toolwindow.knowledge.IdeaKnowledgeContent
2123
import cc.unitmesh.devins.idea.toolwindow.knowledge.IdeaKnowledgeViewModel
2224
import cc.unitmesh.devins.idea.toolwindow.remote.IdeaRemoteAgentContent
@@ -85,6 +87,37 @@ fun IdeaAgentApp(
8587
// Check if GitHub Copilot is available
8688
val isCopilotAvailable = remember { GithubCopilotDetector.isGithubCopilotConfigured() }
8789

90+
// Engine state (AutoDev vs ACP)
91+
var currentEngine by remember { mutableStateOf(IdeaEngine.AUTODEV) }
92+
var acpAgents by remember { mutableStateOf<Map<String, AcpAgentConfig>>(emptyMap()) }
93+
var currentAcpAgentKey by remember { mutableStateOf<String?>(null) }
94+
var showAcpConfigDialog by remember { mutableStateOf(false) }
95+
var acpIsConnected by remember { mutableStateOf(false) }
96+
var acpIsExecuting by remember { mutableStateOf(false) }
97+
98+
// Collect engine state
99+
IdeaLaunchedEffect(viewModel, project = project) {
100+
viewModel.currentEngine.collect { currentEngine = it }
101+
}
102+
IdeaLaunchedEffect(viewModel, project = project) {
103+
viewModel.acpAgents.collect { acpAgents = it }
104+
}
105+
IdeaLaunchedEffect(viewModel, project = project) {
106+
viewModel.currentAcpAgentKey.collect { currentAcpAgentKey = it }
107+
}
108+
IdeaLaunchedEffect(viewModel, project = project) {
109+
viewModel.showAcpConfigDialog.collect { showAcpConfigDialog = it }
110+
}
111+
IdeaLaunchedEffect(viewModel.acpViewModel, project = project) {
112+
viewModel.acpViewModel.isConnected.collect { acpIsConnected = it }
113+
}
114+
IdeaLaunchedEffect(viewModel.acpViewModel, project = project) {
115+
viewModel.acpViewModel.isExecuting.collect { acpIsExecuting = it }
116+
}
117+
118+
// Effective isProcessing: check both engines
119+
val effectiveIsProcessing = if (currentEngine == IdeaEngine.ACP) acpIsExecuting else isExecuting
120+
88121
// Collect StateFlows manually using IdeaLaunchedEffect to avoid ClassLoader conflicts
89122
IdeaLaunchedEffect(viewModel, project = project) {
90123
viewModel.currentAgentType.collect { currentAgentType = it }
@@ -234,7 +267,7 @@ fun IdeaAgentApp(
234267
IdeaDevInInputArea(
235268
project = project,
236269
parentDisposable = viewModel,
237-
isProcessing = isExecuting,
270+
isProcessing = effectiveIsProcessing,
238271
onSend = { viewModel.sendMessage(it) },
239272
onAbort = { viewModel.cancelTask() },
240273
workspacePath = project.basePath,
@@ -305,6 +338,18 @@ fun IdeaAgentApp(
305338
},
306339
onMultimodalAnalysisComplete = { result, error ->
307340
viewModel.renderer.completeMultimodalAnalysis(result, error)
341+
},
342+
// ACP engine integration
343+
acpAgents = acpAgents,
344+
currentAcpAgentKey = if (currentEngine == IdeaEngine.ACP) currentAcpAgentKey else null,
345+
onAcpAgentSelect = { agentKey ->
346+
viewModel.switchToAcpAgent(agentKey)
347+
},
348+
onConfigureAcp = {
349+
viewModel.setShowAcpConfigDialog(true)
350+
},
351+
onSwitchToAutodev = {
352+
viewModel.switchToAutodev()
308353
}
309354
)
310355
}
@@ -494,5 +539,27 @@ fun IdeaAgentApp(
494539
}
495540
onDispose { }
496541
}
542+
543+
// ACP Agent Configuration Dialog
544+
DisposableEffect(showAcpConfigDialog) {
545+
if (showAcpConfigDialog) {
546+
com.intellij.openapi.application.ApplicationManager.getApplication().invokeLater {
547+
IdeaAcpConfigDialogWrapper.show(
548+
project = project,
549+
agents = acpAgents,
550+
activeKey = currentAcpAgentKey,
551+
onSave = { newAgents, newActiveKey ->
552+
// Reload ACP agents and update selection
553+
viewModel.reloadAcpAgents()
554+
if (newActiveKey != null) {
555+
viewModel.switchToAcpAgent(newActiveKey)
556+
}
557+
}
558+
)
559+
viewModel.setShowAcpConfigDialog(false)
560+
}
561+
}
562+
onDispose { }
563+
}
497564
}
498565

0 commit comments

Comments
 (0)