diff --git a/mpp-idea/build.gradle.kts b/mpp-idea/build.gradle.kts index 7df79f882f..323b6c2987 100644 --- a/mpp-idea/build.gradle.kts +++ b/mpp-idea/build.gradle.kts @@ -5,7 +5,6 @@ plugins { kotlin("jvm") id("org.jetbrains.intellij.platform") kotlin("plugin.compose") - kotlin("plugin.serialization") } group = "cc.unitmesh.devins" @@ -102,15 +101,40 @@ dependencies { } // 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") + // Gson for JSON serialization (used by IdeaRemoteAgentClient) + compileOnly("com.google.code.gson:gson:2.11.0") + // Note: We use SimpleJewelMarkdown with intellij-markdown parser instead of mikepenz // to avoid Compose runtime version mismatch with IntelliJ's bundled Compose // SQLite JDBC driver for SQLDelight (required at runtime) implementation("org.xerial:sqlite-jdbc:3.49.1.0") + // DevIn language support for @ and / completion + // These provide the DevIn language parser, completion contributors, and core functionality + implementation("AutoDev-Intellij:exts-devins-lang:$mppVersion") { + // 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-core") + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-serialization-core-jvm") + } + implementation("AutoDev-Intellij:core:$mppVersion") { + // 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-core") + exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-serialization-core-jvm") + } + // Ktor HTTP Client for LLM API calls - use compileOnly for libraries that may conflict compileOnly("io.ktor:ktor-client-core:3.2.2") compileOnly("io.ktor:ktor-client-cio:3.2.2") @@ -128,7 +152,7 @@ dependencies { // Target IntelliJ IDEA 2025.2+ for Compose support create("IC", "2025.2.1") - bundledPlugins("com.intellij.java") + bundledPlugins("com.intellij.java", "org.intellij.plugins.markdown", "com.jetbrains.sh", "Git4Idea") // Compose support dependencies (bundled in IDEA 252+) bundledModules( diff --git a/mpp-idea/gradle.properties b/mpp-idea/gradle.properties new file mode 100644 index 0000000000..c4921efaaf --- /dev/null +++ b/mpp-idea/gradle.properties @@ -0,0 +1,15 @@ +# Gradle JVM memory settings +org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -Dfile.encoding=UTF-8 + +# Kotlin daemon memory settings +kotlin.daemon.jvmargs=-Xmx4g + +# Enable Gradle Configuration Cache +org.gradle.configuration-cache = true + +# Enable Gradle Build Cache +org.gradle.caching = true + +# Kotlin stdlib +kotlin.stdlib.default.dependency = false + diff --git a/mpp-idea/settings.gradle.kts b/mpp-idea/settings.gradle.kts index c8a23317c4..d7a203841c 100644 --- a/mpp-idea/settings.gradle.kts +++ b/mpp-idea/settings.gradle.kts @@ -10,9 +10,8 @@ pluginManagement { } plugins { - kotlin("jvm") version "2.1.20" - kotlin("plugin.compose") version "2.1.20" - kotlin("plugin.serialization") version "2.1.20" + kotlin("jvm") version "2.2.0" + kotlin("plugin.compose") version "2.2.0" id("org.jetbrains.intellij.platform") version "2.10.2" } } @@ -24,6 +23,7 @@ pluginManagement { // - mpp-core: group = "cc.unitmesh" // - mpp-codegraph: uses root project name // - mpp-viewer: group = "cc.unitmesh.viewer" +// - devins-lang, core: uses root project name "AutoDev-Intellij" as group includeBuild("..") { dependencySubstitution { // Substitute Maven coordinates with project dependencies @@ -31,6 +31,9 @@ includeBuild("..") { substitute(module("cc.unitmesh:mpp-core")).using(project(":mpp-core")).because("Using local project") substitute(module("AutoDev-Intellij:mpp-codegraph")).using(project(":mpp-codegraph")).because("Using local project") substitute(module("cc.unitmesh.viewer:mpp-viewer")).using(project(":mpp-viewer")).because("Using local project") + // DevIn language support for @ and / completion + substitute(module("AutoDev-Intellij:exts-devins-lang")).using(project(":exts:devins-lang")).because("Using local project") + substitute(module("AutoDev-Intellij:core")).using(project(":core")).because("Using local project") } } diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaBottomToolbar.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaBottomToolbar.kt index 6af9bc6878..82b62a15d8 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaBottomToolbar.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaBottomToolbar.kt @@ -19,11 +19,13 @@ import org.jetbrains.jewel.ui.component.Icon /** * Bottom toolbar for the input section. - * Provides send/stop buttons, @ trigger for agent completion, / command trigger, model selector, settings, and token info. + * Provides send/stop buttons, model selector, settings, and token info. * - * Layout: Workspace - Token Info - ModelSelector - @ Symbol - / Symbol - Settings - Send Button + * Layout: ModelSelector - Token Info | MCP Settings - Prompt Optimization - Send Button + * - Left side: Model configuration (blends with background) + * - Right side: MCP, prompt optimization, and send * - * Uses Jewel components for native IntelliJ IDEA look and feel. + * Note: @ and / triggers are now in the top toolbar (IdeaTopToolbar). */ @Composable fun IdeaBottomToolbar( @@ -31,10 +33,8 @@ fun IdeaBottomToolbar( sendEnabled: Boolean, isExecuting: Boolean = false, onStopClick: () -> Unit = {}, - onAtClick: () -> Unit = {}, - onSlashClick: () -> Unit = {}, onSettingsClick: () -> Unit = {}, - workspacePath: String? = null, + onPromptOptimizationClick: () -> Unit = {}, totalTokens: Int? = null, // Model selector props availableConfigs: List = emptyList(), @@ -46,74 +46,33 @@ fun IdeaBottomToolbar( Row( modifier = modifier .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 6.dp), + .padding(horizontal = 4.dp, vertical = 4.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - // Left side: workspace and token info + // Left side: Model selector and token info Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f, fill = false) ) { - // Workspace indicator - if (!workspacePath.isNullOrEmpty()) { - // Extract project name from path, handling both Unix and Windows separators - val projectName = workspacePath - .replace('\\', '/') // Normalize to Unix separator - .substringAfterLast('/') - .ifEmpty { "Project" } - - Box( - modifier = Modifier - .clip(RoundedCornerShape(4.dp)) - .background(JewelTheme.globalColors.panelBackground.copy(alpha = 0.8f)) - .padding(horizontal = 8.dp, vertical = 4.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - Icon( - imageVector = IdeaComposeIcons.Folder, - contentDescription = null, - tint = JewelTheme.globalColors.text.normal, - modifier = Modifier.size(12.dp) - ) - Text( - text = projectName, - style = JewelTheme.defaultTextStyle.copy(fontSize = 12.sp), - maxLines = 1 - ) - } - } - } + // Model selector (transparent, blends with background) + IdeaModelSelector( + availableConfigs = availableConfigs, + currentConfigName = currentConfigName, + onConfigSelect = onConfigSelect, + onConfigureClick = onConfigureClick + ) - // Token usage indicator + // Token usage indicator (subtle) if (totalTokens != null && totalTokens > 0) { - Box( - modifier = Modifier - .clip(RoundedCornerShape(4.dp)) - .background(AutoDevColors.Blue.c400.copy(alpha = 0.2f)) - .padding(horizontal = 8.dp, vertical = 4.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - Text( - text = "Token", - style = JewelTheme.defaultTextStyle.copy(fontSize = 11.sp) - ) - Text( - text = "$totalTokens", - style = JewelTheme.defaultTextStyle.copy( - fontSize = 11.sp, - fontWeight = FontWeight.Bold - ) - ) - } - } + Text( + text = "${totalTokens}t", + style = JewelTheme.defaultTextStyle.copy( + fontSize = 11.sp, + color = JewelTheme.globalColors.text.normal.copy(alpha = 0.6f) + ) + ) } } @@ -122,49 +81,27 @@ fun IdeaBottomToolbar( horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically ) { - // Model selector - IdeaModelSelector( - availableConfigs = availableConfigs, - currentConfigName = currentConfigName, - onConfigSelect = onConfigSelect, - onConfigureClick = onConfigureClick - ) - - // @ trigger button for agent completion + // MCP Settings button IconButton( - onClick = onAtClick, + onClick = onSettingsClick, modifier = Modifier.size(32.dp) ) { Icon( - imageVector = IdeaComposeIcons.AlternateEmail, - contentDescription = "@ Agent", + imageVector = IdeaComposeIcons.Settings, + contentDescription = "MCP Settings", tint = JewelTheme.globalColors.text.normal, - modifier = Modifier.size(18.dp) - ) - } - - // / trigger button for slash commands - IconButton( - onClick = onSlashClick, - modifier = Modifier.size(32.dp) - ) { - Text( - text = "/", - style = JewelTheme.defaultTextStyle.copy( - fontSize = 16.sp, - fontWeight = FontWeight.Bold - ) + modifier = Modifier.size(16.dp) ) } - // Settings button + // Prompt Optimization button IconButton( - onClick = onSettingsClick, + onClick = onPromptOptimizationClick, modifier = Modifier.size(32.dp) ) { Icon( - imageVector = IdeaComposeIcons.Settings, - contentDescription = "Settings", + imageVector = IdeaComposeIcons.AutoAwesome, + contentDescription = "Prompt Optimization", tint = JewelTheme.globalColors.text.normal, modifier = Modifier.size(16.dp) ) diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaDevInInput.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaDevInInput.kt index 1e69fc6a61..6ee6e1bac2 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaDevInInput.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaDevInInput.kt @@ -1,7 +1,9 @@ package cc.unitmesh.devins.idea.editor +import cc.unitmesh.devti.language.DevInLanguage +import cc.unitmesh.devti.util.InsertUtil +import com.intellij.codeInsight.AutoPopupController import com.intellij.codeInsight.lookup.LookupManagerListener -import com.intellij.lang.Language import com.intellij.openapi.Disposable import com.intellij.openapi.actionSystem.CustomShortcutSet import com.intellij.openapi.actionSystem.KeyboardShortcut @@ -9,7 +11,6 @@ import com.intellij.openapi.command.CommandProcessor import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.EditorFactory import com.intellij.openapi.editor.EditorModificationUtil import com.intellij.openapi.application.ReadAction import com.intellij.openapi.application.runReadAction @@ -21,6 +22,8 @@ import com.intellij.openapi.fileTypes.FileTypes import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.Project import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiFileFactory import com.intellij.testFramework.LightVirtualFile import com.intellij.ui.EditorTextField import com.intellij.util.EventDispatcher @@ -32,17 +35,18 @@ import javax.swing.KeyStroke /** * DevIn language input component for mpp-idea module. - * + * * Features: * - DevIn language support with syntax highlighting and completion * - Enter to submit, Shift/Ctrl/Cmd+Enter for newline * - Integration with IntelliJ's completion system (lookup listener) + * - Auto-completion for @, /, $, : characters * - Placeholder text support - * + * * Based on AutoDevInput from core module but adapted for standalone mpp-idea usage. */ class IdeaDevInInput( - project: Project, + private val project: Project, private val listeners: List = emptyList(), val disposable: Disposable?, private val showAgent: Boolean = true @@ -191,13 +195,18 @@ class IdeaDevInInput( } } - // Create new document using EditorFactory + // Create new document with DevIn language support + val id = UUID.randomUUID() val document = ReadAction.compute { - EditorFactory.getInstance().createDocument("") + val psiFile = PsiFileFactory.getInstance(project) + .createFileFromText("IdeaDevInInput-$id.devin", DevInLanguage, "") + PsiDocumentManager.getInstance(project).getDocument(psiFile) } - initializeDocumentListeners(document) - setDocument(document) + if (document != null) { + initializeDocumentListeners(document) + setDocument(document) + } } private fun initializeDocumentListeners(inputDocument: Document) { @@ -224,11 +233,12 @@ class IdeaDevInInput( /** * Append text at the end of the document. + * Uses InsertUtil for proper text insertion with DevIn language support. */ fun appendText(textToAppend: String) { WriteCommandAction.runWriteCommandAction(project, "Append text", "intentions.write.action", { val document = this.editor?.document ?: return@runWriteCommandAction - document.insertString(document.textLength, textToAppend) + InsertUtil.insertStringAndSaveChange(project, textToAppend, document, document.textLength, false) }) } diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaInputSection.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaInputSection.kt deleted file mode 100644 index 856f154cf3..0000000000 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaInputSection.kt +++ /dev/null @@ -1,164 +0,0 @@ -package cc.unitmesh.devins.idea.editor - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -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.input.key.* -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import kotlinx.coroutines.flow.distinctUntilChanged -import org.jetbrains.jewel.foundation.theme.JewelTheme -import org.jetbrains.jewel.ui.Orientation -import org.jetbrains.jewel.ui.component.* - -/** - * Complete input section for mpp-idea module. - * - * Combines a text input field with a bottom toolbar for actions. - * Uses Jewel components for native IntelliJ IDEA integration. - * - * Features: - * - Multi-line text input with DevIn command support - * - Enter to submit, Shift+Enter for newline - * - Bottom toolbar with send/stop, @ trigger, settings - * - Workspace and token info display - * - * Note: This is a pure Compose implementation. For full DevIn language support - * with completion, use IdeaDevInInput (Swing-based) embedded via ComposePanel. - */ -@Composable -fun IdeaInputSection( - isProcessing: Boolean, - onSend: (String) -> Unit, - onStop: () -> Unit = {}, - onAtClick: () -> Unit = {}, - onSlashClick: () -> Unit = {}, - onSettingsClick: () -> Unit = {}, - workspacePath: String? = null, - totalTokens: Int? = null, - modifier: Modifier = Modifier -) { - val textFieldState = rememberTextFieldState() - var inputText by remember { mutableStateOf("") } - - // Sync text field state to inputText - LaunchedEffect(Unit) { - snapshotFlow { textFieldState.text.toString() } - .distinctUntilChanged() - .collect { inputText = it } - } - - // Extract send logic to avoid duplication - val doSend: () -> Unit = { - if (inputText.isNotBlank()) { - onSend(inputText) - textFieldState.edit { replace(0, length, "") } - } - } - - Column( - modifier = modifier - .fillMaxWidth() - .background(JewelTheme.globalColors.panelBackground) - .border( - width = 1.dp, - color = JewelTheme.globalColors.borders.normal, - shape = RoundedCornerShape(4.dp) - ) - ) { - // Input area - Box( - modifier = Modifier - .fillMaxWidth() - .heightIn(min = 60.dp, max = 200.dp) - .padding(8.dp) - ) { - TextField( - state = textFieldState, - placeholder = { - Text( - text = "Type your message or /help for commands...", - style = JewelTheme.defaultTextStyle.copy( - fontSize = 14.sp, - color = JewelTheme.globalColors.text.info.copy(alpha = 0.6f) - ) - ) - }, - modifier = Modifier - .fillMaxWidth() - .onPreviewKeyEvent { keyEvent -> - // Enter to send (without modifiers) - if (keyEvent.key == Key.Enter && - keyEvent.type == KeyEventType.KeyDown && - !keyEvent.isShiftPressed && - !keyEvent.isCtrlPressed && - !keyEvent.isMetaPressed && - !isProcessing - ) { - doSend() - true - } else { - false - } - }, - enabled = !isProcessing - ) - } - - Divider(Orientation.Horizontal, modifier = Modifier.fillMaxWidth()) - - // Bottom toolbar - IdeaBottomToolbar( - onSendClick = doSend, - sendEnabled = inputText.isNotBlank() && !isProcessing, - isExecuting = isProcessing, - onStopClick = onStop, - onAtClick = { - // Insert @ character and trigger completion - textFieldState.edit { - append("@") - } - onAtClick() - }, - onSlashClick = { - // Insert / character and trigger slash commands - textFieldState.edit { - append("/") - } - onSlashClick() - }, - onSettingsClick = onSettingsClick, - workspacePath = workspacePath, - totalTokens = totalTokens - ) - } -} - -/** - * Preview hints display for available commands. - */ -@Composable -fun InputHints( - modifier: Modifier = Modifier -) { - Row( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 4.dp), - horizontalArrangement = Arrangement.End - ) { - Text( - text = "Enter to send, Shift+Enter for newline", - style = JewelTheme.defaultTextStyle.copy( - fontSize = 11.sp, - color = JewelTheme.globalColors.text.info.copy(alpha = 0.6f) - ) - ) - } -} - diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaMcpConfigDialog.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaMcpConfigDialog.kt new file mode 100644 index 0000000000..345a885664 --- /dev/null +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaMcpConfigDialog.kt @@ -0,0 +1,440 @@ +package cc.unitmesh.devins.idea.editor + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import kotlinx.coroutines.flow.distinctUntilChanged +import cc.unitmesh.agent.config.McpLoadingState +import cc.unitmesh.agent.config.McpLoadingStateCallback +import cc.unitmesh.agent.config.McpServerState +import cc.unitmesh.agent.config.McpToolConfigManager +import cc.unitmesh.agent.config.ToolConfigFile +import cc.unitmesh.agent.config.ToolItem +import cc.unitmesh.agent.mcp.McpServerConfig +import cc.unitmesh.devins.ui.config.ConfigManager +import kotlinx.coroutines.launch +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import org.jetbrains.jewel.ui.component.* + +// JSON serialization helpers +private val json = Json { + prettyPrint = true + ignoreUnknownKeys = true +} + +private fun serializeMcpConfig(servers: Map): String { + return try { + json.encodeToString(servers) + } catch (e: Exception) { + "{}" + } +} + +private fun deserializeMcpConfig(jsonString: String): Result> { + return try { + val servers = json.decodeFromString>(jsonString) + Result.success(servers) + } catch (e: Exception) { + Result.failure(e) + } +} + +/** + * MCP Configuration Dialog for IntelliJ IDEA. + * + * Features: + * - Two tabs: Tools and MCP Servers + * - Auto-save functionality (2 seconds delay) + * - Real-time JSON validation + * - Incremental MCP server loading + * + * Migrated from mpp-ui/ToolConfigDialog.kt to use Jewel UI components. + */ +@Composable +fun IdeaMcpConfigDialog( + onDismiss: () -> Unit +) { + var toolConfig by remember { mutableStateOf(ToolConfigFile.default()) } + var mcpTools by remember { mutableStateOf>>(emptyMap()) } + var mcpLoadingState by remember { mutableStateOf(McpLoadingState()) } + var isLoading by remember { mutableStateOf(true) } + var selectedTab by remember { mutableStateOf(0) } + var mcpConfigJson by remember { mutableStateOf("") } + var mcpConfigError by remember { mutableStateOf(null) } + var mcpLoadError by remember { mutableStateOf(null) } + var isReloading by remember { mutableStateOf(false) } + var hasUnsavedChanges by remember { mutableStateOf(false) } + var autoSaveJob by remember { mutableStateOf(null) } + + val scope = rememberCoroutineScope() + + // Auto-save function + fun scheduleAutoSave() { + hasUnsavedChanges = true + autoSaveJob?.cancel() + autoSaveJob = scope.launch { + kotlinx.coroutines.delay(2000) // Wait 2 seconds before auto-saving + try { + val enabledMcpTools = mcpTools.values + .flatten() + .filter { it.enabled } + .map { it.name } + + val result = deserializeMcpConfig(mcpConfigJson) + if (result.isSuccess) { + val newMcpServers = result.getOrThrow() + val updatedConfig = toolConfig.copy( + enabledMcpTools = enabledMcpTools, + mcpServers = newMcpServers + ) + + ConfigManager.saveToolConfig(updatedConfig) + toolConfig = updatedConfig + hasUnsavedChanges = false + println("✅ Auto-saved tool configuration") + } + } catch (e: Exception) { + println("❌ Auto-save failed: ${e.message}") + } + } + } + + // Load configuration on startup + LaunchedEffect(Unit) { + scope.launch { + try { + toolConfig = ConfigManager.loadToolConfig() + mcpConfigJson = serializeMcpConfig(toolConfig.mcpServers) + + if (toolConfig.mcpServers.isNotEmpty()) { + scope.launch { + // Create callback for incremental loading + val callback = object : McpLoadingStateCallback { + override fun onServerStateChanged(serverName: String, state: McpServerState) { + mcpLoadingState = mcpLoadingState.updateServerState(serverName, state) + + // Update tools when server is loaded + if (state.isLoaded) { + mcpTools = mcpTools + (serverName to state.tools) + } + } + + override fun onLoadingStateChanged(loadingState: McpLoadingState) { + mcpLoadingState = loadingState + } + + override fun onBuiltinToolsLoaded(tools: List) { + mcpLoadingState = mcpLoadingState.copy(builtinToolsLoaded = true) + } + } + + try { + // Use incremental loading + mcpLoadingState = McpToolConfigManager.discoverMcpToolsIncremental( + toolConfig.mcpServers, + toolConfig.enabledMcpTools.toSet(), + callback + ) + mcpLoadError = null + } catch (e: Exception) { + mcpLoadError = "Failed to load MCP tools: ${e.message}" + println("❌ Error loading MCP tools: ${e.message}") + } + } + } + + isLoading = false + } catch (e: Exception) { + println("Error loading tool config: ${e.message}") + mcpLoadError = "Failed to load configuration: ${e.message}" + isLoading = false + } + } + } + + // Cancel auto-save job on dispose + DisposableEffect(Unit) { + onDispose { + autoSaveJob?.cancel() + } + } + + Dialog(onDismissRequest = onDismiss) { + Column( + modifier = Modifier + .width(800.dp) + .height(600.dp) + .padding(16.dp) + ) { + // Header + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text("Tool Configuration") + if (hasUnsavedChanges) { + Text("(Auto-saving...)", color = org.jetbrains.jewel.foundation.theme.JewelTheme.globalColors.text.info) + } + } + IconButton(onClick = onDismiss) { + Text("×") + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + if (isLoading) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text("Loading...") + } + } else { + // Tab Row + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + DefaultButton( + onClick = { selectedTab = 0 }, + enabled = selectedTab != 0 + ) { + Text("Tools") + } + DefaultButton( + onClick = { selectedTab = 1 }, + enabled = selectedTab != 1 + ) { + Text("MCP Servers") + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + // Error message + mcpLoadError?.let { error -> + Text(error, color = org.jetbrains.jewel.foundation.theme.JewelTheme.globalColors.text.error) + Spacer(modifier = Modifier.height(8.dp)) + } + + // Tab content + Box(modifier = Modifier.weight(1f)) { + when (selectedTab) { + 0 -> McpToolsTab( + mcpTools = mcpTools, + mcpLoadingState = mcpLoadingState, + onToolToggle = { toolName, enabled -> + mcpTools = mcpTools.mapValues { (_, tools) -> + tools.map { tool -> + if (tool.name == toolName) tool.copy(enabled = enabled) else tool + } + } + scheduleAutoSave() + } + ) + 1 -> McpServersTab( + mcpConfigJson = mcpConfigJson, + errorMessage = mcpConfigError, + isReloading = isReloading, + onConfigChange = { newJson -> + mcpConfigJson = newJson + val result = deserializeMcpConfig(newJson) + mcpConfigError = if (result.isFailure) { + result.exceptionOrNull()?.message + } else { + null + } + scheduleAutoSave() + }, + onReload = { + scope.launch { + isReloading = true + val result = deserializeMcpConfig(mcpConfigJson) + if (result.isSuccess) { + val newServers = result.getOrThrow() + toolConfig = toolConfig.copy(mcpServers = newServers) + ConfigManager.saveToolConfig(toolConfig) + // Reload MCP tools + try { + val callback = object : McpLoadingStateCallback { + override fun onServerStateChanged(serverName: String, state: McpServerState) { + mcpLoadingState = mcpLoadingState.updateServerState(serverName, state) + if (state.isLoaded) { + mcpTools = mcpTools + (serverName to state.tools) + } + } + override fun onLoadingStateChanged(loadingState: McpLoadingState) { + mcpLoadingState = loadingState + } + override fun onBuiltinToolsLoaded(tools: List) { + mcpLoadingState = mcpLoadingState.copy(builtinToolsLoaded = true) + } + } + mcpLoadingState = McpToolConfigManager.discoverMcpToolsIncremental( + newServers, + toolConfig.enabledMcpTools.toSet(), + callback + ) + mcpLoadError = null + } catch (e: Exception) { + mcpLoadError = "Failed to reload: ${e.message}" + } + } + isReloading = false + } + } + ) + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + // Footer + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + val enabledMcp = mcpTools.values.flatten().count { it.enabled } + val totalMcp = mcpTools.values.flatten().size + Text("MCP Tools: $enabledMcp/$totalMcp enabled") + + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + OutlinedButton(onClick = onDismiss) { + Text("Close") + } + } + } + } + } + } +} + +@Composable +private fun McpToolsTab( + mcpTools: Map>, + mcpLoadingState: McpLoadingState, + onToolToggle: (String, Boolean) -> Unit +) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + mcpTools.forEach { (serverName, tools) -> + item { + Text(serverName, modifier = Modifier.padding(vertical = 4.dp)) + } + items(tools) { tool -> + Row( + modifier = Modifier.fillMaxWidth().padding(start = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text(tool.displayName) + Text(tool.description, color = org.jetbrains.jewel.foundation.theme.JewelTheme.globalColors.text.info) + } + Checkbox( + checked = tool.enabled, + onCheckedChange = { onToolToggle(tool.name, it) } + ) + } + } + } + + val isLoading = mcpLoadingState.loadingServers.isNotEmpty() + + if (mcpTools.isEmpty() && !isLoading) { + item { + Text("No MCP tools configured. Add MCP servers in the 'MCP Servers' tab.") + } + } + + if (isLoading) { + item { + Text("Loading MCP tools...") + } + } + } +} + +@Composable +private fun McpServersTab( + mcpConfigJson: String, + errorMessage: String?, + isReloading: Boolean, + onConfigChange: (String) -> Unit, + onReload: () -> Unit +) { + val textFieldState = rememberTextFieldState(mcpConfigJson) + + // Sync text field state to callback + LaunchedEffect(Unit) { + snapshotFlow { textFieldState.text.toString() } + .distinctUntilChanged() + .collect { newText -> + if (newText != mcpConfigJson) { + onConfigChange(newText) + } + } + } + + // Update text field when external value changes + LaunchedEffect(mcpConfigJson) { + if (textFieldState.text.toString() != mcpConfigJson) { + textFieldState.setTextAndPlaceCursorAtEnd(mcpConfigJson) + } + } + + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text("MCP Server Configuration (JSON)") + + errorMessage?.let { error -> + Text(error, color = org.jetbrains.jewel.foundation.theme.JewelTheme.globalColors.text.error) + } + + // Use BasicTextField for multi-line text input + BasicTextField( + state = textFieldState, + modifier = Modifier.fillMaxWidth().weight(1f), + textStyle = TextStyle( + fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace, + color = org.jetbrains.jewel.foundation.theme.JewelTheme.globalColors.text.normal + ), + cursorBrush = SolidColor(org.jetbrains.jewel.foundation.theme.JewelTheme.globalColors.text.normal) + ) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + DefaultButton( + onClick = onReload, + enabled = !isReloading && errorMessage == null + ) { + Text(if (isReloading) "Reloading..." else "Reload MCP Tools") + } + } + } +} + diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaModelSelector.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaModelSelector.kt index bd6f778d74..5cb2da0250 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaModelSelector.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaModelSelector.kt @@ -2,6 +2,9 @@ package cc.unitmesh.devins.idea.editor import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape @@ -14,6 +17,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupProperties +import androidx.compose.ui.Alignment as ComposeAlignment import cc.unitmesh.devins.idea.toolwindow.IdeaComposeIcons import cc.unitmesh.llm.ModelConfig import cc.unitmesh.llm.NamedModelConfig @@ -29,6 +33,7 @@ import org.jetbrains.jewel.ui.Orientation * Provides a dropdown for selecting LLM models with a configure option. * * Uses Jewel components for native IntelliJ IDEA look and feel. + * Designed to blend seamlessly with the toolbar background. */ @Composable fun IdeaModelSelector( @@ -39,6 +44,8 @@ fun IdeaModelSelector( modifier: Modifier = Modifier ) { var expanded by remember { mutableStateOf(false) } + val interactionSource = remember { MutableInteractionSource() } + val isHovered by interactionSource.collectIsHoveredAsState() val currentConfig = remember(currentConfigName, availableConfigs) { availableConfigs.find { it.name == currentConfigName } @@ -49,32 +56,48 @@ fun IdeaModelSelector( } Box(modifier = modifier) { - // Main selector button - OutlinedButton( - onClick = { expanded = true }, - modifier = Modifier.height(32.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - Text( - text = displayText, - style = JewelTheme.defaultTextStyle.copy(fontSize = 12.sp), - maxLines = 1 - ) - Icon( - imageVector = IdeaComposeIcons.ArrowDropDown, - contentDescription = null, - tint = JewelTheme.globalColors.text.normal, - modifier = Modifier.size(16.dp) + // Transparent selector that blends with background + Row( + modifier = Modifier + .clip(RoundedCornerShape(6.dp)) + .hoverable(interactionSource = interactionSource) + .background( + if (isHovered || expanded) + JewelTheme.globalColors.borders.normal.copy(alpha = 0.3f) + else + androidx.compose.ui.graphics.Color.Transparent ) - } + .clickable { expanded = true } + .padding(horizontal = 8.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + imageVector = IdeaComposeIcons.SmartToy, + contentDescription = null, + tint = JewelTheme.globalColors.text.normal.copy(alpha = 0.7f), + modifier = Modifier.size(14.dp) + ) + Text( + text = displayText, + style = JewelTheme.defaultTextStyle.copy( + fontSize = 12.sp, + color = JewelTheme.globalColors.text.normal + ), + maxLines = 1 + ) + Icon( + imageVector = IdeaComposeIcons.ArrowDropDown, + contentDescription = null, + tint = JewelTheme.globalColors.text.normal.copy(alpha = 0.6f), + modifier = Modifier.size(14.dp) + ) } - // Dropdown popup + // Dropdown popup - positioned above the selector to avoid covering input area if (expanded) { Popup( + alignment = ComposeAlignment.BottomStart, onDismissRequest = { expanded = false }, properties = PopupProperties(focusable = true) ) { diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaPromptOptimizationDialog.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaPromptOptimizationDialog.kt new file mode 100644 index 0000000000..c5f6f77723 --- /dev/null +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaPromptOptimizationDialog.kt @@ -0,0 +1,186 @@ +package cc.unitmesh.devins.idea.editor + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import cc.unitmesh.llm.KoogLLMService +import cc.unitmesh.llm.PromptEnhancer +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.ui.component.* + +/** + * Prompt Optimization Dialog for IntelliJ IDEA. + * + * Features: + * - Display original and enhanced prompts side by side + * - Real-time enhancement using PromptEnhancer + * - Apply or cancel the enhancement + * + * Migrated from mpp-ui/DevInEditorInput.kt prompt enhancement functionality. + */ +@Composable +fun IdeaPromptOptimizationDialog( + originalText: String, + enhancer: PromptEnhancer?, + onApply: (String) -> Unit, + onDismiss: () -> Unit +) { + var enhancedText by remember { mutableStateOf("") } + var isEnhancing by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf(null) } + + val scope = rememberCoroutineScope() + + // Auto-enhance on dialog open + LaunchedEffect(Unit) { + if (enhancer != null && originalText.isNotBlank()) { + isEnhancing = true + errorMessage = null + try { + val enhanced = enhancer.enhance(originalText.trim(), "zh") + if (enhanced.isNotEmpty() && enhanced != originalText.trim()) { + enhancedText = enhanced + } else { + enhancedText = originalText + errorMessage = "No enhancement needed or enhancement failed" + } + } catch (e: Exception) { + errorMessage = "Enhancement failed: ${e.message}" + enhancedText = originalText + } finally { + isEnhancing = false + } + } else { + enhancedText = originalText + if (enhancer == null) { + errorMessage = "Enhancer not available. Please configure LLM settings." + } + } + } + + Dialog(onDismissRequest = onDismiss) { + Column( + modifier = Modifier + .width(700.dp) + .height(500.dp) + .padding(16.dp) + ) { + // Header + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text("Prompt Optimization (Ctrl+P)") + IconButton(onClick = onDismiss) { + Text("×") + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + // Error message + errorMessage?.let { error -> + Text(error, color = JewelTheme.globalColors.text.error) + Spacer(modifier = Modifier.height(8.dp)) + } + + // Content area + Row( + modifier = Modifier.weight(1f).fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Original text + Column(modifier = Modifier.weight(1f)) { + Text("Original") + Spacer(modifier = Modifier.height(4.dp)) + BasicTextField( + value = originalText, + onValueChange = {}, + readOnly = true, + modifier = Modifier.fillMaxSize(), + textStyle = TextStyle( + fontFamily = FontFamily.Monospace, + color = JewelTheme.globalColors.text.normal + ), + cursorBrush = SolidColor(JewelTheme.globalColors.text.normal) + ) + } + + // Enhanced text + Column(modifier = Modifier.weight(1f)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text("Enhanced") + if (isEnhancing) { + Text("(Enhancing...)", color = JewelTheme.globalColors.text.info) + } + } + Spacer(modifier = Modifier.height(4.dp)) + + val enhancedTextState = rememberTextFieldState(enhancedText) + + LaunchedEffect(enhancedText) { + if (enhancedTextState.text.toString() != enhancedText) { + enhancedTextState.setTextAndPlaceCursorAtEnd(enhancedText) + } + } + + LaunchedEffect(Unit) { + snapshotFlow { enhancedTextState.text.toString() } + .distinctUntilChanged() + .collect { newText -> + if (newText != enhancedText) { + enhancedText = newText + } + } + } + + BasicTextField( + state = enhancedTextState, + modifier = Modifier.fillMaxSize(), + textStyle = TextStyle( + fontFamily = FontFamily.Monospace, + color = JewelTheme.globalColors.text.normal + ), + cursorBrush = SolidColor(JewelTheme.globalColors.text.normal) + ) + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + // Footer buttons + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedButton(onClick = onDismiss) { + Text("Cancel") + } + Spacer(modifier = Modifier.width(8.dp)) + DefaultButton( + onClick = { onApply(enhancedText) }, + enabled = !isEnhancing && enhancedText.isNotBlank() + ) { + Text("Apply") + } + } + } + } +} + diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaTopToolbar.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaTopToolbar.kt new file mode 100644 index 0000000000..fecb461afc --- /dev/null +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaTopToolbar.kt @@ -0,0 +1,189 @@ +package cc.unitmesh.devins.idea.editor + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import cc.unitmesh.devins.idea.toolwindow.IdeaComposeIcons +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.ui.component.Icon +import org.jetbrains.jewel.ui.component.Text +import org.jetbrains.jewel.ui.component.Tooltip + +/** + * Top toolbar for the input section. + * Contains @ trigger, file selection, and other context-related actions. + * + * Layout: @ - / - Clipboard - Save - Cursor | Selected Files... | Add + */ +@Composable +fun IdeaTopToolbar( + onAtClick: () -> Unit = {}, + onSlashClick: () -> Unit = {}, + onClipboardClick: () -> Unit = {}, + onSaveClick: () -> Unit = {}, + onCursorClick: () -> Unit = {}, + onAddFileClick: () -> Unit = {}, + selectedFiles: List = emptyList(), + onRemoveFile: (SelectedFileItem) -> Unit = {}, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 4.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.spacedBy(2.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Left side: Action buttons + Row( + horizontalArrangement = Arrangement.spacedBy(2.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // @ trigger button + ToolbarIconButton(onClick = onAtClick, tooltip = "@ Agent/File Reference") { + Icon( + imageVector = IdeaComposeIcons.AlternateEmail, + contentDescription = "@ Agent", + tint = JewelTheme.globalColors.text.normal, + modifier = Modifier.size(16.dp) + ) + } + // / trigger button + ToolbarIconButton(onClick = onSlashClick, tooltip = "/ Commands") { + Text(text = "/", style = JewelTheme.defaultTextStyle.copy(fontSize = 14.sp, fontWeight = FontWeight.Bold)) + } + // Clipboard button + ToolbarIconButton(onClick = onClipboardClick, tooltip = "Paste from Clipboard") { + Icon( + imageVector = IdeaComposeIcons.ContentPaste, + contentDescription = "Clipboard", + modifier = Modifier.size(16.dp), + tint = JewelTheme.globalColors.text.normal + ) + } + // Save button + ToolbarIconButton(onClick = onSaveClick, tooltip = "Save to Workspace") { + Icon( + imageVector = IdeaComposeIcons.Save, + contentDescription = "Save", + modifier = Modifier.size(16.dp), + tint = JewelTheme.globalColors.text.normal + ) + } + // Cursor button + ToolbarIconButton(onClick = onCursorClick, tooltip = "Current Selection") { + Icon( + imageVector = IdeaComposeIcons.TextFields, + contentDescription = "Cursor", + modifier = Modifier.size(16.dp), + tint = JewelTheme.globalColors.text.normal + ) + } + } + + // Separator + if (selectedFiles.isNotEmpty()) { + Box(Modifier.width(1.dp).height(20.dp).background(JewelTheme.globalColors.borders.normal)) + } + + // Selected files as chips + Row( + modifier = Modifier.weight(1f), + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + selectedFiles.forEach { file -> + FileChip(file = file, onRemove = { onRemoveFile(file) }) + } + } + + // Add file button + ToolbarIconButton(onClick = onAddFileClick, tooltip = "Add File") { + Icon( + imageVector = IdeaComposeIcons.Add, + contentDescription = "Add File", + modifier = Modifier.size(16.dp), + tint = JewelTheme.globalColors.text.normal + ) + } + } +} + +@Composable +private fun ToolbarIconButton( + onClick: () -> Unit, + tooltip: String, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + val interactionSource = remember { MutableInteractionSource() } + val isHovered by interactionSource.collectIsHoveredAsState() + + Tooltip(tooltip = { Text(tooltip) }) { + Box( + modifier = modifier + .size(28.dp) + .clip(RoundedCornerShape(4.dp)) + .hoverable(interactionSource = interactionSource) + .background( + if (isHovered) JewelTheme.globalColors.borders.normal.copy(alpha = 0.3f) + else androidx.compose.ui.graphics.Color.Transparent + ) + .clickable(onClick = onClick), + contentAlignment = Alignment.Center + ) { content() } + } +} + +@Composable +private fun FileChip(file: SelectedFileItem, onRemove: () -> Unit, modifier: Modifier = Modifier) { + val interactionSource = remember { MutableInteractionSource() } + val isHovered by interactionSource.collectIsHoveredAsState() + + Row( + modifier = modifier + .clip(RoundedCornerShape(4.dp)) + .hoverable(interactionSource = interactionSource) + .background(JewelTheme.globalColors.panelBackground.copy(alpha = 0.8f)) + .border(1.dp, JewelTheme.globalColors.borders.normal, RoundedCornerShape(4.dp)) + .padding(horizontal = 6.dp, vertical = 2.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + imageVector = file.icon ?: IdeaComposeIcons.InsertDriveFile, + contentDescription = null, + modifier = Modifier.size(14.dp), + tint = JewelTheme.globalColors.text.normal + ) + Text(text = file.name, style = JewelTheme.defaultTextStyle.copy(fontSize = 12.sp), maxLines = 1) + if (isHovered) { + Icon( + imageVector = IdeaComposeIcons.Close, + contentDescription = "Remove", + modifier = Modifier.size(14.dp).clickable(onClick = onRemove), + tint = JewelTheme.globalColors.text.normal.copy(alpha = 0.6f) + ) + } + } +} + +data class SelectedFileItem( + val name: String, + val path: String, + val icon: androidx.compose.ui.graphics.vector.ImageVector? = null, + val virtualFile: com.intellij.openapi.vfs.VirtualFile? = null +) + 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 68f627a5ea..2f1f07e8c4 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 @@ -16,6 +16,7 @@ import cc.unitmesh.devins.idea.editor.IdeaModelConfigDialog import cc.unitmesh.devins.idea.toolwindow.codereview.IdeaCodeReviewContent import cc.unitmesh.devins.idea.toolwindow.codereview.IdeaCodeReviewViewModel import cc.unitmesh.devins.idea.components.header.IdeaAgentTabsHeader +import cc.unitmesh.devins.idea.components.IdeaVerticalResizableSplitPane import cc.unitmesh.devins.idea.toolwindow.knowledge.IdeaKnowledgeContent import cc.unitmesh.devins.idea.toolwindow.knowledge.IdeaKnowledgeViewModel import cc.unitmesh.devins.idea.toolwindow.remote.IdeaRemoteAgentContent @@ -147,32 +148,92 @@ fun IdeaAgentApp( Divider(Orientation.Horizontal, modifier = Modifier.fillMaxWidth().height(1.dp)) - // Content based on agent type - Box( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - ) { - when (currentAgentType) { - AgentType.CODING, AgentType.LOCAL_CHAT -> { - IdeaTimelineContent( - timeline = timeline, - streamingOutput = streamingOutput, - listState = listState, - project = project - ) - } - AgentType.REMOTE -> { - remoteAgentViewModel?.let { vm -> - IdeaRemoteAgentContent( - viewModel = vm, + // Main content area with resizable split pane for chat-based modes + when (currentAgentType) { + AgentType.CODING, AgentType.LOCAL_CHAT -> { + IdeaVerticalResizableSplitPane( + modifier = Modifier.fillMaxWidth().weight(1f), + initialSplitRatio = 0.75f, + minRatio = 0.3f, + maxRatio = 0.9f, + top = { + IdeaTimelineContent( + timeline = timeline, + streamingOutput = streamingOutput, listState = listState, - onProjectIdChange = { remoteProjectId = it }, - onGitUrlChange = { remoteGitUrl = it } + project = project ) - } ?: IdeaEmptyStateMessage("Loading Remote Agent...") - } - AgentType.CODE_REVIEW -> { + }, + bottom = { + IdeaDevInInputArea( + project = project, + parentDisposable = viewModel, + isProcessing = isExecuting, + onSend = { viewModel.sendMessage(it) }, + onAbort = { viewModel.cancelTask() }, + workspacePath = project.basePath, + totalTokens = null, + onSettingsClick = { viewModel.setShowConfigDialog(true) }, + onAtClick = {}, + availableConfigs = availableConfigs, + currentConfigName = currentConfigName, + onConfigSelect = { config -> + viewModel.setActiveConfig(config.name) + }, + onConfigureClick = { viewModel.setShowConfigDialog(true) } + ) + } + ) + } + AgentType.REMOTE -> { + remoteAgentViewModel?.let { remoteVm -> + val remoteIsExecuting by remoteVm.isExecuting.collectAsState() + val remoteIsConnected by remoteVm.isConnected.collectAsState() + + IdeaVerticalResizableSplitPane( + modifier = Modifier.fillMaxWidth().weight(1f), + initialSplitRatio = 0.75f, + minRatio = 0.3f, + maxRatio = 0.9f, + top = { + IdeaRemoteAgentContent( + viewModel = remoteVm, + listState = listState, + onProjectIdChange = { remoteProjectId = it }, + onGitUrlChange = { remoteGitUrl = it } + ) + }, + bottom = { + IdeaDevInInputArea( + project = project, + parentDisposable = viewModel, + isProcessing = remoteIsExecuting, + onSend = { task -> + val effectiveProjectId = getEffectiveProjectId(remoteProjectId, remoteGitUrl) + if (effectiveProjectId.isNotBlank()) { + remoteVm.executeTask(effectiveProjectId, task, remoteGitUrl) + } else { + remoteVm.renderer.renderError("Please provide a project or Git URL") + } + }, + onAbort = { remoteVm.cancelTask() }, + workspacePath = project.basePath, + totalTokens = null, + onSettingsClick = { viewModel.setShowConfigDialog(true) }, + onAtClick = {}, + availableConfigs = availableConfigs, + currentConfigName = currentConfigName, + onConfigSelect = { config -> + viewModel.setActiveConfig(config.name) + }, + onConfigureClick = { viewModel.setShowConfigDialog(true) } + ) + } + ) + } ?: IdeaEmptyStateMessage("Loading Remote Agent...") + } + AgentType.CODE_REVIEW -> { + Box(modifier = Modifier.fillMaxWidth().weight(1f)) { codeReviewViewModel?.let { vm -> IdeaCodeReviewContent( viewModel = vm, @@ -180,7 +241,9 @@ fun IdeaAgentApp( ) } ?: IdeaEmptyStateMessage("Loading Code Review...") } - AgentType.KNOWLEDGE -> { + } + AgentType.KNOWLEDGE -> { + Box(modifier = Modifier.fillMaxWidth().weight(1f)) { knowledgeViewModel?.let { vm -> IdeaKnowledgeContent(viewModel = vm) } ?: IdeaEmptyStateMessage("Loading Knowledge Agent...") @@ -188,64 +251,6 @@ fun IdeaAgentApp( } } - Divider(Orientation.Horizontal, modifier = Modifier.fillMaxWidth().height(1.dp)) - - // Input area (only for chat-based modes) - if (currentAgentType == AgentType.CODING || currentAgentType == AgentType.LOCAL_CHAT) { - IdeaDevInInputArea( - project = project, - parentDisposable = viewModel, - isProcessing = isExecuting, - onSend = { viewModel.sendMessage(it) }, - onAbort = { viewModel.cancelTask() }, - workspacePath = project.basePath, - totalTokens = null, // TODO: integrate token counting from renderer - onSettingsClick = { viewModel.setShowConfigDialog(true) }, - onAtClick = { - // @ click triggers agent completion - placeholder for now - }, - availableConfigs = availableConfigs, - currentConfigName = currentConfigName, - onConfigSelect = { config -> - viewModel.setActiveConfig(config.name) - }, - onConfigureClick = { viewModel.setShowConfigDialog(true) } - ) - } - - // Remote agent input area - if (currentAgentType == AgentType.REMOTE) { - remoteAgentViewModel?.let { remoteVm -> - val remoteIsExecuting by remoteVm.isExecuting.collectAsState() - val remoteIsConnected by remoteVm.isConnected.collectAsState() - - IdeaDevInInputArea( - project = project, - parentDisposable = viewModel, - isProcessing = remoteIsExecuting, - onSend = { task -> - val effectiveProjectId = getEffectiveProjectId(remoteProjectId, remoteGitUrl) - if (effectiveProjectId.isNotBlank()) { - remoteVm.executeTask(effectiveProjectId, task, remoteGitUrl) - } else { - remoteVm.renderer.renderError("Please provide a project or Git URL") - } - }, - onAbort = { remoteVm.cancelTask() }, - workspacePath = project.basePath, - totalTokens = null, - onSettingsClick = { viewModel.setShowConfigDialog(true) }, - onAtClick = {}, - availableConfigs = availableConfigs, - currentConfigName = currentConfigName, - onConfigSelect = { config -> - viewModel.setActiveConfig(config.name) - }, - onConfigureClick = { viewModel.setShowConfigDialog(true) } - ) - } - } - // Tool loading status bar IdeaToolLoadingStatusBar( viewModel = viewModel, @@ -302,13 +307,13 @@ private fun IdeaDevInInputArea( var devInInput by remember { mutableStateOf(null) } Column( - modifier = Modifier.fillMaxWidth().padding(8.dp) + modifier = Modifier.fillMaxSize().padding(8.dp) ) { - // DevIn Editor via SwingPanel + // DevIn Editor via SwingPanel - uses weight(1f) to fill available space SwingPanel( modifier = Modifier .fillMaxWidth() - .height(120.dp), + .weight(1f), factory = { val input = IdeaDevInInput( project = project, @@ -344,11 +349,11 @@ private fun IdeaDevInInputArea( Disposer.register(parentDisposable, input) devInInput = input - // Wrap in a JPanel to handle sizing + // Wrap in a JPanel to handle dynamic sizing JPanel(BorderLayout()).apply { add(input, BorderLayout.CENTER) - preferredSize = Dimension(800, 120) - minimumSize = Dimension(200, 80) + // Don't set fixed preferredSize - let it fill available space + minimumSize = Dimension(200, 60) } }, update = { panel -> @@ -369,16 +374,7 @@ private fun IdeaDevInInputArea( sendEnabled = inputText.isNotBlank() && !isProcessing, isExecuting = isProcessing, onStopClick = onAbort, - onAtClick = { - devInInput?.appendText("@") - onAtClick() - }, - onSlashClick = { - // Insert / at current cursor position to trigger slash commands - devInInput?.appendText("/") - }, onSettingsClick = onSettingsClick, - workspacePath = workspacePath, totalTokens = totalTokens, availableConfigs = availableConfigs, currentConfigName = currentConfigName, diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaComposeIcons.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaComposeIcons.kt index 2720883ad0..5ffb0dbc7f 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaComposeIcons.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaComposeIcons.kt @@ -1255,5 +1255,290 @@ object IdeaComposeIcons { }.build() } + /** + * SmartToy icon (robot/AI model) + */ + val SmartToy: ImageVector by lazy { + ImageVector.Builder( + name = "SmartToy", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f + ).apply { + path( + fill = SolidColor(Color.Black) + ) { + // Robot head + moveTo(20f, 9f) + verticalLineTo(7f) + curveToRelative(0f, -1.1f, -0.9f, -2f, -2f, -2f) + horizontalLineToRelative(-3f) + curveToRelative(0f, -1.66f, -1.34f, -3f, -3f, -3f) + reflectiveCurveTo(9f, 3.34f, 9f, 5f) + horizontalLineTo(6f) + curveToRelative(-1.1f, 0f, -2f, 0.9f, -2f, 2f) + verticalLineToRelative(2f) + curveToRelative(-1.66f, 0f, -3f, 1.34f, -3f, 3f) + reflectiveCurveToRelative(1.34f, 3f, 3f, 3f) + verticalLineToRelative(4f) + curveToRelative(0f, 1.1f, 0.9f, 2f, 2f, 2f) + horizontalLineToRelative(12f) + curveToRelative(1.1f, 0f, 2f, -0.9f, 2f, -2f) + verticalLineToRelative(-4f) + curveToRelative(1.66f, 0f, 3f, -1.34f, 3f, -3f) + reflectiveCurveToRelative(-1.34f, -3f, -3f, -3f) + close() + // Left eye + moveTo(7.5f, 11.5f) + curveToRelative(0f, -0.83f, 0.67f, -1.5f, 1.5f, -1.5f) + reflectiveCurveToRelative(1.5f, 0.67f, 1.5f, 1.5f) + reflectiveCurveTo(9.83f, 13f, 9f, 13f) + reflectiveCurveToRelative(-1.5f, -0.67f, -1.5f, -1.5f) + close() + // Right eye + moveTo(13.5f, 11.5f) + curveToRelative(0f, -0.83f, 0.67f, -1.5f, 1.5f, -1.5f) + reflectiveCurveToRelative(1.5f, 0.67f, 1.5f, 1.5f) + reflectiveCurveTo(15.83f, 13f, 15f, 13f) + reflectiveCurveToRelative(-1.5f, -0.67f, -1.5f, -1.5f) + close() + // Mouth + moveTo(8f, 15f) + horizontalLineToRelative(8f) + verticalLineToRelative(2f) + horizontalLineTo(8f) + close() + } + }.build() + } + + /** + * AutoAwesome icon (sparkles/stars for AI/optimization) + */ + val AutoAwesome: ImageVector by lazy { + ImageVector.Builder( + name = "AutoAwesome", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f + ).apply { + path( + fill = SolidColor(Color.Black) + ) { + // Large star + moveTo(19f, 9f) + lineToRelative(1.25f, -2.75f) + lineTo(23f, 5f) + lineToRelative(-2.75f, -1.25f) + lineTo(19f, 1f) + lineToRelative(-1.25f, 2.75f) + lineTo(15f, 5f) + lineToRelative(2.75f, 1.25f) + close() + // Medium star + moveTo(19f, 15f) + lineToRelative(-1.25f, 2.75f) + lineTo(15f, 19f) + lineToRelative(2.75f, 1.25f) + lineTo(19f, 23f) + lineToRelative(1.25f, -2.75f) + lineTo(23f, 19f) + lineToRelative(-2.75f, -1.25f) + close() + // Large center star + moveTo(11.5f, 9.5f) + lineTo(9f, 4f) + lineTo(6.5f, 9.5f) + lineTo(1f, 12f) + lineToRelative(5.5f, 2.5f) + lineTo(9f, 20f) + lineToRelative(2.5f, -5.5f) + lineTo(17f, 12f) + close() + } + }.build() + } + + /** + * ContentPaste icon (clipboard paste) + */ + val ContentPaste: ImageVector by lazy { + ImageVector.Builder( + name = "ContentPaste", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f + ).apply { + path(fill = SolidColor(Color.Black)) { + moveTo(19f, 2f) + horizontalLineToRelative(-4.18f) + curveTo(14.4f, 0.84f, 13.3f, 0f, 12f, 0f) + curveToRelative(-1.3f, 0f, -2.4f, 0.84f, -2.82f, 2f) + horizontalLineTo(5f) + curveToRelative(-1.1f, 0f, -2f, 0.9f, -2f, 2f) + verticalLineToRelative(16f) + curveToRelative(0f, 1.1f, 0.9f, 2f, 2f, 2f) + horizontalLineToRelative(14f) + curveToRelative(1.1f, 0f, 2f, -0.9f, 2f, -2f) + verticalLineTo(4f) + curveToRelative(0f, -1.1f, -0.9f, -2f, -2f, -2f) + close() + moveTo(12f, 2f) + curveToRelative(0.55f, 0f, 1f, 0.45f, 1f, 1f) + reflectiveCurveToRelative(-0.45f, 1f, -1f, 1f) + reflectiveCurveToRelative(-1f, -0.45f, -1f, -1f) + reflectiveCurveToRelative(0.45f, -1f, 1f, -1f) + close() + moveTo(19f, 20f) + horizontalLineTo(5f) + verticalLineTo(4f) + horizontalLineToRelative(2f) + verticalLineToRelative(3f) + horizontalLineToRelative(10f) + verticalLineTo(4f) + horizontalLineToRelative(2f) + verticalLineToRelative(16f) + close() + } + }.build() + } + + /** + * Save icon (floppy disk) + */ + val Save: ImageVector by lazy { + ImageVector.Builder( + name = "Save", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f + ).apply { + path(fill = SolidColor(Color.Black)) { + moveTo(17f, 3f) + horizontalLineTo(5f) + curveToRelative(-1.11f, 0f, -2f, 0.9f, -2f, 2f) + verticalLineToRelative(14f) + curveToRelative(0f, 1.1f, 0.89f, 2f, 2f, 2f) + horizontalLineToRelative(14f) + curveToRelative(1.1f, 0f, 2f, -0.9f, 2f, -2f) + verticalLineTo(7f) + lineToRelative(-4f, -4f) + close() + moveTo(12f, 19f) + curveToRelative(-1.66f, 0f, -3f, -1.34f, -3f, -3f) + reflectiveCurveToRelative(1.34f, -3f, 3f, -3f) + reflectiveCurveToRelative(3f, 1.34f, 3f, 3f) + reflectiveCurveToRelative(-1.34f, 3f, -3f, 3f) + close() + moveTo(15f, 9f) + horizontalLineTo(5f) + verticalLineTo(5f) + horizontalLineToRelative(10f) + verticalLineToRelative(4f) + close() + } + }.build() + } + + /** + * TextFields icon (cursor/text selection) + */ + val TextFields: ImageVector by lazy { + ImageVector.Builder( + name = "TextFields", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f + ).apply { + path(fill = SolidColor(Color.Black)) { + moveTo(2.5f, 4f) + verticalLineToRelative(3f) + horizontalLineToRelative(5f) + verticalLineToRelative(12f) + horizontalLineToRelative(3f) + verticalLineTo(7f) + horizontalLineToRelative(5f) + verticalLineTo(4f) + horizontalLineTo(2.5f) + close() + moveTo(21.5f, 9f) + horizontalLineToRelative(-9f) + verticalLineToRelative(3f) + horizontalLineToRelative(3f) + verticalLineToRelative(7f) + horizontalLineToRelative(3f) + verticalLineToRelative(-7f) + horizontalLineToRelative(3f) + verticalLineTo(9f) + close() + } + }.build() + } + + /** + * InsertDriveFile icon (file document) + */ + val InsertDriveFile: ImageVector by lazy { + ImageVector.Builder( + name = "InsertDriveFile", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f + ).apply { + path(fill = SolidColor(Color.Black)) { + moveTo(6f, 2f) + curveToRelative(-1.1f, 0f, -1.99f, 0.9f, -1.99f, 2f) + lineTo(4f, 20f) + curveToRelative(0f, 1.1f, 0.89f, 2f, 1.99f, 2f) + horizontalLineTo(18f) + curveToRelative(1.1f, 0f, 2f, -0.9f, 2f, -2f) + verticalLineTo(8f) + lineToRelative(-6f, -6f) + horizontalLineTo(6f) + close() + moveTo(13f, 9f) + verticalLineTo(3.5f) + lineTo(18.5f, 9f) + horizontalLineTo(13f) + close() + } + }.build() + } + + /** + * Close icon (X) + */ + val Close: ImageVector by lazy { + ImageVector.Builder( + name = "Close", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f + ).apply { + path(fill = SolidColor(Color.Black)) { + moveTo(19f, 6.41f) + lineTo(17.59f, 5f) + lineTo(12f, 10.59f) + lineTo(6.41f, 5f) + lineTo(5f, 6.41f) + lineTo(10.59f, 12f) + lineTo(5f, 17.59f) + lineTo(6.41f, 19f) + lineTo(12f, 13.41f) + lineTo(17.59f, 19f) + lineTo(19f, 17.59f) + lineTo(13.41f, 12f) + close() + } + }.build() + } + } diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentClient.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentClient.kt index 8e2149b0c4..e6ba3bc69d 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentClient.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/remote/IdeaRemoteAgentClient.kt @@ -1,6 +1,8 @@ package cc.unitmesh.devins.idea.toolwindow.remote import cc.unitmesh.agent.RemoteAgentEvent +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken import io.ktor.client.* import io.ktor.client.engine.cio.* import io.ktor.client.plugins.sse.* @@ -10,8 +12,6 @@ import io.ktor.http.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.mapNotNull -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json import kotlin.time.Duration.Companion.seconds /** @@ -45,10 +45,7 @@ class IdeaRemoteAgentClient( } } - private val json = Json { - ignoreUnknownKeys = true - isLenient = true - } + private val gson = Gson() /** * Health check to verify server is running @@ -58,7 +55,7 @@ class IdeaRemoteAgentClient( if (!response.status.isSuccess()) { throw RemoteAgentException("Health check failed: ${response.status}") } - return json.decodeFromString(response.bodyAsText()) + return gson.fromJson(response.bodyAsText(), HealthResponse::class.java) } /** @@ -69,7 +66,7 @@ class IdeaRemoteAgentClient( if (!response.status.isSuccess()) { throw RemoteAgentException("Failed to fetch projects: ${response.status}") } - return json.decodeFromString(response.bodyAsText()) + return gson.fromJson(response.bodyAsText(), ProjectListResponse::class.java) } /** @@ -83,7 +80,7 @@ class IdeaRemoteAgentClient( request = { method = HttpMethod.Post contentType(ContentType.Application.Json) - setBody(json.encodeToString(RemoteAgentRequest.serializer(), request)) + setBody(gson.toJson(request)) } ) { // Check HTTP status before processing SSE events @@ -118,7 +115,6 @@ class IdeaRemoteAgentClient( /** * Request/Response Data Classes */ -@Serializable data class RemoteAgentRequest( val projectId: String, val task: String, @@ -129,7 +125,6 @@ data class RemoteAgentRequest( val password: String? = null ) -@Serializable data class LLMConfig( val provider: String, val modelName: String, @@ -137,12 +132,10 @@ data class LLMConfig( val baseUrl: String? = null ) -@Serializable data class HealthResponse( val status: String ) -@Serializable data class ProjectInfo( val id: String, val name: String, @@ -150,7 +143,6 @@ data class ProjectInfo( val description: String ) -@Serializable data class ProjectListResponse( val projects: List ) diff --git a/mpp-idea/src/main/resources/META-INF/plugin.xml b/mpp-idea/src/main/resources/META-INF/plugin.xml index 64855cd9a8..f3c963d3f7 100644 --- a/mpp-idea/src/main/resources/META-INF/plugin.xml +++ b/mpp-idea/src/main/resources/META-INF/plugin.xml @@ -1,22 +1,60 @@ - + cc.unitmesh.devins.idea AutoDev Compose UI UnitMesh + + + + messages.AutoDevIdeaBundle + + + + + + + + + + + + + + + + + + + +