package cc.unitmesh.devins.idea.toolwindow import cc.unitmesh.agent.AgentTask import cc.unitmesh.agent.AgentType import cc.unitmesh.agent.CodingAgent import cc.unitmesh.agent.config.McpToolConfigManager import cc.unitmesh.agent.config.McpToolConfigService import cc.unitmesh.agent.config.PreloadingStatus import cc.unitmesh.agent.config.ToolConfigFile import cc.unitmesh.agent.tool.ToolType import cc.unitmesh.agent.tool.schema.ToolCategory import cc.unitmesh.config.AcpAgentConfig import cc.unitmesh.config.AutoDevConfigWrapper import cc.unitmesh.config.ConfigManager import cc.unitmesh.devins.compiler.service.DevInsCompilerService import cc.unitmesh.devins.idea.compiler.IdeaDevInsCompilerService import cc.unitmesh.devins.idea.renderer.JewelRenderer import cc.unitmesh.devins.idea.services.IdeaToolConfigService import cc.unitmesh.devins.idea.tool.IdeaToolProvider import cc.unitmesh.devins.idea.toolwindow.acp.IdeaAcpAgentPreset import cc.unitmesh.devins.idea.toolwindow.acp.IdeaAcpAgentViewModel import cc.unitmesh.llm.LLMService import cc.unitmesh.llm.ModelConfig import cc.unitmesh.llm.NamedModelConfig import com.intellij.openapi.Disposable import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow /** * ViewModel for the Agent ToolWindow. * Manages agent type tabs, chat messages, and LLM integration. * * Uses mpp-ui's ConfigManager for configuration management to ensure * cross-platform consistency with CLI and Desktop apps. * * Integrates with mpp-core's CodingAgent for actual agent execution. * * Aligned with CodingAgentViewModel from mpp-ui for feature parity. */ class IdeaAgentViewModel( private val project: Project, private val coroutineScope: CoroutineScope, private val maxIterations: Int = 100 ) : Disposable { private val vmLogger = Logger.getInstance("IdeaAgentViewModel") // Renderer for agent output (uses StateFlow instead of Compose mutableStateOf) val renderer = JewelRenderer() // Current agent type tab (using mpp-core's AgentType) // Initialize with default value, will be updated asynchronously from config private val _currentAgentType = MutableStateFlow(AgentType.CODING) val currentAgentType: StateFlow = _currentAgentType.asStateFlow() // Is executing a task private val _isExecuting = MutableStateFlow(false) val isExecuting: StateFlow = _isExecuting.asStateFlow() // Is processing (alias for isExecuting for backward compatibility) val isProcessing: StateFlow get() = _isExecuting // LLM Configuration from mpp-ui's ConfigManager private val _configWrapper = MutableStateFlow(null) val configWrapper: StateFlow = _configWrapper.asStateFlow() // Current model config (derived from configWrapper) private val _currentModelConfig = MutableStateFlow(null) val currentModelConfig: StateFlow = _currentModelConfig.asStateFlow() // LLM Service (created from config) private var llmService: LLMService? = null // IDEA DevIns Compiler Service (uses PSI-based compiler with full IDE features) private val ideaCompilerService: DevInsCompilerService by lazy { IdeaDevInsCompilerService.create(project) } // CodingAgent instance private var codingAgent: CodingAgent? = null private var agentInitialized = false // Show config dialog private val _showConfigDialog = MutableStateFlow(false) val showConfigDialog: StateFlow = _showConfigDialog.asStateFlow() private val _isNewConfig = MutableStateFlow(false) val isNewConfig: StateFlow = _isNewConfig.asStateFlow() // Current execution job (for cancellation) private var currentJob: Job? = null // MCP Preloading status private val _mcpPreloadingStatus = MutableStateFlow(PreloadingStatus(false, emptyList(), 0)) val mcpPreloadingStatus: StateFlow = _mcpPreloadingStatus.asStateFlow() private val _mcpPreloadingMessage = MutableStateFlow("") val mcpPreloadingMessage: StateFlow = _mcpPreloadingMessage.asStateFlow() // Cached tool configuration private var cachedToolConfig: ToolConfigFile? = null // ---- Engine Selection (AutoDev built-in LLM vs ACP agents) ---- /** * Current engine type. When ACP, messages are routed to the ACP agent. */ private val _currentEngine = MutableStateFlow(IdeaEngine.AUTODEV) val currentEngine: StateFlow = _currentEngine.asStateFlow() /** * Currently active ACP agent key (e.g., "codex", "kimi"). */ private val _currentAcpAgentKey = MutableStateFlow(null) val currentAcpAgentKey: StateFlow = _currentAcpAgentKey.asStateFlow() /** * Available ACP agents from config.yaml. */ private val _acpAgents = MutableStateFlow>(emptyMap()) val acpAgents: StateFlow> = _acpAgents.asStateFlow() /** * Show ACP config dialog. */ private val _showAcpConfigDialog = MutableStateFlow(false) val showAcpConfigDialog: StateFlow = _showAcpConfigDialog.asStateFlow() /** * ACP ViewModel that shares this ViewModel's renderer for unified timeline. */ val acpViewModel: IdeaAcpAgentViewModel by lazy { IdeaAcpAgentViewModel( project = project, coroutineScope = coroutineScope, externalRenderer = renderer ) } /** * Display name for the current engine (shown in model selector). */ val currentEngineDisplay: String get() { return when (_currentEngine.value) { IdeaEngine.ACP -> { val key = _currentAcpAgentKey.value val config = if (key != null) _acpAgents.value[key] else null config?.name?.ifBlank { key ?: "ACP" } ?: "ACP" } IdeaEngine.AUTODEV -> { _configWrapper.value?.getActiveName() ?: "AutoDev" } } } init { vmLogger.warn("=== IdeaAgentViewModel init START === Project: ${project.name}") // Load configuration on initialization loadConfiguration() // Load ACP agents loadAcpAgents() vmLogger.warn("=== IdeaAgentViewModel init END === (async operations launched)") } /** * Load configuration from ConfigManager (~/.autodev/config.yaml) * Uses Dispatchers.IO to avoid blocking EDT */ private fun loadConfiguration() { vmLogger.warn("loadConfiguration() called - launching coroutine on Dispatchers.IO") coroutineScope.launch(Dispatchers.IO) { vmLogger.warn("loadConfiguration() coroutine started") try { vmLogger.warn("ConfigManager.load() starting...") val wrapper = ConfigManager.load() vmLogger.warn("ConfigManager.load() complete - got wrapper") _configWrapper.value = wrapper val modelConfig = wrapper.getActiveModelConfig() vmLogger.warn("Active model config: ${modelConfig?.modelName ?: "null"}") _currentModelConfig.value = modelConfig // Update agent type from config (async to avoid blocking EDT) val agentType = wrapper.getAgentType() vmLogger.warn("Agent type from config: $agentType") _currentAgentType.value = agentType // Create LLM service if config is valid // Inject IDEA compiler service for full IDE feature support if (modelConfig != null && modelConfig.isValid()) { vmLogger.warn("Creating LLM service...") llmService = LLMService( config = modelConfig, compilerService = ideaCompilerService ) vmLogger.warn("LLM service created successfully") // Start MCP preloading after LLM service is created (fully async, non-blocking) startMcpPreloading() } vmLogger.warn("loadConfiguration() completed successfully") } catch (e: Exception) { vmLogger.error("loadConfiguration() failed with exception", e) // Config file doesn't exist or is invalid, use defaults _configWrapper.value = null _currentModelConfig.value = null llmService = null } } } /** * Start MCP servers preloading in background. * * This method is fully asynchronous and non-blocking: * 1. Launches a background coroutine on Dispatchers.IO * 2. Initializes MCP servers via McpToolConfigManager.init() * 3. Monitors preloading status by waiting for the preloading job to complete * 4. Updates UI state via StateFlow (no EDT blocking) * * The key difference from the old implementation: * - OLD: Used a while loop with delay() that could block for up to 60 seconds * - NEW: Uses waitForPreloading() which suspends without blocking EDT */ private fun startMcpPreloading() { vmLogger.warn("startMcpPreloading() called - launching background coroutine") // Launch on IO dispatcher to avoid blocking EDT coroutineScope.launch(Dispatchers.IO) { try { vmLogger.warn("MCP preloading coroutine started on ${Thread.currentThread().name}") _mcpPreloadingMessage.value = "Loading MCP servers configuration..." // Use IdeaToolConfigService to get and cache tool config val toolConfigService = IdeaToolConfigService.getInstance(project) toolConfigService.reloadConfig() val toolConfig = toolConfigService.getToolConfig() cachedToolConfig = toolConfig vmLogger.warn("Tool config loaded - ${toolConfig.mcpServers.size} MCP servers configured") if (toolConfig.mcpServers.isEmpty()) { vmLogger.warn("No MCP servers configured - skipping preloading") _mcpPreloadingMessage.value = "No MCP servers configured" return@launch } val enabledCount = toolConfig.mcpServers.filter { !it.value.disabled }.size vmLogger.warn("Initializing $enabledCount enabled MCP servers...") _mcpPreloadingMessage.value = "Initializing $enabledCount MCP servers..." // Initialize MCP servers (this starts background preloading in McpToolConfigManager) McpToolConfigManager.init(toolConfig) vmLogger.warn("McpToolConfigManager.init() returned - preloading started in background") // Launch a separate coroutine to monitor preloading status // This doesn't block - it just updates the UI state periodically val monitorJob = launch { while (McpToolConfigManager.isPreloading()) { _mcpPreloadingStatus.value = McpToolConfigManager.getPreloadingStatus() val preloadedCount = _mcpPreloadingStatus.value.preloadedServers.size _mcpPreloadingMessage.value = "Loading MCP servers... ($preloadedCount/$enabledCount completed)" delay(500) // Update UI every 500ms } } // Wait for preloading to complete (non-blocking suspend) // This uses Job.join() internally, which is a proper suspend function vmLogger.warn("Waiting for MCP preloading to complete...") McpToolConfigManager.waitForPreloading() vmLogger.warn("MCP preloading job completed") // Cancel the monitor job since preloading is done monitorJob.cancel() // Final status update _mcpPreloadingStatus.value = McpToolConfigManager.getPreloadingStatus() val preloadedCount = _mcpPreloadingStatus.value.preloadedServers.size vmLogger.warn("MCP preloading complete - $preloadedCount/$enabledCount servers loaded") _mcpPreloadingMessage.value = if (preloadedCount > 0) { "MCP servers loaded successfully ($preloadedCount/$enabledCount servers)" } else { "MCP servers initialization completed (no tools loaded)" } } catch (e: CancellationException) { vmLogger.warn("MCP preloading cancelled", e) // Cancellation is expected when configuration is reloaded, don't log as error throw e } catch (e: Exception) { vmLogger.error("MCP preloading failed with exception", e) _mcpPreloadingMessage.value = "Failed to load MCP servers: ${e.message}" } } } /** * Load ACP agents from config.yaml. */ private fun loadAcpAgents() { vmLogger.warn("loadAcpAgents() called - launching coroutine on Dispatchers.IO") coroutineScope.launch(Dispatchers.IO) { vmLogger.warn("loadAcpAgents() coroutine started") try { vmLogger.warn("ConfigManager.load() for ACP agents starting...") val wrapper = ConfigManager.load() vmLogger.warn("ConfigManager.load() for ACP agents complete") val agents = wrapper.getAcpAgents() vmLogger.warn("Loaded ${agents.size} ACP agents from config") agents.forEach { (key, config) -> vmLogger.warn(" - ACP Agent: $key -> ${config.name} (${config.command})") } _acpAgents.value = agents val activeKey = wrapper.getActiveAcpAgentKey() _currentAcpAgentKey.value = activeKey vmLogger.warn("Active ACP agent key: $activeKey") vmLogger.warn("loadAcpAgents() completed successfully") } catch (e: Exception) { vmLogger.error("Failed to load ACP agents from config", e) // Set empty map so UI still renders "Configure ACP..." _acpAgents.value = emptyMap() } } } /** * Reload ACP agents from config (e.g., after dialog changes). */ fun reloadAcpAgents() { loadAcpAgents() } /** * Switch to an ACP agent engine. */ fun switchToAcpAgent(agentKey: String) { val config = _acpAgents.value[agentKey] ?: return _currentEngine.value = IdeaEngine.ACP _currentAcpAgentKey.value = agentKey // Save preference (only save once here) coroutineScope.launch(Dispatchers.IO) { try { AutoDevConfigWrapper.saveActiveAcpAgent(agentKey) } catch (e: Exception) { vmLogger.warn("Failed to save active ACP agent", e) } } // Update ACP ViewModel selection without saving again acpViewModel.selectAgent(agentKey, saveConfig = false) if (!acpViewModel.isConnected.value) { acpViewModel.connectSelectedAgent() } } /** * Switch back to AutoDev (built-in LLM) engine. */ fun switchToAutodev() { _currentEngine.value = IdeaEngine.AUTODEV } /** * Check if current engine is ACP. */ fun isAcpEngine(): Boolean = _currentEngine.value == IdeaEngine.ACP /** * Show/hide ACP config dialog. */ fun setShowAcpConfigDialog(show: Boolean) { _showAcpConfigDialog.value = show } /** * Reload configuration from file */ fun reloadConfiguration() { agentInitialized = false codingAgent = null cachedToolConfig = null loadConfiguration() } /** * Reload only the config wrapper (for UI refresh after adding new configs) */ fun reloadConfigs() { coroutineScope.launch(Dispatchers.IO) { try { val wrapper = ConfigManager.load() _configWrapper.value = wrapper _currentModelConfig.value = wrapper.getActiveModelConfig() } catch (e: Exception) { // Ignore reload errors } } } /** * Change the current agent type tab and persist to config. */ fun onAgentTypeChange(agentType: AgentType) { _currentAgentType.value = agentType // Save to config file for persistence coroutineScope.launch { try { val typeString = agentType.getDisplayName() AutoDevConfigWrapper.saveAgentTypePreference(typeString) } catch (e: Exception) { // Silently fail - not critical if we can't save preference println("⚠️ Failed to save agent type preference: ${e.message}") } } } /** * Initialize the CodingAgent with tool configuration. * Aligned with CodingAgentViewModel's initializeCodingAgent(). */ private suspend fun initializeCodingAgent(): CodingAgent { val service = llmService ?: throw IllegalStateException("LLM service is not configured. Please configure in ~/.autodev/config.yaml") if (codingAgent == null || !agentInitialized) { val toolConfig = cachedToolConfig ?: try { ConfigManager.loadToolConfig().also { cachedToolConfig = it } } catch (e: Exception) { ToolConfigFile.default() } val mcpToolConfigService = McpToolConfigService(toolConfig) val projectPath = project.basePath ?: System.getProperty("user.home") codingAgent = CodingAgent( projectPath = projectPath, llmService = service, maxIterations = maxIterations, renderer = renderer, mcpToolConfigService = mcpToolConfigService, enableLLMStreaming = true ) // Register IDEA-specific tools from ToolchainFunctionProvider extensions registerIdeaTools(codingAgent!!) agentInitialized = true // Start observing PlanStateService and sync to renderer startPlanStateObserver(codingAgent!!) } return codingAgent!! } /** * Register IDEA-specific tools from ToolchainFunctionProvider extensions. * This bridges IDEA's extension point system with mpp-core's tool registry. * This is a suspend function to avoid blocking the calling thread. */ private suspend fun registerIdeaTools(agent: CodingAgent) { try { val ideaToolProvider = IdeaToolProvider.create(project) val ideaTools = ideaToolProvider.provideTools() for (tool in ideaTools) { agent.registerTool(tool) } if (ideaTools.isNotEmpty()) { println("Registered ${ideaTools.size} IDEA tools from ToolchainFunctionProvider extensions") } } catch (e: Exception) { println("Warning: Failed to register IDEA tools: ${e.message}") } } // Job for observing PlanStateService private var planStateObserverJob: Job? = null /** * Start observing PlanStateService and sync plan state to renderer. */ private fun startPlanStateObserver(agent: CodingAgent) { // Cancel any existing observer planStateObserverJob?.cancel() val planStateService = agent.getPlanStateService() ?: return planStateObserverJob = coroutineScope.launch { planStateService.currentPlan.collect { plan -> renderer.setPlan(plan) } } } /** * Check if LLM service is configured. */ fun isConfigured(): Boolean = llmService != null /** * Execute a task with the agent. * Aligned with CodingAgentViewModel's executeTask(). * * @param task The task description * @param onConfigRequired Callback when configuration is needed */ fun executeTask(task: String, onConfigRequired: (() -> Unit)? = null) { if (_isExecuting.value) return // Handle builtin commands first (before config check) so they work without LLM configuration if (task.trim().startsWith("/")) { handleBuiltinCommand(task.trim(), onConfigRequired) return } if (!isConfigured()) { renderer.addUserMessage(task) renderer.renderError("WARNING: LLM model is not configured. Please configure your model to continue.") onConfigRequired?.invoke() return } _isExecuting.value = true renderer.clearError() renderer.addUserMessage(task) // Use Dispatchers.IO to avoid blocking EDT during LLM streaming currentJob = coroutineScope.launch(Dispatchers.IO) { try { val agent = initializeCodingAgent() val projectPath = project.basePath ?: System.getProperty("user.home") val language = _configWrapper.value?.getLanguage()?.uppercase() ?: "EN" val agentTask = AgentTask( requirement = task, projectPath = projectPath, language = language ) agent.executeTask(agentTask) } catch (e: CancellationException) { renderer.forceStop() renderer.renderError("Task cancelled by user") } catch (e: Exception) { renderer.renderError(e.message ?: "Unknown error") } finally { _isExecuting.value = false currentJob = null } } } /** * Handle builtin commands (/init, /clear, /help, etc.). * Aligned with CodingAgentViewModel's handleBuiltinCommand(). */ private fun handleBuiltinCommand(command: String, onConfigRequired: (() -> Unit)? = null) { val parts = command.substring(1).trim().split("\\s+".toRegex()) val commandName = parts[0].lowercase() val args = parts.drop(1).joinToString(" ") renderer.addUserMessage(command) when (commandName) { "clear" -> { renderer.clearTimeline() renderer.renderFinalResult(true, "SUCCESS: Chat history cleared", 0) } "help" -> { val helpText = buildString { appendLine("HELP: Available Commands:") appendLine(" /clear - Clear chat history") appendLine(" /help - Show this help message") appendLine(" /status - Show MCP servers and tools status") appendLine("") appendLine("TIP: You can also use @ for agents and other DevIns commands") } renderer.renderFinalResult(true, helpText, 0) } "status" -> { val status = getToolLoadingStatus() val statusText = buildString { appendLine("STATUS: Tool Loading Status") appendLine(" SubAgents: ${status.subAgentsEnabled}/${status.subAgentsTotal}") appendLine(" MCP Servers: ${status.mcpServersLoaded}/${status.mcpServersTotal}") appendLine(" MCP Tools: ${status.mcpToolsEnabled}/${status.mcpToolsTotal}") appendLine(" Loading: ${status.isLoading}") } renderer.renderFinalResult(true, statusText, 0) } else -> { if (!isConfigured()) { renderer.renderError("WARNING: LLM model is not configured. Please configure your model to continue.") onConfigRequired?.invoke() return } // Treat as a regular task // Use Dispatchers.IO to avoid blocking EDT during LLM streaming _isExecuting.value = true currentJob = coroutineScope.launch(Dispatchers.IO) { try { val agent = initializeCodingAgent() val projectPath = project.basePath ?: System.getProperty("user.home") val language = _configWrapper.value?.getLanguage()?.uppercase() ?: "EN" val agentTask = AgentTask( requirement = command, projectPath = projectPath, language = language ) agent.executeTask(agentTask) } catch (e: Exception) { renderer.renderError(e.message ?: "Unknown error") } finally { _isExecuting.value = false currentJob = null } } } } } /** * Send a message to the Agent. * Routes to ACP agent when engine is ACP, otherwise uses built-in CodingAgent. */ fun sendMessage(content: String) { if (_currentEngine.value == IdeaEngine.ACP) { sendAcpMessage(content) } else { executeTask(content) { setShowConfigDialog(true) } } } /** * Send message via ACP agent. * Auto-connects if not already connected. */ private fun sendAcpMessage(content: String) { val acpVm = acpViewModel if (!acpVm.isConnected.value) { // Auto-connect on first message val key = _currentAcpAgentKey.value if (key != null) { acpVm.selectAgent(key) acpVm.connectSelectedAgent() // Wait for connection, then send coroutineScope.launch { var attempts = 0 while (!acpVm.isConnected.value && attempts < 100) { delay(100) attempts++ } if (acpVm.isConnected.value) { acpVm.sendMessage(content) } else { renderer.addUserMessage(content) renderer.renderError("Failed to connect to ACP agent. Please check the configuration.") } } } else { renderer.addUserMessage(content) renderer.renderError("No ACP agent selected. Please select an agent from the model dropdown.") } return } acpVm.sendMessage(content) } /** * Cancel the current task. */ fun cancelTask() { if (_currentEngine.value == IdeaEngine.ACP) { acpViewModel.cancelTask() return } if (_isExecuting.value && currentJob != null) { currentJob?.cancel("Task cancelled by user") currentJob = null renderer.forceStop() _isExecuting.value = false } } /** * Handle process cancel event from LiveTerminal. * Terminates the process and sends the current output log to AI. */ fun handleProcessCancel(cancelEvent: cc.unitmesh.devins.idea.components.timeline.CancelEvent) { // Mark the session as cancelled by user BEFORE terminating the process // This allows ToolOrchestrator.startSessionMonitoring() to detect user cancellation cc.unitmesh.agent.tool.shell.ShellSessionManager.markSessionCancelledByUser(cancelEvent.sessionId) // Terminate the process cancelEvent.process.destroyForcibly() // Update the timeline with cancellation info val cancelMessage = buildString { appendLine("Process cancelled by user.") appendLine() appendLine("Command: ${cancelEvent.command}") appendLine("Session ID: ${cancelEvent.sessionId}") appendLine() appendLine("Output before cancellation:") appendLine("```") append(cancelEvent.output.ifEmpty { "(no output)" }) appendLine() appendLine("```") } // Render the cancellation result as a tool result with metadata renderer.renderToolResult( toolName = "shell", success = false, output = cancelMessage, fullOutput = cancelEvent.output, metadata = mapOf( "isLiveSession" to "true", "sessionId" to cancelEvent.sessionId, "exit_code" to "-1", "cancelled" to "true" ) ) } /** * Abort the current request (alias for cancelTask for backward compatibility). */ fun abortRequest() { cancelTask() } /** * Clear chat history. Also disconnects ACP agent to start fresh. */ fun clearHistory() { renderer.clearTimeline() if (_currentEngine.value == IdeaEngine.ACP) { acpViewModel.disconnect() } } /** * Get tool loading status. * Aligned with CodingAgentViewModel's getToolLoadingStatus(). * Uses IdeaToolConfigService for up-to-date configuration. */ fun getToolLoadingStatus(): ToolLoadingStatus { // Get fresh config from service to ensure we have latest changes val toolConfigService = IdeaToolConfigService.getInstance(project) val toolConfig = toolConfigService.getToolConfig() // Update cached config cachedToolConfig = toolConfig val subAgentTools = ToolType.byCategory(ToolCategory.SubAgent) val subAgentsEnabled = subAgentTools.size val mcpServersTotal = toolConfig.mcpServers.filter { !it.value.disabled }.size val mcpServersLoaded = _mcpPreloadingStatus.value.preloadedServers.size val mcpToolsEnabled = if (McpToolConfigManager.isPreloading()) { 0 } else { val enabledMcpToolsCount = toolConfig.enabledMcpTools.size if (enabledMcpToolsCount > 0) enabledMcpToolsCount else 0 } val mcpToolsTotal = if (McpToolConfigManager.isPreloading()) { 0 } else { McpToolConfigManager.getTotalDiscoveredTools() } return ToolLoadingStatus( subAgentsEnabled = subAgentsEnabled, subAgentsTotal = subAgentTools.size, mcpServersLoaded = mcpServersLoaded, mcpServersTotal = mcpServersTotal, mcpToolsEnabled = mcpToolsEnabled, mcpToolsTotal = mcpToolsTotal, isLoading = McpToolConfigManager.isPreloading() ) } /** * Save a new LLM configuration using ConfigManager */ fun saveModelConfig(config: NamedModelConfig, setActive: Boolean = true) { coroutineScope.launch { try { ConfigManager.saveConfig(config, setActive) // Reload configuration after saving reloadConfiguration() } catch (e: Exception) { // Handle save error renderer.renderError("Failed to save configuration: ${e.message}") } } } /** * Show/hide config dialog. */ fun setShowConfigDialog(show: Boolean) { _showConfigDialog.value = show } /** * Show config dialog for adding a new config. */ fun showAddNewConfigDialog() { _isNewConfig.value = true _showConfigDialog.value = true } /** * Show config dialog for editing current config. */ fun showEditConfigDialog() { _isNewConfig.value = false _showConfigDialog.value = true } /** * Set the active configuration by name */ fun setActiveConfig(configName: String) { coroutineScope.launch { try { ConfigManager.setActive(configName) // Reload configuration after changing active config reloadConfiguration() } catch (e: Exception) { renderer.renderError("Failed to set active configuration: ${e.message}") } } } /** * Check if configuration is valid for LLM calls */ fun isConfigValid(): Boolean { return _configWrapper.value?.isValid() == true } override fun dispose() { currentJob?.cancel() planStateObserverJob?.cancel() acpViewModel.dispose() coroutineScope.cancel() } } /** * Engine type for the agent ViewModel. * AUTODEV uses the built-in CodingAgent with local LLM. * ACP routes messages to an external ACP agent (Codex, Kimi, Gemini, etc.). */ enum class IdeaEngine { AUTODEV, ACP, } /** * Data class to hold tool loading status information. * Aligned with CodingAgentViewModel's ToolLoadingStatus. */ data class ToolLoadingStatus( val subAgentsEnabled: Int = 0, val subAgentsTotal: Int = 0, val mcpServersLoaded: Int = 0, val mcpServersTotal: Int = 0, val mcpToolsEnabled: Int = 0, val mcpToolsTotal: Int = 0, val isLoading: Boolean = false )