Skip to content

Commit 490147e

Browse files
committed
feat(ui): add External CLI Agent engine selector in Compose GUI
Add engine selector dropdown to DevInEditorInput toolbar, allowing users to switch between AutoDev (built-in), Claude CLI, and Codex CLI agents. Changes: - Add GuiAgentEngine enum with AUTODEV, CLAUDE_CLI, CODEX_CLI options - Implement switchEngine() in CodingAgentViewModel - Dynamic executeTask dispatch based on selected engine - External engines bypass LLM config requirement - Add EngineSelector composable in TopToolbar Relates to #536
1 parent c0290d0 commit 490147e

File tree

4 files changed

+139
-27
lines changed

4 files changed

+139
-27
lines changed

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/CodingAgentPage.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ fun CodingAgentPage(
282282
AgentType.LOCAL_CHAT,
283283
AgentType.CODING -> {
284284
runOutputDockContent()
285+
val engineOptions = remember { GuiAgentEngine.entries.map { it.displayName } }
285286
DevInEditorInput(
286287
initialText = "",
287288
placeholder = "Describe your coding task...",
@@ -291,6 +292,11 @@ fun CodingAgentPage(
291292
isExecuting = viewModel.isExecuting,
292293
onStopClick = { viewModel.cancelTask() },
293294
onModelConfigChange = { /* Handle model config change if needed */ },
295+
engine = viewModel.currentEngine.displayName,
296+
engineOptions = engineOptions,
297+
onEngineChange = { display ->
298+
GuiAgentEngine.fromDisplayName(display)?.let { viewModel.switchEngine(it) }
299+
},
294300
renderer = viewModel.renderer,
295301
modifier =
296302
Modifier

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/CodingAgentViewModel.kt

Lines changed: 72 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ import cc.unitmesh.agent.AgentTask
77
import cc.unitmesh.agent.AgentType
88
import cc.unitmesh.agent.CodeReviewAgent
99
import cc.unitmesh.agent.CodingAgent
10+
import cc.unitmesh.agent.external.ExternalCliCodingAgent
11+
import cc.unitmesh.agent.external.ExternalCliKind
12+
import cc.unitmesh.agent.external.ExternalCliMode
1013
import cc.unitmesh.agent.config.McpToolConfigManager
1114
import cc.unitmesh.agent.config.McpToolConfigService
1215
import cc.unitmesh.agent.config.PreloadingStatus
@@ -36,6 +39,14 @@ class CodingAgentViewModel(
3639
var currentAgentType by mutableStateOf(AgentType.CODING)
3740
private set
3841

42+
/**
43+
* GUI execution engine selection.
44+
* - AUTODEV: our built-in CodingAgent (requires LLM config)
45+
* - CLAUDE_CLI / CODEX_CLI: external CLIs (do not require LLM config inside Xiuper)
46+
*/
47+
var currentEngine by mutableStateOf(GuiAgentEngine.AUTODEV)
48+
private set
49+
3950
private var _codingAgent: CodingAgent? = null
4051
private var _codeReviewAgent: CodeReviewAgent? = null
4152
private var agentInitialized = false
@@ -150,14 +161,23 @@ class CodingAgentViewModel(
150161
}
151162
}
152163

164+
fun switchEngine(engine: GuiAgentEngine) {
165+
if (currentEngine != engine) {
166+
currentEngine = engine
167+
}
168+
}
169+
153170
fun isConfigured(): Boolean = llmService != null
154171

155172
fun executeTask(task: String, onConfigRequired: (() -> Unit)? = null) {
156173
if (isExecuting) {
157174
return
158175
}
159176

160-
if (!isConfigured()) {
177+
// Engine-dependent config check:
178+
// - AUTODEV engine requires local LLM config
179+
// - External engines do not require Xiuper LLM config
180+
if (currentEngine == GuiAgentEngine.AUTODEV && !isConfigured()) {
161181
renderer.addUserMessage(task)
162182
renderer.renderError("WARNING: LLM model is not configured. Please configure your model to continue.")
163183
onConfigRequired?.invoke()
@@ -173,36 +193,50 @@ class CodingAgentViewModel(
173193
renderer.clearError()
174194
renderer.addUserMessage(task)
175195

176-
currentExecutionJob =
177-
scope.launch {
178-
val codingAgent = initializeCodingAgent()
179-
try {
180-
val agentTask =
181-
AgentTask(
182-
requirement = task,
183-
projectPath = projectPath,
184-
language = LanguageManager.getLanguage().code.uppercase()
185-
)
196+
currentExecutionJob = scope.launch {
197+
try {
198+
val agentTask = AgentTask(
199+
requirement = task,
200+
projectPath = projectPath,
201+
language = LanguageManager.getLanguage().code.uppercase()
202+
)
186203

187-
val result = codingAgent.executeTask(agentTask)
188-
isExecuting = false
189-
currentExecutionJob = null
190-
} catch (e: CancellationException) {
191-
renderer.forceStop() // Stop all loading states
192-
renderer.renderError("Task cancelled by user")
193-
isExecuting = false
194-
currentExecutionJob = null
195-
} catch (e: Exception) {
196-
renderer.renderError(e.message ?: "Unknown error")
197-
isExecuting = false
198-
currentExecutionJob = null
199-
} finally {
200-
saveConversationHistory(codingAgent)
204+
when (currentEngine) {
205+
GuiAgentEngine.AUTODEV -> {
206+
val codingAgent = initializeCodingAgent()
207+
codingAgent.executeTask(agentTask)
208+
}
209+
GuiAgentEngine.CLAUDE_CLI -> {
210+
ExternalCliCodingAgent(
211+
projectPath = projectPath,
212+
kind = ExternalCliKind.CLAUDE,
213+
renderer = renderer,
214+
mode = ExternalCliMode.NON_INTERACTIVE
215+
).executeTask(agentTask)
216+
}
217+
GuiAgentEngine.CODEX_CLI -> {
218+
ExternalCliCodingAgent(
219+
projectPath = projectPath,
220+
kind = ExternalCliKind.CODEX,
221+
renderer = renderer,
222+
mode = ExternalCliMode.NON_INTERACTIVE
223+
).executeTask(agentTask)
224+
}
201225
}
226+
} catch (e: CancellationException) {
227+
renderer.forceStop() // Stop all loading states
228+
renderer.renderError("Task cancelled by user")
229+
} catch (e: Exception) {
230+
renderer.renderError(e.message ?: "Unknown error")
231+
} finally {
232+
isExecuting = false
233+
currentExecutionJob = null
234+
saveConversationHistory()
202235
}
236+
}
203237
}
204238

205-
private suspend fun saveConversationHistory(codingAgent: CodingAgent) {
239+
private suspend fun saveConversationHistory() {
206240
chatHistoryManager?.let { manager ->
207241
try {
208242
// Get timeline snapshot with metadata from renderer
@@ -476,6 +510,18 @@ class CodingAgentViewModel(
476510
}
477511
}
478512

513+
enum class GuiAgentEngine(val displayName: String) {
514+
AUTODEV("AutoDev"),
515+
CLAUDE_CLI("Claude"),
516+
CODEX_CLI("Codex");
517+
518+
companion object {
519+
fun fromDisplayName(name: String): GuiAgentEngine? {
520+
return entries.firstOrNull { it.displayName.equals(name, ignoreCase = true) }
521+
}
522+
}
523+
}
524+
479525
/**
480526
* Data class to hold tool loading status information
481527
*/

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/DevInEditorInput.kt

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ import androidx.compose.foundation.clickable
44
import androidx.compose.foundation.layout.*
55
import androidx.compose.foundation.shape.RoundedCornerShape
66
import androidx.compose.foundation.text.BasicTextField
7+
import androidx.compose.material3.DropdownMenu
8+
import androidx.compose.material3.DropdownMenuItem
79
import androidx.compose.material3.MaterialTheme
810
import androidx.compose.material3.Surface
911
import androidx.compose.material3.Text
12+
import androidx.compose.material3.TextButton
1013
import androidx.compose.runtime.*
1114
import androidx.compose.ui.Alignment
1215
import androidx.compose.ui.Modifier
@@ -83,6 +86,13 @@ fun DevInEditorInput(
8386
onStopClick: () -> Unit = {},
8487
modifier: Modifier = Modifier,
8588
onModelConfigChange: (ModelConfig) -> Unit = {},
89+
/**
90+
* Optional agent engine selector (desktop only).
91+
* When provided, the editor shows a dropdown in the top toolbar.
92+
*/
93+
engine: String = "",
94+
engineOptions: List<String> = emptyList(),
95+
onEngineChange: (String) -> Unit = {},
8696
dismissKeyboardOnSend: Boolean = true,
8797
renderer: cc.unitmesh.devins.ui.compose.agent.ComposeRenderer? = null,
8898
fileSearchProvider: FileSearchProvider? = null,
@@ -645,7 +655,16 @@ fun DevInEditorInput(
645655
onClearFiles = { selectedFiles = emptyList() },
646656
autoAddCurrentFile = autoAddCurrentFile,
647657
onToggleAutoAdd = { autoAddCurrentFile = !autoAddCurrentFile },
648-
searchProvider = effectiveSearchProvider
658+
searchProvider = effectiveSearchProvider,
659+
trailingContent = {
660+
if (engineOptions.isNotEmpty()) {
661+
EngineSelector(
662+
engine = engine,
663+
engineOptions = engineOptions,
664+
onEngineChange = onEngineChange
665+
)
666+
}
667+
}
649668
)
650669
}
651670

@@ -900,3 +919,40 @@ fun DevInEditorInput(
900919
// No auto-focus on any platform - user must tap to show keyboard
901920
// This provides consistent behavior across mobile and desktop
902921
}
922+
923+
@Composable
924+
private fun RowScope.EngineSelector(
925+
engine: String,
926+
engineOptions: List<String>,
927+
onEngineChange: (String) -> Unit
928+
) {
929+
var expanded by remember { mutableStateOf(false) }
930+
931+
Box {
932+
TextButton(
933+
onClick = { expanded = true },
934+
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
935+
modifier = Modifier.height(28.dp)
936+
) {
937+
Text(
938+
text = if (engine.isNotBlank()) "Engine: $engine" else "Engine",
939+
style = MaterialTheme.typography.labelSmall
940+
)
941+
}
942+
943+
DropdownMenu(
944+
expanded = expanded,
945+
onDismissRequest = { expanded = false }
946+
) {
947+
engineOptions.forEach { option ->
948+
DropdownMenuItem(
949+
text = { Text(option) },
950+
onClick = {
951+
onEngineChange(option)
952+
expanded = false
953+
}
954+
)
955+
}
956+
}
957+
}
958+
}

mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/TopToolbar.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ fun TopToolbar(
3030
autoAddCurrentFile: Boolean = true,
3131
onToggleAutoAdd: () -> Unit = {},
3232
searchProvider: FileSearchProvider = DefaultFileSearchProvider,
33+
trailingContent: @Composable RowScope.() -> Unit = {},
3334
modifier: Modifier = Modifier
3435
) {
3536
var isExpanded by remember { mutableStateOf(false) }
@@ -130,6 +131,9 @@ fun TopToolbar(
130131
onClick = onToggleAutoAdd
131132
)
132133

134+
// Optional trailing content (engine selectors, etc.)
135+
trailingContent()
136+
133137
// Expand/Collapse button (only when multiple files)
134138
if (selectedFiles.size > 1) {
135139
IconButton(

0 commit comments

Comments
 (0)