From 13dea293a408a3724d08a164f807adbf9ad36862 Mon Sep 17 00:00:00 2001 From: Phodal Huang Date: Sat, 29 Nov 2025 22:48:32 +0800 Subject: [PATCH 1/4] refactor(mpp-idea): use mpp-ui's ConfigManager and AgentType for cross-platform consistency - Remove duplicate AgentType.kt and LLMConfig.kt from mpp-idea - Use cc.unitmesh.agent.AgentType from mpp-core - Use ConfigManager from mpp-ui for LLM configuration (~/.autodev/config.yaml) - Update IdeaAgentViewModel to load config via ConfigManager.load() - Update IdeaAgentApp to use AgentType.getDisplayName() - Add jogamp and other required repositories for mpp-ui dependencies - Configure dependency substitution for mpp-ui-jvm and mpp-core-jvm targets --- mpp-idea/build.gradle.kts | 19 ++-- mpp-idea/settings.gradle.kts | 20 ++++- .../unitmesh/devins/idea/model/AgentType.kt | 35 -------- .../unitmesh/devins/idea/model/LLMConfig.kt | 43 --------- .../devins/idea/toolwindow/IdeaAgentApp.kt | 13 +-- .../idea/toolwindow/IdeaAgentViewModel.kt | 87 ++++++++++++++++--- 6 files changed, 111 insertions(+), 106 deletions(-) delete mode 100644 mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/model/AgentType.kt delete mode 100644 mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/model/LLMConfig.kt diff --git a/mpp-idea/build.gradle.kts b/mpp-idea/build.gradle.kts index 0070ad5b4e..5e5e67edb6 100644 --- a/mpp-idea/build.gradle.kts +++ b/mpp-idea/build.gradle.kts @@ -2,10 +2,10 @@ import org.jetbrains.intellij.platform.gradle.TestFrameworkType plugins { id("java") - kotlin("jvm") version "2.2.0" - id("org.jetbrains.intellij.platform") version "2.10.2" - kotlin("plugin.compose") version "2.2.0" - kotlin("plugin.serialization") version "2.2.0" + kotlin("jvm") + id("org.jetbrains.intellij.platform") + kotlin("plugin.compose") + kotlin("plugin.serialization") } group = "cc.unitmesh.devins" @@ -26,14 +26,23 @@ kotlin { repositories { mavenCentral() + google() + // Required for mpp-ui's webview dependencies (jogamp) + maven("https://jogamp.org/deployment/maven") + maven("https://oss.sonatype.org/content/repositories/snapshots/") + maven("https://packages.jetbrains.team/maven/p/ij/intellij-dependencies/") intellijPlatform { defaultRepositories() } - google() } dependencies { + // Depend on mpp-ui and mpp-core JVM targets for shared UI components and ConfigManager + // For KMP projects, we need to depend on the JVM target specifically + implementation("cc.unitmesh.devins:mpp-ui-jvm") + implementation("cc.unitmesh.devins:mpp-core-jvm") + // Use platform-provided kotlinx libraries to avoid classloader conflicts compileOnly("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") compileOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1") diff --git a/mpp-idea/settings.gradle.kts b/mpp-idea/settings.gradle.kts index 97ea357ad5..a205d98f32 100644 --- a/mpp-idea/settings.gradle.kts +++ b/mpp-idea/settings.gradle.kts @@ -1,16 +1,28 @@ rootProject.name = "mpp-idea" +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + pluginManagement { repositories { google() mavenCentral() gradlePluginPortal() } + + plugins { + kotlin("jvm") version "2.1.20" + kotlin("plugin.compose") version "2.1.20" + kotlin("plugin.serialization") version "2.1.20" + id("org.jetbrains.intellij.platform") version "2.10.2" + } } -dependencyResolutionManagement { - repositories { - google() - mavenCentral() +// Include mpp-ui from parent project for shared UI components and ConfigManager +// For KMP projects, we substitute the JVM target artifacts +includeBuild("..") { + dependencySubstitution { + substitute(module("cc.unitmesh.devins:mpp-ui-jvm")).using(project(":mpp-ui")) + substitute(module("cc.unitmesh.devins:mpp-core-jvm")).using(project(":mpp-core")) } } + diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/model/AgentType.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/model/AgentType.kt deleted file mode 100644 index 10c66563a3..0000000000 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/model/AgentType.kt +++ /dev/null @@ -1,35 +0,0 @@ -package cc.unitmesh.devins.idea.model - -/** - * Agent types for the IntelliJ ToolWindow. - * Mirrors the AgentType enum from mpp-core but adapted for IntelliJ plugin. - */ -enum class AgentType(val displayName: String) { - /** - * Full-featured coding agent with tools - */ - CODING("Agentic"), - - /** - * Code review mode - specialized agent for code analysis - */ - CODE_REVIEW("Review"), - - /** - * Document reader mode - AI-native document reading and analysis - */ - KNOWLEDGE("Knowledge"), - - /** - * Remote agent mode - connects to remote mpp-server - */ - REMOTE("Remote"); - - companion object { - fun fromDisplayName(name: String): AgentType { - return entries.find { it.displayName.equals(name, ignoreCase = true) } - ?: CODING - } - } -} - diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/model/LLMConfig.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/model/LLMConfig.kt deleted file mode 100644 index 3b30e5e86f..0000000000 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/model/LLMConfig.kt +++ /dev/null @@ -1,43 +0,0 @@ -package cc.unitmesh.devins.idea.model - -import kotlinx.serialization.Serializable - -/** - * LLM provider configuration. - */ -@Serializable -data class LLMConfig( - val provider: LLMProvider = LLMProvider.OPENAI, - val apiKey: String = "", - val modelName: String = "gpt-4", - val baseUrl: String = "", - val temperature: Double = 0.7, - val maxTokens: Int = 4096 -) { - fun isValid(): Boolean { - return when (provider) { - LLMProvider.OLLAMA -> modelName.isNotEmpty() && baseUrl.isNotEmpty() - else -> apiKey.isNotEmpty() && modelName.isNotEmpty() - } - } -} - -/** - * Supported LLM providers. - */ -@Serializable -enum class LLMProvider(val displayName: String) { - OPENAI("OpenAI"), - ANTHROPIC("Anthropic"), - DEEPSEEK("DeepSeek"), - OLLAMA("Ollama"), - OPENROUTER("OpenRouter"); - - companion object { - fun fromDisplayName(name: String): LLMProvider { - return entries.find { it.displayName.equals(name, ignoreCase = true) } - ?: OPENAI - } - } -} - diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt index 3c01a2a4e2..9c60665dc1 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.input.key.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import cc.unitmesh.devins.idea.model.AgentType +import cc.unitmesh.agent.AgentType import cc.unitmesh.devins.idea.model.ChatMessage as ModelChatMessage import cc.unitmesh.devins.idea.model.MessageRole import kotlinx.coroutines.flow.distinctUntilChanged @@ -70,7 +70,7 @@ fun IdeaAgentApp(viewModel: IdeaAgentViewModel) { .weight(1f) ) { when (currentAgentType) { - AgentType.CODING, AgentType.REMOTE -> { + AgentType.CODING, AgentType.REMOTE, AgentType.LOCAL_CHAT -> { ChatContent( messages = messages, streamingOutput = streamingOutput, @@ -89,7 +89,7 @@ fun IdeaAgentApp(viewModel: IdeaAgentViewModel) { Divider(Orientation.Horizontal, modifier = Modifier.fillMaxWidth().height(1.dp)) // Input area (only for chat-based modes) - if (currentAgentType == AgentType.CODING || currentAgentType == AgentType.REMOTE) { + if (currentAgentType == AgentType.CODING || currentAgentType == AgentType.REMOTE || currentAgentType == AgentType.LOCAL_CHAT) { ChatInputArea( isProcessing = isProcessing, onSend = { viewModel.sendMessage(it) }, @@ -215,12 +215,13 @@ private fun AgentTabsHeader( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - // Left: Agent Type Tabs + // Left: Agent Type Tabs (show main agent types, skip LOCAL_CHAT as it's similar to CODING) Row( horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically ) { - AgentType.entries.forEach { type -> + // Show only main agent types for cleaner UI + listOf(AgentType.CODING, AgentType.CODE_REVIEW, AgentType.KNOWLEDGE, AgentType.REMOTE).forEach { type -> AgentTab( type = type, isSelected = type == currentAgentType, @@ -264,7 +265,7 @@ private fun AgentTab( .padding(horizontal = 4.dp, vertical = 2.dp) ) { Text( - text = type.displayName, + text = type.getDisplayName(), style = JewelTheme.defaultTextStyle.copy( fontSize = 12.sp, fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentViewModel.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentViewModel.kt index 4e2952eb38..259275c64a 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentViewModel.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentViewModel.kt @@ -1,9 +1,12 @@ package cc.unitmesh.devins.idea.toolwindow -import cc.unitmesh.devins.idea.model.AgentType +import cc.unitmesh.agent.AgentType import cc.unitmesh.devins.idea.model.ChatMessage as ModelChatMessage -import cc.unitmesh.devins.idea.model.LLMConfig import cc.unitmesh.devins.idea.model.MessageRole +import cc.unitmesh.devins.ui.config.AutoDevConfigWrapper +import cc.unitmesh.devins.ui.config.ConfigManager +import cc.unitmesh.llm.ModelConfig +import cc.unitmesh.llm.NamedModelConfig import com.intellij.openapi.Disposable import com.intellij.openapi.project.Project import kotlinx.coroutines.* @@ -14,13 +17,16 @@ 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. */ class IdeaAgentViewModel( private val project: Project, private val coroutineScope: CoroutineScope ) : Disposable { - // Current agent type tab + // Current agent type tab (using mpp-core's AgentType) private val _currentAgentType = MutableStateFlow(AgentType.CODING) val currentAgentType: StateFlow = _currentAgentType.asStateFlow() @@ -36,9 +42,13 @@ class IdeaAgentViewModel( private val _isProcessing = MutableStateFlow(false) val isProcessing: StateFlow = _isProcessing.asStateFlow() - // LLM Configuration - private val _llmConfig = MutableStateFlow(LLMConfig()) - val llmConfig: StateFlow = _llmConfig.asStateFlow() + // 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() // Show config dialog private val _showConfigDialog = MutableStateFlow(false) @@ -47,6 +57,38 @@ class IdeaAgentViewModel( // Current streaming job (for cancellation) private var currentJob: Job? = null + init { + // Load configuration on initialization + loadConfiguration() + } + + /** + * Load configuration from ConfigManager (~/.autodev/config.yaml) + */ + private fun loadConfiguration() { + coroutineScope.launch { + try { + val wrapper = ConfigManager.load() + _configWrapper.value = wrapper + _currentModelConfig.value = wrapper.getActiveModelConfig() + + // Set agent type from config + _currentAgentType.value = wrapper.getAgentType() + } catch (e: Exception) { + // Config file doesn't exist or is invalid, use defaults + _configWrapper.value = null + _currentModelConfig.value = null + } + } + } + + /** + * Reload configuration from file + */ + fun reloadConfiguration() { + loadConfiguration() + } + /** * Change the current agent type tab. */ @@ -73,7 +115,7 @@ class IdeaAgentViewModel( currentJob = coroutineScope.launch { try { - // TODO: Integrate with actual LLM service + // TODO: Integrate with actual LLM service using _currentModelConfig // For now, simulate a response simulateResponse(content) } catch (e: CancellationException) { @@ -92,11 +134,15 @@ class IdeaAgentViewModel( } private suspend fun simulateResponse(userMessage: String) { + val configInfo = _currentModelConfig.value?.let { config -> + "Using model: ${config.modelName} (${config.provider})" + } ?: "No LLM configured. Please configure in ~/.autodev/config.yaml" + val response = """ This is a simulated response for: "$userMessage" - - To enable real LLM responses, please configure your API key in the settings. - + + $configInfo + Supported features: - **Agentic**: Full coding agent with file operations - **Review**: Code review and analysis @@ -146,10 +192,18 @@ class IdeaAgentViewModel( } /** - * Update LLM configuration. + * Save a new LLM configuration using ConfigManager */ - fun updateLLMConfig(config: LLMConfig) { - _llmConfig.value = config + fun saveModelConfig(config: NamedModelConfig, setActive: Boolean = true) { + coroutineScope.launch { + try { + ConfigManager.saveConfig(config, setActive) + // Reload configuration after saving + loadConfiguration() + } catch (e: Exception) { + // Handle save error + } + } } /** @@ -159,6 +213,13 @@ class IdeaAgentViewModel( _showConfigDialog.value = show } + /** + * Check if configuration is valid for LLM calls + */ + fun isConfigValid(): Boolean { + return _configWrapper.value?.isValid() == true + } + override fun dispose() { currentJob?.cancel() coroutineScope.cancel() From a1f5030b0aa28c072ff47245d486370408fc0c02 Mon Sep 17 00:00:00 2001 From: Phodal Huang Date: Sat, 29 Nov 2025 23:03:02 +0800 Subject: [PATCH 2/4] fix(mpp-idea): exclude Compose dependencies to avoid classloader conflicts Exclude all Compose and UI library dependencies from mpp-ui and mpp-core to prevent ClassCastException caused by duplicate Compose Runtime classes loaded from different classloaders (plugin vs IntelliJ bundled). Excluded dependencies: - org.jetbrains.compose.* (runtime, foundation, material3, ui, desktop, etc.) - org.jetbrains.skiko - io.github.kevinnzou (webview/KCEF) - dev.datlag (jcef) - com.mohamedrejeb.richeditor - cafe.adriel.bonsai - com.mikepenz (markdown renderer) - org.jetbrains.jediterm, org.jetbrains.pty4j - io.github.vinceglb (filekit) --- mpp-idea/build.gradle.kts | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/mpp-idea/build.gradle.kts b/mpp-idea/build.gradle.kts index 5e5e67edb6..e18b335f67 100644 --- a/mpp-idea/build.gradle.kts +++ b/mpp-idea/build.gradle.kts @@ -40,8 +40,38 @@ repositories { dependencies { // Depend on mpp-ui and mpp-core JVM targets for shared UI components and ConfigManager // For KMP projects, we need to depend on the JVM target specifically - implementation("cc.unitmesh.devins:mpp-ui-jvm") - implementation("cc.unitmesh.devins:mpp-core-jvm") + // IMPORTANT: Exclude Compose dependencies to avoid classloader conflicts with IntelliJ's bundled Compose + implementation("cc.unitmesh.devins:mpp-ui-jvm") { + // Exclude all Compose dependencies - IntelliJ provides its own via bundledModules + exclude(group = "org.jetbrains.compose.runtime") + exclude(group = "org.jetbrains.compose.foundation") + exclude(group = "org.jetbrains.compose.material3") + exclude(group = "org.jetbrains.compose.material") + exclude(group = "org.jetbrains.compose.ui") + exclude(group = "org.jetbrains.compose.desktop") + exclude(group = "org.jetbrains.compose.components") + exclude(group = "org.jetbrains.compose.animation") + exclude(group = "org.jetbrains.skiko") + // Exclude webview/KCEF - not needed in IntelliJ and causes issues + exclude(group = "io.github.kevinnzou") + exclude(group = "dev.datlag") + // Exclude other UI libraries that may conflict + exclude(group = "com.mohamedrejeb.richeditor") + exclude(group = "cafe.adriel.bonsai") + exclude(group = "com.mikepenz") + exclude(group = "org.jetbrains.jediterm") + exclude(group = "org.jetbrains.pty4j") + exclude(group = "io.github.vinceglb") + } + implementation("cc.unitmesh.devins:mpp-core-jvm") { + // Exclude Compose dependencies from mpp-core as well + exclude(group = "org.jetbrains.compose.runtime") + exclude(group = "org.jetbrains.compose.foundation") + exclude(group = "org.jetbrains.compose.material3") + exclude(group = "org.jetbrains.compose.material") + exclude(group = "org.jetbrains.compose.ui") + exclude(group = "org.jetbrains.skiko") + } // Use platform-provided kotlinx libraries to avoid classloader conflicts compileOnly("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3") From b792e2469ac6b0c5a545def75e3ec67a866445f9 Mon Sep 17 00:00:00 2001 From: Phodal Huang Date: Sat, 29 Nov 2025 23:10:31 +0800 Subject: [PATCH 3/4] fix(mpp-idea): exclude all kotlinx dependencies and add verification task - Exclude kotlinx-serialization-json-io and kotlinx-io-core modules - Add verifyNoDuplicateDependencies task to detect classloader conflicts - Task runs automatically before build to catch issues early - Validates that no Compose/Kotlinx dependencies are included --- mpp-idea/build.gradle.kts | 76 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/mpp-idea/build.gradle.kts b/mpp-idea/build.gradle.kts index e18b335f67..96b528012b 100644 --- a/mpp-idea/build.gradle.kts +++ b/mpp-idea/build.gradle.kts @@ -40,9 +40,10 @@ repositories { dependencies { // Depend on mpp-ui and mpp-core JVM targets for shared UI components and ConfigManager // For KMP projects, we need to depend on the JVM target specifically - // IMPORTANT: Exclude Compose dependencies to avoid classloader conflicts with IntelliJ's bundled Compose + // IMPORTANT: Exclude ALL transitive dependencies that conflict with IntelliJ's bundled libraries implementation("cc.unitmesh.devins:mpp-ui-jvm") { // Exclude all Compose dependencies - IntelliJ provides its own via bundledModules + exclude(group = "org.jetbrains.compose") exclude(group = "org.jetbrains.compose.runtime") exclude(group = "org.jetbrains.compose.foundation") exclude(group = "org.jetbrains.compose.material3") @@ -52,6 +53,18 @@ dependencies { exclude(group = "org.jetbrains.compose.components") exclude(group = "org.jetbrains.compose.animation") exclude(group = "org.jetbrains.skiko") + // Exclude kotlinx libraries - IntelliJ provides its own + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-coroutines-core") + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-coroutines-core-jvm") + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-coroutines-swing") + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-serialization-json") + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-serialization-json-jvm") + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-serialization-json-io") + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-serialization-json-io-jvm") + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-serialization-core") + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-serialization-core-jvm") + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-io-core") + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-io-core-jvm") // Exclude webview/KCEF - not needed in IntelliJ and causes issues exclude(group = "io.github.kevinnzou") exclude(group = "dev.datlag") @@ -62,15 +75,29 @@ dependencies { exclude(group = "org.jetbrains.jediterm") exclude(group = "org.jetbrains.pty4j") exclude(group = "io.github.vinceglb") + // Exclude SQLDelight - not needed in IntelliJ plugin + exclude(group = "app.cash.sqldelight") } implementation("cc.unitmesh.devins:mpp-core-jvm") { // Exclude Compose dependencies from mpp-core as well + exclude(group = "org.jetbrains.compose") exclude(group = "org.jetbrains.compose.runtime") exclude(group = "org.jetbrains.compose.foundation") exclude(group = "org.jetbrains.compose.material3") exclude(group = "org.jetbrains.compose.material") exclude(group = "org.jetbrains.compose.ui") exclude(group = "org.jetbrains.skiko") + // Exclude kotlinx libraries - IntelliJ provides its own + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-coroutines-core") + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-coroutines-core-jvm") + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-serialization-json") + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-serialization-json-jvm") + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-serialization-json-io") + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-serialization-json-io-jvm") + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-serialization-core") + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-serialization-core-jvm") + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-io-core") + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-io-core-jvm") } // Use platform-provided kotlinx libraries to avoid classloader conflicts @@ -124,4 +151,51 @@ tasks { test { useJUnitPlatform() } + + // Task to verify no conflicting dependencies are included + register("verifyNoDuplicateDependencies") { + group = "verification" + description = "Verifies that no Compose/Kotlinx dependencies are included that would conflict with IntelliJ's bundled versions" + + doLast { + val forbiddenPatterns = listOf( + "org.jetbrains.compose", + "org.jetbrains.skiko", + "kotlinx-coroutines-core", + "kotlinx-coroutines-swing", + "kotlinx-serialization-json", + "kotlinx-serialization-core" + ) + + val runtimeClasspath = configurations.getByName("runtimeClasspath") + val violations = mutableListOf() + + runtimeClasspath.resolvedConfiguration.resolvedArtifacts.forEach { artifact -> + val id = artifact.moduleVersion.id + val fullName = "${id.group}:${id.name}:${id.version}" + forbiddenPatterns.forEach { pattern -> + if (fullName.contains(pattern)) { + violations.add(fullName) + } + } + } + + if (violations.isNotEmpty()) { + throw GradleException(""" + |DEPENDENCY CONFLICT DETECTED! + |The following dependencies will conflict with IntelliJ's bundled libraries: + |${violations.joinToString("\n") { " - $it" }} + | + |These dependencies must be excluded from mpp-ui and mpp-core. + """.trimMargin()) + } else { + println("✓ No conflicting dependencies found in runtime classpath") + } + } + } + + // Run verification before build + named("build") { + dependsOn("verifyNoDuplicateDependencies") + } } From 6cd95fe77e821b46fc117d26d239bbd1350c3bdd Mon Sep 17 00:00:00 2001 From: Phodal Huang Date: Sat, 29 Nov 2025 23:22:15 +0800 Subject: [PATCH 4/4] feat(mpp-idea): integrate CodingAgent from mpp-core for real agent execution - Create JewelRenderer implementing CodingAgentRenderer with StateFlow - Refactor IdeaAgentViewModel to use CodingAgent instead of simulated responses - Update IdeaAgentApp to display timeline-based agent output - Add UI components for tool calls, errors, and task completion - Remove obsolete ChatMessage model (replaced by JewelRenderer.TimelineItem) The IDEA plugin now uses the same CodingAgent as CLI and Desktop apps, ensuring consistent behavior across all platforms. --- .../unitmesh/devins/idea/model/ChatMessage.kt | 21 -- .../devins/idea/renderer/JewelRenderer.kt | 262 ++++++++++++++++++ .../devins/idea/toolwindow/IdeaAgentApp.kt | 181 ++++++++++-- .../idea/toolwindow/IdeaAgentViewModel.kt | 152 +++++----- 4 files changed, 495 insertions(+), 121 deletions(-) delete mode 100644 mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/model/ChatMessage.kt create mode 100644 mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/JewelRenderer.kt diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/model/ChatMessage.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/model/ChatMessage.kt deleted file mode 100644 index 21cd3174bb..0000000000 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/model/ChatMessage.kt +++ /dev/null @@ -1,21 +0,0 @@ -package cc.unitmesh.devins.idea.model - -/** - * Represents a chat message in the agent interface. - */ -data class ChatMessage( - val id: String = java.util.UUID.randomUUID().toString(), - val content: String, - val role: MessageRole, - val timestamp: Long = System.currentTimeMillis() -) - -/** - * The role of a message sender. - */ -enum class MessageRole { - USER, - ASSISTANT, - SYSTEM -} - diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/JewelRenderer.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/JewelRenderer.kt new file mode 100644 index 0000000000..9595598049 --- /dev/null +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/JewelRenderer.kt @@ -0,0 +1,262 @@ +package cc.unitmesh.devins.idea.renderer + +import cc.unitmesh.agent.render.BaseRenderer +import cc.unitmesh.llm.compression.TokenInfo +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +/** + * Jewel-compatible Renderer for IntelliJ IDEA plugin. + * + * Uses Kotlin StateFlow instead of Compose mutableStateOf to avoid + * ClassLoader conflicts with IntelliJ's bundled Compose runtime. + * + * Implements CodingAgentRenderer interface from mpp-core. + */ +class JewelRenderer : BaseRenderer() { + + // Timeline of all events (messages, tool calls, results) + private val _timeline = MutableStateFlow>(emptyList()) + val timeline: StateFlow> = _timeline.asStateFlow() + + // Current streaming output from LLM + private val _currentStreamingOutput = MutableStateFlow("") + val currentStreamingOutput: StateFlow = _currentStreamingOutput.asStateFlow() + + // Processing state + private val _isProcessing = MutableStateFlow(false) + val isProcessing: StateFlow = _isProcessing.asStateFlow() + + // Iteration tracking + private val _currentIteration = MutableStateFlow(0) + val currentIteration: StateFlow = _currentIteration.asStateFlow() + + private val _maxIterations = MutableStateFlow(100) + val maxIterations: StateFlow = _maxIterations.asStateFlow() + + // Current active tool call + private val _currentToolCall = MutableStateFlow(null) + val currentToolCall: StateFlow = _currentToolCall.asStateFlow() + + // Error state + private val _errorMessage = MutableStateFlow(null) + val errorMessage: StateFlow = _errorMessage.asStateFlow() + + // Task completion state + private val _taskCompleted = MutableStateFlow(false) + val taskCompleted: StateFlow = _taskCompleted.asStateFlow() + + // Token tracking + private val _totalTokenInfo = MutableStateFlow(TokenInfo()) + val totalTokenInfo: StateFlow = _totalTokenInfo.asStateFlow() + + // Execution timing + private var executionStartTime = 0L + + // Data classes for timeline items + sealed class TimelineItem(val timestamp: Long = System.currentTimeMillis()) { + data class MessageItem( + val role: MessageRole, + val content: String, + val tokenInfo: TokenInfo? = null, + val itemTimestamp: Long = System.currentTimeMillis() + ) : TimelineItem(itemTimestamp) + + data class ToolCallItem( + val toolName: String, + val params: String, + val success: Boolean? = null, + val output: String? = null, + val executionTimeMs: Long? = null, + val itemTimestamp: Long = System.currentTimeMillis() + ) : TimelineItem(itemTimestamp) + + data class ErrorItem( + val message: String, + val itemTimestamp: Long = System.currentTimeMillis() + ) : TimelineItem(itemTimestamp) + + data class TaskCompleteItem( + val success: Boolean, + val message: String, + val iterations: Int, + val itemTimestamp: Long = System.currentTimeMillis() + ) : TimelineItem(itemTimestamp) + } + + data class ToolCallInfo( + val toolName: String, + val params: String + ) + + enum class MessageRole { + USER, ASSISTANT, SYSTEM + } + + // BaseRenderer implementation + + override fun renderIterationHeader(current: Int, max: Int) { + _currentIteration.value = current + _maxIterations.value = max + } + + override fun renderLLMResponseStart() { + super.renderLLMResponseStart() + _currentStreamingOutput.value = "" + _isProcessing.value = true + + if (executionStartTime == 0L) { + executionStartTime = System.currentTimeMillis() + } + } + + override fun renderLLMResponseChunk(chunk: String) { + reasoningBuffer.append(chunk) + + // Wait for more content if we detect an incomplete devin block + if (hasIncompleteDevinBlock(reasoningBuffer.toString())) { + return + } + + // Filter devin blocks and output clean content + val processedContent = filterDevinBlocks(reasoningBuffer.toString()) + val cleanContent = cleanNewlines(processedContent) + + _currentStreamingOutput.value = cleanContent + } + + override fun renderLLMResponseEnd() { + super.renderLLMResponseEnd() + + val content = _currentStreamingOutput.value.trim() + if (content.isNotEmpty()) { + addTimelineItem( + TimelineItem.MessageItem( + role = MessageRole.ASSISTANT, + content = content, + tokenInfo = _totalTokenInfo.value + ) + ) + } + + _currentStreamingOutput.value = "" + _isProcessing.value = false + } + + override fun renderToolCall(toolName: String, paramsStr: String) { + _currentToolCall.value = ToolCallInfo(toolName, paramsStr) + + addTimelineItem( + TimelineItem.ToolCallItem( + toolName = toolName, + params = paramsStr + ) + ) + } + + override fun renderToolResult( + toolName: String, + success: Boolean, + output: String?, + fullOutput: String?, + metadata: Map + ) { + _currentToolCall.value = null + + // Update the last tool call item with result + _timeline.update { items -> + items.mapIndexed { index, item -> + if (index == items.lastIndex && item is TimelineItem.ToolCallItem && item.toolName == toolName) { + item.copy( + success = success, + output = output ?: fullOutput + ) + } else { + item + } + } + } + } + + override fun renderTaskComplete() { + _taskCompleted.value = true + } + + override fun renderFinalResult(success: Boolean, message: String, iterations: Int) { + addTimelineItem( + TimelineItem.TaskCompleteItem( + success = success, + message = message, + iterations = iterations + ) + ) + _isProcessing.value = false + executionStartTime = 0L + } + + override fun renderError(message: String) { + _errorMessage.value = message + addTimelineItem(TimelineItem.ErrorItem(message)) + } + + override fun renderRepeatWarning(toolName: String, count: Int) { + val warning = "Tool '$toolName' called repeatedly ($count times)" + addTimelineItem(TimelineItem.ErrorItem(warning)) + } + + override fun renderRecoveryAdvice(recoveryAdvice: String) { + addTimelineItem( + TimelineItem.MessageItem( + role = MessageRole.ASSISTANT, + content = "🔧 Recovery Advice:\n$recoveryAdvice" + ) + ) + } + + override fun renderUserConfirmationRequest(toolName: String, params: Map) { + // For now, just render as a message + addTimelineItem( + TimelineItem.MessageItem( + role = MessageRole.SYSTEM, + content = "⚠️ Confirmation required for tool: $toolName" + ) + ) + } + + override fun updateTokenInfo(tokenInfo: TokenInfo) { + _totalTokenInfo.value = tokenInfo + } + + // Public methods for UI interaction + + fun addUserMessage(content: String) { + addTimelineItem( + TimelineItem.MessageItem( + role = MessageRole.USER, + content = content + ) + ) + } + + fun clearTimeline() { + _timeline.value = emptyList() + _currentStreamingOutput.value = "" + _isProcessing.value = false + _currentIteration.value = 0 + _errorMessage.value = null + _taskCompleted.value = false + _totalTokenInfo.value = TokenInfo() + executionStartTime = 0L + } + + fun reset() { + clearTimeline() + } + + private fun addTimelineItem(item: TimelineItem) { + _timeline.update { it + item } + } +} + diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt index 9c60665dc1..5a05aa29b4 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt @@ -9,13 +9,13 @@ import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import cc.unitmesh.agent.AgentType -import cc.unitmesh.devins.idea.model.ChatMessage as ModelChatMessage -import cc.unitmesh.devins.idea.model.MessageRole +import cc.unitmesh.devins.idea.renderer.JewelRenderer import kotlinx.coroutines.flow.distinctUntilChanged import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.ui.Orientation @@ -24,27 +24,29 @@ import org.jetbrains.jewel.ui.theme.defaultBannerStyle /** * Main Compose application for Agent ToolWindow. - * + * * Features: * - Tab-based agent type switching (Agentic, Review, Knowledge, Remote) - * - Chat interface with message history - * - LLM configuration support + * - Timeline-based chat interface with tool calls + * - LLM configuration support via mpp-ui's ConfigManager + * - Real agent execution using mpp-core's CodingAgent */ @Composable fun IdeaAgentApp(viewModel: IdeaAgentViewModel) { val currentAgentType by viewModel.currentAgentType.collectAsState() - val messages by viewModel.messages.collectAsState() - val streamingOutput by viewModel.streamingOutput.collectAsState() + val timeline by viewModel.renderer.timeline.collectAsState() + val streamingOutput by viewModel.renderer.currentStreamingOutput.collectAsState() val isProcessing by viewModel.isProcessing.collectAsState() val showConfigDialog by viewModel.showConfigDialog.collectAsState() val listState = rememberLazyListState() - // Auto-scroll to bottom when new messages arrive - LaunchedEffect(messages.size, streamingOutput) { - if (messages.isNotEmpty() || streamingOutput.isNotEmpty()) { - listState.animateScrollToItem( - if (streamingOutput.isNotEmpty()) messages.size else messages.lastIndex.coerceAtLeast(0) - ) + // Auto-scroll to bottom when new items arrive + LaunchedEffect(timeline.size, streamingOutput) { + if (timeline.isNotEmpty() || streamingOutput.isNotEmpty()) { + val targetIndex = if (streamingOutput.isNotEmpty()) timeline.size else timeline.lastIndex.coerceAtLeast(0) + if (targetIndex >= 0) { + listState.animateScrollToItem(targetIndex) + } } } @@ -71,8 +73,8 @@ fun IdeaAgentApp(viewModel: IdeaAgentViewModel) { ) { when (currentAgentType) { AgentType.CODING, AgentType.REMOTE, AgentType.LOCAL_CHAT -> { - ChatContent( - messages = messages, + TimelineContent( + timeline = timeline, streamingOutput = streamingOutput, listState = listState ) @@ -100,12 +102,12 @@ fun IdeaAgentApp(viewModel: IdeaAgentViewModel) { } @Composable -private fun ChatContent( - messages: List, +private fun TimelineContent( + timeline: List, streamingOutput: String, listState: androidx.compose.foundation.lazy.LazyListState ) { - if (messages.isEmpty() && streamingOutput.isEmpty()) { + if (timeline.isEmpty() && streamingOutput.isEmpty()) { EmptyStateMessage("Start a conversation with your AI Assistant!") } else { LazyColumn( @@ -114,8 +116,8 @@ private fun ChatContent( contentPadding = PaddingValues(8.dp), verticalArrangement = Arrangement.spacedBy(4.dp) ) { - items(messages, key = { it.id }) { message -> - MessageBubble(message) + items(timeline, key = { it.timestamp }) { item -> + TimelineItemView(item) } // Show streaming output @@ -128,6 +130,27 @@ private fun ChatContent( } } +@Composable +private fun TimelineItemView(item: JewelRenderer.TimelineItem) { + when (item) { + is JewelRenderer.TimelineItem.MessageItem -> { + MessageBubble( + role = item.role, + content = item.content + ) + } + is JewelRenderer.TimelineItem.ToolCallItem -> { + ToolCallBubble(item) + } + is JewelRenderer.TimelineItem.ErrorItem -> { + ErrorBubble(item.message) + } + is JewelRenderer.TimelineItem.TaskCompleteItem -> { + TaskCompleteBubble(item) + } + } +} + @Composable private fun CodeReviewContent() { EmptyStateMessage("Code Review mode - Coming soon!") @@ -155,15 +178,15 @@ private fun EmptyStateMessage(text: String) { } @Composable -private fun MessageBubble(message: ModelChatMessage) { - val isUser = message.role == MessageRole.USER +private fun MessageBubble(role: JewelRenderer.MessageRole, content: String) { + val isUser = role == JewelRenderer.MessageRole.USER Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = if (isUser) Arrangement.End else Arrangement.Start ) { Box( modifier = Modifier - .widthIn(max = 400.dp) + .widthIn(max = 500.dp) .background( if (isUser) JewelTheme.defaultBannerStyle.information.colors.background.copy(alpha = 0.75f) @@ -173,13 +196,121 @@ private fun MessageBubble(message: ModelChatMessage) { .padding(8.dp) ) { Text( - text = message.content, + text = content, style = JewelTheme.defaultTextStyle ) } } } +@Composable +private fun ToolCallBubble(item: JewelRenderer.TimelineItem.ToolCallItem) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start + ) { + Box( + modifier = Modifier + .widthIn(max = 500.dp) + .background(JewelTheme.globalColors.panelBackground.copy(alpha = 0.5f)) + .padding(8.dp) + ) { + Column { + // Tool name with icon + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val statusIcon = when (item.success) { + true -> "✓" + false -> "✗" + null -> "⏳" + } + val statusColor = when (item.success) { + true -> Color(0xFF4CAF50) // Green + false -> Color(0xFFF44336) // Red + null -> JewelTheme.globalColors.text.info + } + Text( + text = statusIcon, + style = JewelTheme.defaultTextStyle.copy(color = statusColor) + ) + Text( + text = "🔧 ${item.toolName}", + style = JewelTheme.defaultTextStyle.copy(fontWeight = FontWeight.Bold) + ) + } + + // Tool parameters (truncated) + if (item.params.isNotEmpty()) { + Text( + text = item.params.take(200) + if (item.params.length > 200) "..." else "", + style = JewelTheme.defaultTextStyle.copy( + fontSize = 12.sp, + color = JewelTheme.globalColors.text.info + ) + ) + } + + // Tool output (if available) + item.output?.let { output -> + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = output.take(300) + if (output.length > 300) "..." else "", + style = JewelTheme.defaultTextStyle.copy(fontSize = 12.sp) + ) + } + } + } + } +} + +@Composable +private fun ErrorBubble(message: String) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Start + ) { + Box( + modifier = Modifier + .widthIn(max = 500.dp) + .background(Color(0x33F44336)) // Light red background + .padding(8.dp) + ) { + Text( + text = "❌ $message", + style = JewelTheme.defaultTextStyle.copy( + color = Color(0xFFF44336) + ) + ) + } + } +} + +@Composable +private fun TaskCompleteBubble(item: JewelRenderer.TimelineItem.TaskCompleteItem) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + Box( + modifier = Modifier + .background( + if (item.success) Color(0x334CAF50) else Color(0x33F44336) + ) + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + val icon = if (item.success) "✅" else "❌" + Text( + text = "$icon ${item.message} (${item.iterations} iterations)", + style = JewelTheme.defaultTextStyle.copy( + fontWeight = FontWeight.Bold + ) + ) + } + } +} + @Composable private fun StreamingMessageBubble(content: String) { Row( @@ -188,7 +319,7 @@ private fun StreamingMessageBubble(content: String) { ) { Box( modifier = Modifier - .widthIn(max = 400.dp) + .widthIn(max = 500.dp) .background(JewelTheme.globalColors.panelBackground) .padding(8.dp) ) { diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentViewModel.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentViewModel.kt index 259275c64a..7b2f8a7793 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentViewModel.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentViewModel.kt @@ -1,10 +1,14 @@ package cc.unitmesh.devins.idea.toolwindow +import cc.unitmesh.agent.AgentTask import cc.unitmesh.agent.AgentType -import cc.unitmesh.devins.idea.model.ChatMessage as ModelChatMessage -import cc.unitmesh.devins.idea.model.MessageRole +import cc.unitmesh.agent.CodingAgent +import cc.unitmesh.agent.config.McpToolConfigService +import cc.unitmesh.agent.config.ToolConfigFile +import cc.unitmesh.devins.idea.renderer.JewelRenderer import cc.unitmesh.devins.ui.config.AutoDevConfigWrapper import cc.unitmesh.devins.ui.config.ConfigManager +import cc.unitmesh.llm.KoogLLMService import cc.unitmesh.llm.ModelConfig import cc.unitmesh.llm.NamedModelConfig import com.intellij.openapi.Disposable @@ -20,24 +24,21 @@ import kotlinx.coroutines.flow.asStateFlow * * 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. */ class IdeaAgentViewModel( private val project: Project, private val coroutineScope: CoroutineScope ) : Disposable { + // Renderer for agent output (uses StateFlow instead of Compose mutableStateOf) + val renderer = JewelRenderer() + // Current agent type tab (using mpp-core's AgentType) private val _currentAgentType = MutableStateFlow(AgentType.CODING) val currentAgentType: StateFlow = _currentAgentType.asStateFlow() - // Chat messages - private val _messages = MutableStateFlow>(emptyList()) - val messages: StateFlow> = _messages.asStateFlow() - - // Current streaming output - private val _streamingOutput = MutableStateFlow("") - val streamingOutput: StateFlow = _streamingOutput.asStateFlow() - // Is processing a request private val _isProcessing = MutableStateFlow(false) val isProcessing: StateFlow = _isProcessing.asStateFlow() @@ -50,11 +51,18 @@ class IdeaAgentViewModel( private val _currentModelConfig = MutableStateFlow(null) val currentModelConfig: StateFlow = _currentModelConfig.asStateFlow() + // LLM Service (created from config) + private var llmService: KoogLLMService? = null + + // CodingAgent instance + private var codingAgent: CodingAgent? = null + private var agentInitialized = false + // Show config dialog private val _showConfigDialog = MutableStateFlow(false) val showConfigDialog: StateFlow = _showConfigDialog.asStateFlow() - // Current streaming job (for cancellation) + // Current execution job (for cancellation) private var currentJob: Job? = null init { @@ -70,7 +78,13 @@ class IdeaAgentViewModel( try { val wrapper = ConfigManager.load() _configWrapper.value = wrapper - _currentModelConfig.value = wrapper.getActiveModelConfig() + val modelConfig = wrapper.getActiveModelConfig() + _currentModelConfig.value = modelConfig + + // Create LLM service if config is valid + if (modelConfig != null && modelConfig.isValid()) { + llmService = KoogLLMService.create(modelConfig) + } // Set agent type from config _currentAgentType.value = wrapper.getAgentType() @@ -78,6 +92,7 @@ class IdeaAgentViewModel( // Config file doesn't exist or is invalid, use defaults _configWrapper.value = null _currentModelConfig.value = null + llmService = null } } } @@ -86,6 +101,8 @@ class IdeaAgentViewModel( * Reload configuration from file */ fun reloadConfiguration() { + agentInitialized = false + codingAgent = null loadConfiguration() } @@ -97,98 +114,83 @@ class IdeaAgentViewModel( } /** - * Send a message to the LLM. + * Initialize the CodingAgent with tool configuration + */ + 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 = try { + ConfigManager.loadToolConfig() + } catch (e: Exception) { + ToolConfigFile.default() + } + + val mcpToolConfigService = McpToolConfigService(toolConfig) + val projectPath = project.basePath ?: System.getProperty("user.home") + + codingAgent = CodingAgent( + projectPath = projectPath, + llmService = service, + maxIterations = 100, + renderer = renderer, + mcpToolConfigService = mcpToolConfigService, + enableLLMStreaming = true + ) + agentInitialized = true + } + return codingAgent!! + } + + /** + * Send a message to the Agent. */ fun sendMessage(content: String) { if (content.isBlank() || _isProcessing.value) return - // Add user message - val userMessage = ModelChatMessage( - content = content, - role = MessageRole.USER - ) - _messages.value = _messages.value + listOf(userMessage) + // Add user message to renderer timeline + renderer.addUserMessage(content) // Start processing _isProcessing.value = true - _streamingOutput.value = "" currentJob = coroutineScope.launch { try { - // TODO: Integrate with actual LLM service using _currentModelConfig - // For now, simulate a response - simulateResponse(content) + val agent = initializeCodingAgent() + val projectPath = project.basePath ?: System.getProperty("user.home") + + val task = AgentTask( + requirement = content, + projectPath = projectPath + ) + + // Execute the agent task + agent.executeTask(task) + } catch (e: CancellationException) { - // Cancelled by user + renderer.renderError("Task cancelled by user") } catch (e: Exception) { - val errorMessage = ModelChatMessage( - content = "Error: ${e.message}", - role = MessageRole.ASSISTANT - ) - _messages.value = _messages.value + listOf(errorMessage) + renderer.renderError("Error: ${e.message}") } finally { _isProcessing.value = false - _streamingOutput.value = "" } } } - private suspend fun simulateResponse(userMessage: String) { - val configInfo = _currentModelConfig.value?.let { config -> - "Using model: ${config.modelName} (${config.provider})" - } ?: "No LLM configured. Please configure in ~/.autodev/config.yaml" - - val response = """ - This is a simulated response for: "$userMessage" - - $configInfo - - Supported features: - - **Agentic**: Full coding agent with file operations - - **Review**: Code review and analysis - - **Knowledge**: Document reading and Q&A - - **Remote**: Connect to remote mpp-server - """.trimIndent() - - // Simulate streaming - for (char in response) { - if (!coroutineScope.isActive) break - _streamingOutput.value += char - delay(10) - } - - // Add final message - val assistantMessage = ModelChatMessage( - content = response, - role = MessageRole.ASSISTANT - ) - _messages.value = _messages.value + listOf(assistantMessage) - } - /** * Abort the current request. - * Preserves partial streaming output if any. */ fun abortRequest() { currentJob?.cancel() - // Preserve partial output if any - if (_streamingOutput.value.isNotEmpty()) { - val partialMessage = ModelChatMessage( - content = _streamingOutput.value + "\n\n[Interrupted]", - role = MessageRole.ASSISTANT - ) - _messages.value = _messages.value + listOf(partialMessage) - } _isProcessing.value = false - _streamingOutput.value = "" } /** * Clear chat history. */ fun clearHistory() { - _messages.value = emptyList() - _streamingOutput.value = "" + renderer.clearTimeline() } /** @@ -199,7 +201,7 @@ class IdeaAgentViewModel( try { ConfigManager.saveConfig(config, setActive) // Reload configuration after saving - loadConfiguration() + reloadConfiguration() } catch (e: Exception) { // Handle save error }