diff --git a/AGENTS.md b/AGENTS.md index ec0054da51..341d469459 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,6 +64,35 @@ cd mpp-idea && ../gradlew buildPlugin - `IdeaAgentViewModelTest` requires IntelliJ Platform Test Framework - `JewelRendererTest` can run standalone with JUnit 5 +**Swing/Compose Z-Index Issues (SwingPanel blocking Compose popups):** + +When using `SwingPanel` to embed Swing components (e.g., `EditorTextField`) in Compose, Swing components render on top of Compose popups, causing z-index issues. + +**Solution 1: For Popup/Dropdown menus** +1. Enable Jewel's custom popup renderer in `IdeaAgentToolWindowFactory`: + ```kotlin + JewelFlags.useCustomPopupRenderer = true + ``` +2. Use Jewel's `PopupMenu` instead of `androidx.compose.ui.window.Popup`: + ```kotlin + PopupMenu( + onDismissRequest = { expanded = false; true }, + horizontalAlignment = Alignment.Start + ) { + selectableItem(selected = ..., onClick = { ... }) { Text("Item") } + } + ``` + +**Solution 2: For Dialogs** +Use IntelliJ's `DialogWrapper` with `org.jetbrains.jewel.bridge.compose` instead of `androidx.compose.ui.window.Dialog`: +```kotlin +class MyDialogWrapper(project: Project?) : DialogWrapper(project) { + override fun createCenterPanel(): JComponent = compose { + // Compose content here + } +} +``` + ## Release 1. modify version in `gradle.properties` diff --git a/gradle.properties b/gradle.properties index e9a09cbcc9..3426d92392 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ pluginRepositoryUrl = https://github.com/unit-mesh/auto-dev pluginVersion = 2.4.6 # MPP Unified Version (mpp-core, mpp-ui, mpp-server) -mppVersion = 0.3.2 +mppVersion = 0.3.3 # Supported IDEs: idea, pycharm baseIDE=idea diff --git a/mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/CodingAgentTemplate.kt b/mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/CodingAgentTemplate.kt index 858aeac88f..d47d6a642b 100644 --- a/mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/CodingAgentTemplate.kt +++ b/mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/CodingAgentTemplate.kt @@ -2,16 +2,17 @@ package cc.unitmesh.agent /** * Template for Coding Agent system prompt - * Similar to sketch.vm in JetBrains plugin + * Inspired by Augment Agent's prompt design */ object CodingAgentTemplate { /** * English version of the coding agent system prompt + * Based on Augment Agent's prompt structure */ - const val EN = """You are AutoDev, an autonomous AI coding agent designed to complete development tasks. + const val EN = """You are AutoDev, an autonomous AI coding agent with access to the developer's codebase through powerful tools and integrations. -## Environment Information +# Environment - OS: ${'$'}{osInfo} - Project Path: ${'$'}{projectPath} - Current Time: ${'$'}{timestamp} @@ -19,188 +20,124 @@ object CodingAgentTemplate { - Build Tool: ${'$'}{buildTool} - Shell: ${'$'}{shell} -## Available Tools -You have access to the following tools through DevIns commands. Each tool uses JSON Schema for parameter validation: +# Available Tools + +You have access to the following tools through DevIns commands: ${'$'}{toolList} ## Tool Usage Format All tools use the DevIns format with JSON parameters: -``` + /tool-name ```json -{"parameter": "value", "optional_param": 123} +{"parameter": "value"} ``` -``` - -Each tool's parameters are validated against its JSON Schema. Refer to the schema for required fields, types, and constraints. - -## Task Execution Guidelines - -1. **Gather Context First**: Before making changes understand the codebase -2. **Plan Your Approach**: Think step-by-step about what needs to be done -3. **Make Incremental Changes**: Make one change at a time and verify it works -4. **Test Your Changes**: Run tests or build commands to verify changes -5. **Handle Errors Gracefully**: When a tool fails, analyze the error and try alternative approaches - -## Smart File Search Guidelines - -When searching for files, use **specific and targeted patterns** to avoid overwhelming context: - -**DO:** -- ✅ Use specific patterns: `src/**/*.kt`, `**/test/**/*.java`, `**/config/*.yml` -- ✅ Target specific directories: `/glob pattern="*.ts" path="src/main"` -- ✅ Use grep with specific patterns to narrow down first -- ✅ For broad exploration, use `/ask-agent` to get a summary instead - -**DON'T:** -- ❌ Avoid `**/*` or overly broad patterns (returns too many files, wastes context) -- ❌ Don't glob the entire codebase without a specific goal - -**Smart Strategy:** -1. If you need to understand the project structure, use grep for specific keywords first -2. Use targeted glob patterns based on what you found -3. For very large result sets (100+ files), the system will automatically invoke a SummaryAgent to provide a concise overview + -## Agent Communication & Collaboration +# Task Completion Strategy -When dealing with complex information or large content, you can **communicate with specialized SubAgents** to get focused analysis: +**IMPORTANT: Focus on completing the task efficiently.** -**Available SubAgents:** -- `analysis-agent`: Analyzes and summarizes any content (logs, file lists, code, data) -- `error-agent`: Analyzes errors and provides recovery suggestions -- `code-agent`: Deep codebase investigation and architectural analysis +1. **Understand the Task**: Read the user's request carefully +2. **Gather Minimum Required Information**: Only collect information directly needed for the task +3. **Execute the Task**: Make the necessary changes or provide the answer +4. **Verify if Needed**: For code changes, compile/test to verify +5. **Provide Summary**: Always end with a clear summary of what was done -**When to Use `/ask-agent`:** -1. **After automatic summarization**: When a tool (like glob) triggers auto-summarization, you can ask follow-up questions - ``` - /ask-agent - ```json - {"agentName": "analysis-agent", "question": "What are the main patterns in the file structure you analyzed?"} - ``` - ``` +**Avoid over-exploration**: Don't spend iterations exploring unrelated code. Stay focused on the task. -2. **For specific insights**: Ask targeted questions about previously analyzed content - ``` - /ask-agent - ```json - {"agentName": "analysis-agent", "question": "Which files are most likely related to authentication?"} - ``` - ``` +# Information-Gathering Strategy -3. **To avoid re-reading large content**: If you need different perspectives on the same data - ``` - /ask-agent - ```json - {"agentName": "analysis-agent", "question": "Can you identify the main dependencies in the files you saw?"} - ``` - ``` +Use the appropriate tool based on what you need: -**Example Workflow:** -1. `/glob pattern="**/*.kt"` → Auto-triggers AnalysisAgent (returns summary) -2. Review the summary, then ask: `/ask-agent` to get specific insights -3. Based on insights, use targeted `/read-file` or `/grep` commands +## `/grep` - Find text patterns +- Find specific text, symbols, or references in files +- Example: Search for function usages, error messages, or patterns -This approach keeps your context efficient while getting deep insights from specialized agents! +## `/glob` - Find files +- Find files matching a pattern (e.g., `**/BlogController.java`) +- Use **specific patterns**, avoid overly broad ones like `**/*` -## Task Progress Communication +## `/read-file` - Read file content +- Read a specific file's content before editing +- **ALWAYS read before edit** -For complex multi-step tasks (5+ steps), use `/task-boundary` to help users understand your progress: +# Making Edits -**When to use:** -- At the start of a complex task: Set status to PLANNING and describe what you're about to do -- When switching major phases: Update to WORKING when you start implementation -- At completion: Mark as COMPLETED with a summary of what was done -- If blocked: Mark as BLOCKED and explain why +## Before Editing +- **ALWAYS** read the file first using `/read-file` +- Confirm the exact location and context of changes -**Example for a complex task:** - -/task-boundary -```json -{"taskName": "Implement User Authentication System", "status": "PLANNING", "summary": "Analyzing requirements and existing code structure"} -``` - - -Then after several implementation steps: - -/task-boundary -```json -{"taskName": "Implement User Authentication System", "status": "WORKING", "summary": "Creating User entity, JWT service, and authentication endpoints"} -``` - +## Edit Guidelines +- Use `/edit-file` for modifying existing files +- Use `/write-file` only for creating new files +- Add all necessary imports +- After editing, verify with `/shell` to compile (e.g., `./gradlew compileJava -q`) -**Keep it concise** - one update per major phase is enough. Focus on high-level progress, not individual tool calls. +## After Editing +- Verify the change was applied by reading the file or compiling +- If the task requires testing, run relevant tests -## Error Handling Guidelines +# Following Instructions -When a tool execution fails: +- Do what the user asks; nothing more, nothing less +- If the task is analysis/reading, provide a **clear summary** at the end +- If the task is code modification, verify the change works -1. **Read the Error Message Carefully**: Look for specific error patterns, file paths, and error codes -2. **Analyze the Context**: Consider what you were trying to do and what might have gone wrong -3. **Use Error Recovery**: The system will automatically provide error analysis and recovery suggestions -4. **Try Alternative Approaches**: If one method fails, consider different tools or approaches -5. **Check Prerequisites**: Ensure required files, dependencies, or permissions exist -6. **Verify Paths and Parameters**: Double-check file paths, parameter values, and syntax +# Error Handling -Common error scenarios and solutions: -- **File not found**: Use /glob to verify the file exists and check the correct path -- **Permission denied**: Check file permissions or try alternative locations -- **Build failures**: Read build logs carefully, check dependencies and configuration files -- **Syntax errors**: Review recent changes and validate code syntax -- **Tool not available**: Verify the tool is installed or use alternative tools +When a tool fails: +1. Read the error message carefully +2. Try an alternative approach (different path, different tool) +3. If stuck after 2-3 attempts, summarize the issue -## IMPORTANT: One Tool Per Response +# IMPORTANT: One Tool Per Response -**You MUST execute ONLY ONE tool per response.** Do not include multiple tool calls in a single response. +**Execute ONLY ONE tool per response.** - ✅ CORRECT: One block with ONE tool call -- ❌ WRONG: Multiple blocks or multiple tools in one block +- ❌ WRONG: Multiple blocks -After each tool execution, you will see the result and can decide the next step. +# Response Format -## Response Format - -For each step, respond with: -1. Your reasoning about what to do next (explain your thinking) -2. **EXACTLY ONE** DevIns command (wrapped in tags) -3. What you expect to happen +For each step: +1. Brief reasoning (1-2 sentences) +2. **ONE** DevIns command in tags Example: -I need to check the existing implementation first to understand the current code structure. +I need to read the controller file before making changes. /read-file ```json -{"path": "src/main.ts"} +{"path": "src/main/java/com/example/Controller.java"} ``` -I expect to see the main entry point of the application. -## Making Code Changes +# Task Completion + +When the task is complete, provide a clear summary in your response (no tool call needed): +- For **analysis tasks**: List your findings in a structured format +- For **code changes**: Confirm what was changed and that it was verified -When modifying code: -- **DO NOT output code to the user unless explicitly requested**. Use code editing tools instead. -- Before editing, **read the file or section you want to modify** (unless it's a simple append or new file). -- Add all necessary import statements, dependencies, and endpoints required to run the code. -- If creating a codebase from scratch, provide a dependency management file (e.g., `requirements.txt`) with package versions and a helpful README. -- If building a web app from scratch, design a **modern, beautiful UI with best UX practices**. -- **NEVER generate extremely long hashes or non-textual code (like binary)**. These are unhelpful and expensive. -- When refactoring code, create the new code first, then update the old references. +If you have completed the task, simply respond with your summary without any block. #if (${'$'}{agentRules}) -## Project-Specific Rules +# Project-Specific Rules ${'$'}{agentRules} #end -Remember: You are autonomous. Keep working until the task is complete or you encounter an error you cannot resolve. +Remember: Stay focused, be efficient, and complete the task. """ /** * Chinese version of the coding agent system prompt + * Based on Augment Agent's prompt structure */ - const val ZH = """You are AutoDev, 一个由 Unit Mesh 设计的开源自主 AI 编程代理。 + const val ZH = """你是 AutoDev,一个自主 AI 编程代理,可以通过强大的工具和集成访问开发者的代码库。 -## 环境信息 +# 环境 - OS: ${'$'}{osInfo} - 项目路径: ${'$'}{projectPath} - 当前时间: ${'$'}{timestamp} @@ -208,148 +145,114 @@ Remember: You are autonomous. Keep working until the task is complete or you enc - 构建工具: ${'$'}{buildTool} - Shell: ${'$'}{shell} -## 项目结构 -${'$'}{projectStructure} +# 可用工具 -## 可用工具 你可以通过 DevIns 命令访问以下工具: ${'$'}{toolList} -## 任务执行指南 - -1. **先获取上下文**: 在进行更改之前,先来了解代码库 -2. **规划你的方法**: 逐步思考需要做什么 -3. **增量更改**: 一次做一个更改并验证其有效性 -4. **测试更改**: 运行测试或构建命令来验证更改 +## 工具使用格式 -## 智能文件搜索指南 - -搜索文件时,使用**具体且有针对性的模式**以避免上下文超载: +所有工具都使用 DevIns 格式和 JSON 参数: + +/tool-name +```json +{"parameter": "value"} +``` + -**应该做:** -- ✅ 使用具体的模式:`src/**/*.kt`、`**/test/**/*.java`、`**/config/*.yml` -- ✅ 针对特定目录:`/glob pattern="*.ts" path="src/main"` -- ✅ 先使用 grep 配合具体模式来缩小范围 -- ✅ 对于广泛探索,使用 `/ask-agent` 获取摘要 +# 任务完成策略 -**不应该做:** -- ❌ 避免 `**/*` 或过于宽泛的模式(返回太多文件,浪费上下文) -- ❌ 不要在没有明确目标的情况下 glob 整个代码库 +**重要:专注于高效完成任务。** -**智能策略:** -1. 如果需要了解项目结构,先使用 grep 搜索特定关键词 -2. 根据发现的内容使用有针对性的 glob 模式 -3. 对于非常大的结果集(100+ 文件),系统会自动调用 SummaryAgent 提供简洁概述 +1. **理解任务**:仔细阅读用户的请求 +2. **收集最少必要信息**:只收集任务直接需要的信息 +3. **执行任务**:进行必要的更改或提供答案 +4. **必要时验证**:对于代码更改,编译/测试以验证 +5. **提供总结**:始终以清晰的总结结束 -## Agent 通信与协作 +**避免过度探索**:不要花费迭代次数探索无关代码。保持专注于任务。 -处理复杂信息或大量内容时,你可以**与专业的 SubAgent 通信**来获取专注的分析: +# 信息收集策略 -**可用的 SubAgent:** -- `analysis-agent`: 分析和总结任何内容(日志、文件列表、代码、数据) -- `error-agent`: 分析错误并提供恢复建议 -- `code-agent`: 深度代码库调查和架构分析 +根据需要使用适当的工具: -**何时使用 `/ask-agent`:** -1. **自动总结之后**: 当工具(如 glob)触发自动总结后,你可以询问后续问题 - ``` - /ask-agent - ```json - {"agentName": "analysis-agent", "question": "你分析的文件结构中有哪些主要模式?"} - ``` - ``` +## `/grep` - 查找文本模式 +- 在文件中查找特定文本、符号或引用 +- 示例:搜索函数用法、错误消息或模式 -2. **获取特定见解**: 就之前分析的内容提出针对性问题 - ``` - /ask-agent - ```json - {"agentName": "analysis-agent", "question": "哪些文件最可能与身份验证相关?"} - ``` - ``` +## `/glob` - 查找文件 +- 查找匹配模式的文件(如 `**/BlogController.java`) +- 使用**具体的模式**,避免过于宽泛的如 `**/*` -3. **避免重复读取大内容**: 需要从不同角度看待相同数据时 - ``` - /ask-agent - ```json - {"agentName": "analysis-agent", "question": "你能识别出文件中的主要依赖关系吗?"} - ``` - ``` +## `/read-file` - 读取文件内容 +- 在编辑前读取特定文件的内容 +- **编辑前必须先读取** -**示例工作流:** -1. `/glob pattern="**/*.kt"` → 自动触发 AnalysisAgent(返回摘要) -2. 查看摘要,然后询问:`/ask-agent` 获取特定见解 -3. 基于见解,使用有针对性的 `/read-file` 或 `/grep` 命令 +# 进行编辑 -这种方法既保持上下文高效,又能从专业 Agent 获得深度见解! +## 编辑前 +- **始终**先使用 `/read-file` 读取文件 +- 确认更改的确切位置和上下文 -## 任务进度沟通 +## 编辑指南 +- 使用 `/edit-file` 修改现有文件 +- 仅使用 `/write-file` 创建新文件 +- 添加所有必要的导入 +- 编辑后,使用 `/shell` 验证编译(如 `./gradlew compileJava -q`) -对于复杂的多步骤任务(5+ 步骤),使用 `/task-boundary` 帮助用户了解你的进度: +## 编辑后 +- 通过读取文件或编译来验证更改已应用 +- 如果任务需要测试,运行相关测试 -**何时使用:** -- 复杂任务开始时:将状态设置为 PLANNING 并描述你要做什么 -- 切换主要阶段时:开始实施时更新为 WORKING -- 完成时:标记为 COMPLETED 并总结完成的内容 -- 如果被阻塞:标记为 BLOCKED 并解释原因 +# 遵循指令 -**复杂任务示例:** - -/task-boundary -```json -{"taskName": "实现用户认证系统", "status": "PLANNING", "summary": "分析需求和现有代码结构"} -``` - +- 做用户要求的事情;不多不少 +- 如果任务是分析/阅读,在最后提供**清晰的总结** +- 如果任务是代码修改,验证更改有效 -然后在几个实施步骤后: - -/task-boundary -```json -{"taskName": "实现用户认证系统", "status": "WORKING", "summary": "创建 User 实体、JWT 服务和认证端点"} -``` - +# 错误处理 -**保持简洁** - 每个主要阶段更新一次就够了。关注高层进度,而不是单个工具调用。 +当工具失败时: +1. 仔细阅读错误消息 +2. 尝试替代方法(不同路径、不同工具) +3. 如果尝试 2-3 次后仍卡住,总结问题 -## 重要:每次响应只执行一个工具 +# 重要:每次响应只执行一个工具 -**你必须每次响应只执行一个工具。** 不要在单个响应中包含多个工具调用。 +**每次响应只执行一个工具。** - ✅ 正确:一个 块包含一个工具调用 -- ❌ 错误:多个 块或一个块中有多个工具 +- ❌ 错误:多个 块 -每次工具执行后,你会看到结果,然后可以决定下一步。 +# 响应格式 -## 响应格式 - -对于每一步,请回复: -1. 你对下一步该做什么的推理(解释你的思考) -2. **恰好一个** DevIns 命令(包装在 标签中) -3. 你期望发生什么 +每一步: +1. 简短推理(1-2 句) +2. **一个** DevIns 命令在 标签中 示例: -我需要先检查现有实现以了解当前的代码结构。 +我需要在修改前先读取控制器文件。 -/read-file path="src/main.ts" +/read-file +```json +{"path": "src/main/java/com/example/Controller.java"} +``` -我期望看到应用程序的主入口点。 -## 进行代码更改 +# 任务完成 + +当任务完成时,在响应中直接提供清晰的总结(无需工具调用): +- 对于**分析任务**:以结构化格式列出你的发现 +- 对于**代码更改**:确认更改了什么以及已验证 -在修改代码时: -- **除非用户明确请求,否则不要向用户输出代码**。应使用代码编辑工具。 -- 在编辑之前,**读取你要修改的文件或部分**(除非是简单的追加或新文件)。 -- 添加运行代码所需的所有必要导入语句、依赖项和端点。 -- 如果从头创建代码库,请提供依赖管理文件(例如 `requirements.txt`),包含包版本和有用的 README。 -- 如果从头构建 Web 应用,请设计**现代、美观且符合最佳用户体验实践的界面**。 -- **绝不要生成极长的哈希值或非文本代码(如二进制)**。这些无用且成本高昂。 -- 重构代码时,先生成新代码,然后更新旧引用。 +如果你已完成任务,直接回复你的总结,不要包含任何 块。 #if (${'$'}{agentRules}) -## 项目特定规则 +# 项目特定规则 ${'$'}{agentRules} #end -记住:你是自主的。持续工作直到任务完成或遇到无法解决的错误。 +记住:保持专注,高效完成任务。 """ } diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaModelConfigDialog.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaModelConfigDialog.kt index d6a2fa535d..751edcff5d 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaModelConfigDialog.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaModelConfigDialog.kt @@ -19,8 +19,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties -import androidx.compose.ui.window.Popup -import androidx.compose.ui.window.PopupProperties import cc.unitmesh.devins.idea.toolwindow.IdeaComposeIcons import cc.unitmesh.llm.LLMProviderType import cc.unitmesh.llm.ModelConfig @@ -34,6 +32,8 @@ import org.jetbrains.jewel.ui.component.TextField /** * Dialog for configuring LLM model settings for IntelliJ IDEA plugin. * Uses Jewel components for native IntelliJ look and feel. + * + * @deprecated Use IdeaModelConfigDialogWrapper.show() instead for proper z-index handling with SwingPanel. */ @Composable fun IdeaModelConfigDialog( @@ -41,6 +41,30 @@ fun IdeaModelConfigDialog( currentConfigName: String? = null, onDismiss: () -> Unit, onSave: (configName: String, config: ModelConfig) -> Unit +) { + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + IdeaModelConfigDialogContent( + currentConfig = currentConfig, + currentConfigName = currentConfigName, + onDismiss = onDismiss, + onSave = onSave + ) + } +} + +/** + * Content for the model configuration dialog. + * This is extracted to be used both in Compose Dialog and DialogWrapper. + */ +@Composable +fun IdeaModelConfigDialogContent( + currentConfig: ModelConfig, + currentConfigName: String? = null, + onDismiss: () -> Unit, + onSave: (configName: String, config: ModelConfig) -> Unit ) { // Use TextFieldState for Jewel TextField val configNameState = rememberTextFieldState(currentConfigName ?: "") @@ -55,221 +79,230 @@ fun IdeaModelConfigDialog( var modelExpanded by remember { mutableStateOf(false) } var showAdvanced by remember { mutableStateOf(false) } - Dialog( - onDismissRequest = onDismiss, - properties = DialogProperties(usePlatformDefaultWidth = false) + Box( + modifier = Modifier + .width(500.dp) + .heightIn(max = 600.dp) + .clip(RoundedCornerShape(12.dp)) + .background(JewelTheme.globalColors.panelBackground) + .onKeyEvent { event -> + if (event.key == Key.Escape) { + onDismiss() + true + } else false + } ) { - Box( + Column( modifier = Modifier - .width(500.dp) - .heightIn(max = 600.dp) - .clip(RoundedCornerShape(12.dp)) - .background(JewelTheme.globalColors.panelBackground) - .onKeyEvent { event -> - if (event.key == Key.Escape) { - onDismiss() - true - } else false - } + .padding(24.dp) + .verticalScroll(rememberScrollState()) ) { - Column( - modifier = Modifier - .padding(24.dp) - .verticalScroll(rememberScrollState()) - ) { - // Title - Text( - text = "Model Configuration", - style = JewelTheme.defaultTextStyle.copy(fontSize = 18.sp) + // Title + Text( + text = "Model Configuration", + style = JewelTheme.defaultTextStyle.copy(fontSize = 18.sp) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Config Name + IdeaConfigFormField(label = "Config Name") { + TextField( + state = configNameState, + placeholder = { Text("e.g., my-gpt4") }, + modifier = Modifier.fillMaxWidth() ) + } - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(12.dp)) - // Config Name - IdeaConfigFormField(label = "Config Name") { - TextField( - state = configNameState, - placeholder = { Text("e.g., my-gpt4") }, - modifier = Modifier.fillMaxWidth() - ) - } + // Provider Selector + IdeaConfigFormField(label = "Provider") { + IdeaProviderSelector( + provider = provider, + expanded = providerExpanded, + onExpandedChange = { providerExpanded = it }, + onProviderSelect = { selectedProvider -> + provider = selectedProvider + val defaultModels = ModelRegistry.getAvailableModels(selectedProvider) + if (defaultModels.isNotEmpty()) { + modelNameState.edit { replace(0, length, defaultModels[0]) } + } + if (selectedProvider == LLMProviderType.OLLAMA && baseUrlState.text.isEmpty()) { + baseUrlState.edit { replace(0, length, "http://localhost:11434") } + } + providerExpanded = false + } + ) + } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(12.dp)) - // Provider Selector - IdeaConfigFormField(label = "Provider") { - IdeaProviderSelector( - provider = provider, - expanded = providerExpanded, - onExpandedChange = { providerExpanded = it }, - onProviderSelect = { selectedProvider -> - provider = selectedProvider - val defaultModels = ModelRegistry.getAvailableModels(selectedProvider) - if (defaultModels.isNotEmpty()) { - modelNameState.edit { replace(0, length, defaultModels[0]) } - } - if (selectedProvider == LLMProviderType.OLLAMA && baseUrlState.text.isEmpty()) { - baseUrlState.edit { replace(0, length, "http://localhost:11434") } - } - providerExpanded = false + // Model Name + val availableModels = remember(provider) { ModelRegistry.getAvailableModels(provider) } + IdeaConfigFormField(label = "Model") { + if (availableModels.isNotEmpty()) { + IdeaModelNameSelector( + modelNameState = modelNameState, + availableModels = availableModels, + expanded = modelExpanded, + onExpandedChange = { modelExpanded = it }, + onModelSelect = { selectedModel -> + modelNameState.edit { replace(0, length, selectedModel) } + modelExpanded = false } ) + } else { + TextField( + state = modelNameState, + placeholder = { Text("Enter model name") }, + modifier = Modifier.fillMaxWidth() + ) } + } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(12.dp)) - // Model Name - val availableModels = remember(provider) { ModelRegistry.getAvailableModels(provider) } - IdeaConfigFormField(label = "Model") { - if (availableModels.isNotEmpty()) { - IdeaModelNameSelector( - modelNameState = modelNameState, - availableModels = availableModels, - expanded = modelExpanded, - onExpandedChange = { modelExpanded = it }, - onModelSelect = { selectedModel -> - modelNameState.edit { replace(0, length, selectedModel) } - modelExpanded = false - } - ) - } else { - TextField( - state = modelNameState, - placeholder = { Text("Enter model name") }, - modifier = Modifier.fillMaxWidth() + // API Key + IdeaConfigFormField(label = "API Key") { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + TextField( + state = apiKeyState, + placeholder = { Text("Enter API key") }, + modifier = Modifier.weight(1f) + ) + IconButton( + onClick = { showApiKey = !showApiKey }, + modifier = Modifier.size(32.dp) + ) { + Icon( + imageVector = if (showApiKey) IdeaComposeIcons.VisibilityOff else IdeaComposeIcons.Visibility, + contentDescription = if (showApiKey) "Hide" else "Show", + tint = JewelTheme.globalColors.text.normal, + modifier = Modifier.size(18.dp) ) } } + } + // Base URL (for certain providers) + val needsBaseUrl = provider in listOf( + LLMProviderType.OLLAMA, LLMProviderType.GLM, LLMProviderType.QWEN, + LLMProviderType.KIMI, LLMProviderType.CUSTOM_OPENAI_BASE + ) + if (needsBaseUrl) { Spacer(modifier = Modifier.height(12.dp)) - - // API Key - IdeaConfigFormField(label = "API Key") { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - TextField( - state = apiKeyState, - placeholder = { Text("Enter API key") }, - modifier = Modifier.weight(1f) - ) - IconButton( - onClick = { showApiKey = !showApiKey }, - modifier = Modifier.size(32.dp) - ) { - Icon( - imageVector = if (showApiKey) IdeaComposeIcons.VisibilityOff else IdeaComposeIcons.Visibility, - contentDescription = if (showApiKey) "Hide" else "Show", - tint = JewelTheme.globalColors.text.normal, - modifier = Modifier.size(18.dp) - ) - } - } + IdeaConfigFormField(label = "Base URL") { + TextField( + state = baseUrlState, + placeholder = { Text("e.g., http://localhost:11434") }, + modifier = Modifier.fillMaxWidth() + ) } + } + + Spacer(modifier = Modifier.height(16.dp)) - // Base URL (for certain providers) - val needsBaseUrl = provider in listOf( - LLMProviderType.OLLAMA, LLMProviderType.GLM, LLMProviderType.QWEN, - LLMProviderType.KIMI, LLMProviderType.CUSTOM_OPENAI_BASE + // Advanced Settings Toggle + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { showAdvanced = !showAdvanced } + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = if (showAdvanced) IdeaComposeIcons.ExpandLess else IdeaComposeIcons.ExpandMore, + contentDescription = null, + tint = JewelTheme.globalColors.text.normal, + modifier = Modifier.size(20.dp) ) - if (needsBaseUrl) { - Spacer(modifier = Modifier.height(12.dp)) - IdeaConfigFormField(label = "Base URL") { - TextField( - state = baseUrlState, - placeholder = { Text("e.g., http://localhost:11434") }, - modifier = Modifier.fillMaxWidth() - ) - } - } + Text( + text = "Advanced Settings", + style = JewelTheme.defaultTextStyle.copy(fontSize = 14.sp) + ) + } - Spacer(modifier = Modifier.height(16.dp)) + if (showAdvanced) { + Spacer(modifier = Modifier.height(8.dp)) - // Advanced Settings Toggle - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { showAdvanced = !showAdvanced } - .padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - imageVector = if (showAdvanced) IdeaComposeIcons.ExpandLess else IdeaComposeIcons.ExpandMore, - contentDescription = null, - tint = JewelTheme.globalColors.text.normal, - modifier = Modifier.size(20.dp) - ) - Text( - text = "Advanced Settings", - style = JewelTheme.defaultTextStyle.copy(fontSize = 14.sp) + // Temperature + IdeaConfigFormField(label = "Temperature") { + TextField( + state = temperatureState, + placeholder = { Text("0.0 - 2.0") }, + modifier = Modifier.fillMaxWidth() ) } - if (showAdvanced) { - Spacer(modifier = Modifier.height(8.dp)) - - // Temperature - IdeaConfigFormField(label = "Temperature") { - TextField( - state = temperatureState, - placeholder = { Text("0.0 - 2.0") }, - modifier = Modifier.fillMaxWidth() - ) - } - - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(12.dp)) - // Max Tokens - IdeaConfigFormField(label = "Max Tokens") { - TextField( - state = maxTokensState, - placeholder = { Text("e.g., 128000") }, - modifier = Modifier.fillMaxWidth() - ) - } + // Max Tokens + IdeaConfigFormField(label = "Max Tokens") { + TextField( + state = maxTokensState, + placeholder = { Text("e.g., 128000") }, + modifier = Modifier.fillMaxWidth() + ) } + } - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(24.dp)) - // Action Buttons - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically + // Action Buttons + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedButton(onClick = onDismiss) { + Text("Cancel") + } + Spacer(modifier = Modifier.width(12.dp)) + DefaultButton( + onClick = { + val modelName = modelNameState.text.toString() + val apiKey = apiKeyState.text.toString() + val config = ModelConfig( + provider = provider, + modelName = modelName, + apiKey = apiKey, + temperature = temperatureState.text.toString().toDoubleOrNull() ?: 0.0, + maxTokens = maxTokensState.text.toString().toIntOrNull() ?: 128000, + baseUrl = baseUrlState.text.toString() + ) + val configName = configNameState.text.toString() + val name = configName.ifEmpty { "${provider.name.lowercase()}-${modelName}" } + onSave(name, config) + }, + enabled = modelNameState.text.isNotEmpty() && apiKeyState.text.isNotEmpty() ) { - OutlinedButton(onClick = onDismiss) { - Text("Cancel") - } - Spacer(modifier = Modifier.width(12.dp)) - DefaultButton( - onClick = { - val modelName = modelNameState.text.toString() - val apiKey = apiKeyState.text.toString() - val config = ModelConfig( - provider = provider, - modelName = modelName, - apiKey = apiKey, - temperature = temperatureState.text.toString().toDoubleOrNull() ?: 0.0, - maxTokens = maxTokensState.text.toString().toIntOrNull() ?: 128000, - baseUrl = baseUrlState.text.toString() - ) - val configName = configNameState.text.toString() - val name = configName.ifEmpty { "${provider.name.lowercase()}-${modelName}" } - onSave(name, config) - }, - enabled = modelNameState.text.isNotEmpty() && apiKeyState.text.isNotEmpty() - ) { - Text("Save") - } + Text("Save") } } } } } +/** + * Deprecated: Use IdeaModelConfigDialogWrapper.show() instead. + */ +@Deprecated("Use IdeaModelConfigDialogWrapper.show() for proper z-index handling") +@Composable +fun IdeaModelConfigDialogLegacy( + currentConfig: ModelConfig, + currentConfigName: String? = null, + onDismiss: () -> Unit, + onSave: (configName: String, config: ModelConfig) -> Unit +) { + IdeaModelConfigDialog(currentConfig, currentConfigName, onDismiss, onSave) +} + /** * Form field wrapper with label */ @@ -289,7 +322,7 @@ private fun IdeaConfigFormField( } /** - * Provider selector dropdown + * Provider selector dropdown using Jewel's PopupMenu for proper z-index handling */ @Composable private fun IdeaProviderSelector( @@ -319,43 +352,23 @@ private fun IdeaProviderSelector( } if (expanded) { - Popup( - onDismissRequest = { onExpandedChange(false) }, - properties = PopupProperties(focusable = true) + PopupMenu( + onDismissRequest = { + onExpandedChange(false) + true + }, + horizontalAlignment = Alignment.Start, + modifier = Modifier.widthIn(min = 200.dp, max = 300.dp) ) { - Box( - modifier = Modifier - .widthIn(min = 200.dp, max = 300.dp) - .heightIn(max = 300.dp) - .clip(RoundedCornerShape(8.dp)) - .background(JewelTheme.globalColors.panelBackground) - .padding(4.dp) - ) { - Column(modifier = Modifier.verticalScroll(rememberScrollState())) { - LLMProviderType.entries.forEach { providerType -> - Row( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(4.dp)) - .clickable { onProviderSelect(providerType) } - .padding(horizontal = 12.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = providerType.name, - style = JewelTheme.defaultTextStyle.copy(fontSize = 13.sp), - modifier = Modifier.weight(1f) - ) - if (providerType == provider) { - Icon( - imageVector = IdeaComposeIcons.Check, - contentDescription = "Selected", - tint = JewelTheme.globalColors.text.normal, - modifier = Modifier.size(16.dp) - ) - } - } - } + LLMProviderType.entries.forEach { providerType -> + selectableItem( + selected = providerType == provider, + onClick = { onProviderSelect(providerType) } + ) { + Text( + text = providerType.name, + style = JewelTheme.defaultTextStyle.copy(fontSize = 13.sp) + ) } } } @@ -364,7 +377,7 @@ private fun IdeaProviderSelector( } /** - * Model name selector with dropdown for known models + * Model name selector with dropdown for known models using Jewel's PopupMenu */ @Composable private fun IdeaModelNameSelector( @@ -401,47 +414,26 @@ private fun IdeaModelNameSelector( } if (expanded) { - Popup( - onDismissRequest = { onExpandedChange(false) }, - properties = PopupProperties(focusable = true) + PopupMenu( + onDismissRequest = { + onExpandedChange(false) + true + }, + horizontalAlignment = Alignment.Start, + modifier = Modifier.widthIn(min = 200.dp, max = 400.dp) ) { - Box( - modifier = Modifier - .widthIn(min = 200.dp, max = 400.dp) - .heightIn(max = 250.dp) - .clip(RoundedCornerShape(8.dp)) - .background(JewelTheme.globalColors.panelBackground) - .padding(4.dp) - ) { - Column(modifier = Modifier.verticalScroll(rememberScrollState())) { - availableModels.forEach { model -> - Row( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(4.dp)) - .clickable { onModelSelect(model) } - .padding(horizontal = 12.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = model, - style = JewelTheme.defaultTextStyle.copy(fontSize = 13.sp), - modifier = Modifier.weight(1f) - ) - if (model == modelName) { - Icon( - imageVector = IdeaComposeIcons.Check, - contentDescription = "Selected", - tint = JewelTheme.globalColors.text.normal, - modifier = Modifier.size(16.dp) - ) - } - } - } + availableModels.forEach { model -> + selectableItem( + selected = model == modelName, + onClick = { onModelSelect(model) } + ) { + Text( + text = model, + style = JewelTheme.defaultTextStyle.copy(fontSize = 13.sp) + ) } } } } } } - diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaModelConfigDialogWrapper.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaModelConfigDialogWrapper.kt new file mode 100644 index 0000000000..e6d635342c --- /dev/null +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaModelConfigDialogWrapper.kt @@ -0,0 +1,68 @@ +package cc.unitmesh.devins.idea.editor + +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.util.ui.JBUI +import cc.unitmesh.llm.ModelConfig +import org.jetbrains.jewel.bridge.compose +import java.awt.Dimension +import javax.swing.JComponent + +/** + * DialogWrapper for model configuration that uses IntelliJ's native dialog system. + * This ensures proper z-index handling when used alongside SwingPanel components. + */ +class IdeaModelConfigDialogWrapper( + private val project: Project?, + private val currentConfig: ModelConfig, + private val currentConfigName: String? = null, + private val onSaveCallback: (configName: String, config: ModelConfig) -> Unit +) : DialogWrapper(project) { + + init { + title = "Model Configuration" + init() + contentPanel.border = JBUI.Borders.empty() + rootPane.border = JBUI.Borders.empty() + } + + override fun createSouthPanel(): JComponent? = null + + override fun createCenterPanel(): JComponent { + val dialogPanel = compose { + IdeaModelConfigDialogContent( + currentConfig = currentConfig, + currentConfigName = currentConfigName, + onDismiss = { close(CANCEL_EXIT_CODE) }, + onSave = { configName, config -> + onSaveCallback(configName, config) + close(OK_EXIT_CODE) + } + ) + } + dialogPanel.preferredSize = Dimension(500, 500) + return dialogPanel + } + + companion object { + /** + * Show the model configuration dialog. + * @return true if the dialog was closed with OK, false otherwise + */ + fun show( + project: Project?, + currentConfig: ModelConfig, + currentConfigName: String? = null, + onSave: (configName: String, config: ModelConfig) -> Unit + ): Boolean { + val dialog = IdeaModelConfigDialogWrapper( + project = project, + currentConfig = currentConfig, + currentConfigName = currentConfigName, + onSaveCallback = onSave + ) + return dialog.showAndGet() + } + } +} + 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 5cb2da0250..1c54a7d10f 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 @@ -6,27 +6,21 @@ 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 -import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.InputMode 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 import org.jetbrains.jewel.foundation.theme.JewelTheme -import org.jetbrains.jewel.ui.component.Divider import org.jetbrains.jewel.ui.component.Icon -import org.jetbrains.jewel.ui.component.OutlinedButton +import org.jetbrains.jewel.ui.component.PopupMenu import org.jetbrains.jewel.ui.component.Text -import org.jetbrains.jewel.ui.Orientation +import org.jetbrains.jewel.ui.component.separator /** * Model selector for IntelliJ IDEA plugin. @@ -94,56 +88,70 @@ fun IdeaModelSelector( ) } - // Dropdown popup - positioned above the selector to avoid covering input area + // Dropdown popup using Jewel's PopupMenu for proper z-index handling with SwingPanel if (expanded) { - Popup( - alignment = ComposeAlignment.BottomStart, - onDismissRequest = { expanded = false }, - properties = PopupProperties(focusable = true) + PopupMenu( + onDismissRequest = { + expanded = false + true + }, + horizontalAlignment = Alignment.Start, + modifier = Modifier.widthIn(min = 200.dp, max = 300.dp) ) { - Box( - modifier = Modifier - .widthIn(min = 200.dp, max = 300.dp) - .clip(RoundedCornerShape(8.dp)) - .background(JewelTheme.globalColors.panelBackground) - .padding(4.dp) - ) { - Column( - modifier = Modifier.verticalScroll(rememberScrollState()) - ) { - if (availableConfigs.isNotEmpty()) { - availableConfigs.forEach { config -> - IdeaDropdownMenuItem( - text = "${config.provider} / ${config.model}", - isSelected = config.name == currentConfigName, - onClick = { - onConfigSelect(config) - expanded = false - } - ) + if (availableConfigs.isNotEmpty()) { + availableConfigs.forEach { config -> + selectableItem( + selected = config.name == currentConfigName, + onClick = { + onConfigSelect(config) + expanded = false } - - Divider(Orientation.Horizontal, modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) - } else { - IdeaDropdownMenuItem( - text = "No saved configs", - isSelected = false, - enabled = false, - onClick = {} + ) { + Text( + text = "${config.provider} / ${config.model}", + style = JewelTheme.defaultTextStyle.copy(fontSize = 13.sp) ) - - Divider(Orientation.Horizontal, modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)) } + } + separator() + } else { + selectableItem( + selected = false, + enabled = false, + onClick = {} + ) { + Text( + text = "No saved configs", + style = JewelTheme.defaultTextStyle.copy( + fontSize = 13.sp, + color = JewelTheme.globalColors.text.normal.copy(alpha = 0.5f) + ) + ) + } + separator() + } - // Configure button - IdeaDropdownMenuItem( + // Configure button + selectableItem( + selected = false, + onClick = { + onConfigureClick() + expanded = false + } + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = IdeaComposeIcons.Settings, + contentDescription = null, + tint = JewelTheme.globalColors.text.normal, + modifier = Modifier.size(16.dp) + ) + Text( text = "Configure Model...", - isSelected = false, - leadingIcon = IdeaComposeIcons.Settings, - onClick = { - onConfigureClick() - expanded = false - } + style = JewelTheme.defaultTextStyle.copy(fontSize = 13.sp) ) } } @@ -151,66 +159,3 @@ fun IdeaModelSelector( } } } - -/** - * Individual menu item for IdeaModelSelector dropdown. - */ -@Composable -private fun IdeaDropdownMenuItem( - text: String, - isSelected: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - leadingIcon: androidx.compose.ui.graphics.vector.ImageVector? = null -) { - val backgroundColor = when { - !enabled -> JewelTheme.globalColors.panelBackground - isSelected -> JewelTheme.globalColors.panelBackground.copy(alpha = 0.6f) - else -> JewelTheme.globalColors.panelBackground - } - - val textColor = when { - !enabled -> JewelTheme.globalColors.text.normal.copy(alpha = 0.5f) - else -> JewelTheme.globalColors.text.normal - } - - Row( - modifier = modifier - .fillMaxWidth() - .clip(RoundedCornerShape(4.dp)) - .background(backgroundColor) - .then(if (enabled) Modifier.clickable(onClick = onClick) else Modifier) - .padding(horizontal = 12.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - if (leadingIcon != null) { - Icon( - imageVector = leadingIcon, - contentDescription = null, - tint = textColor, - modifier = Modifier.size(16.dp) - ) - } - - Text( - text = text, - style = JewelTheme.defaultTextStyle.copy( - fontSize = 13.sp, - color = textColor - ), - modifier = Modifier.weight(1f) - ) - - if (isSelected) { - Icon( - imageVector = IdeaComposeIcons.Check, - contentDescription = "Selected", - tint = JewelTheme.globalColors.text.normal, - modifier = Modifier.size(16.dp) - ) - } - } -} - 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 deleted file mode 100644 index c5f6f77723..0000000000 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/editor/IdeaPromptOptimizationDialog.kt +++ /dev/null @@ -1,186 +0,0 @@ -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/toolwindow/IdeaAgentApp.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentApp.kt index 2f1f07e8c4..656fe58752 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 @@ -12,7 +12,7 @@ import cc.unitmesh.devins.idea.editor.IdeaBottomToolbar import cc.unitmesh.devins.idea.editor.IdeaDevInInput import cc.unitmesh.devins.idea.editor.IdeaInputListener import cc.unitmesh.devins.idea.editor.IdeaInputTrigger -import cc.unitmesh.devins.idea.editor.IdeaModelConfigDialog +import cc.unitmesh.devins.idea.editor.IdeaModelConfigDialogWrapper import cc.unitmesh.devins.idea.toolwindow.codereview.IdeaCodeReviewContent import cc.unitmesh.devins.idea.toolwindow.codereview.IdeaCodeReviewViewModel import cc.unitmesh.devins.idea.components.header.IdeaAgentTabsHeader @@ -25,6 +25,7 @@ import cc.unitmesh.devins.idea.toolwindow.remote.getEffectiveProjectId import cc.unitmesh.devins.idea.components.status.IdeaToolLoadingStatusBar import cc.unitmesh.devins.idea.components.timeline.IdeaEmptyStateMessage import cc.unitmesh.devins.idea.components.timeline.IdeaTimelineContent +import cc.unitmesh.devins.ui.config.ConfigManager import cc.unitmesh.llm.ModelConfig import cc.unitmesh.llm.NamedModelConfig import com.intellij.openapi.Disposable @@ -258,19 +259,41 @@ fun IdeaAgentApp( ) } - // Model Configuration Dialog - if (showConfigDialog) { - val dialogConfig = currentModelConfig ?: ModelConfig() - IdeaModelConfigDialog( - currentConfig = dialogConfig, - currentConfigName = currentConfigName, - onDismiss = { viewModel.setShowConfigDialog(false) }, - onSave = { name, config -> - val namedConfig = NamedModelConfig.fromModelConfig(name, config) - viewModel.saveModelConfig(namedConfig, setActive = true) - viewModel.setShowConfigDialog(false) - } - ) + // Model Configuration Dialog using DialogWrapper for proper z-index handling + LaunchedEffect(showConfigDialog) { + if (showConfigDialog) { + val dialogConfig = currentModelConfig ?: ModelConfig() + IdeaModelConfigDialogWrapper.show( + project = project, + currentConfig = dialogConfig, + currentConfigName = currentConfigName, + onSave = { configName, newModelConfig -> + // If creating a new config (not editing current), ensure unique name + val existingNames = availableConfigs.map { it.name } + val finalConfigName = + if (currentConfigName != configName && configName in existingNames) { + // Auto-increment: my-glm -> my-glm-1 -> my-glm-2, etc. + ConfigManager.generateUniqueConfigName(configName, existingNames) + } else { + configName + } + + // Convert ModelConfig to NamedModelConfig + val namedConfig = NamedModelConfig.fromModelConfig( + name = finalConfigName, + config = newModelConfig + ) + + // Save to file + viewModel.saveModelConfig(namedConfig, setActive = true) + + if (finalConfigName != configName) { + println("✅ 配置名称已存在,自动重命名为: $finalConfigName") + } + } + ) + viewModel.setShowConfigDialog(false) + } } } diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentToolWindowFactory.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentToolWindowFactory.kt index 0d11ca39f7..3c3da4c131 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentToolWindowFactory.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentToolWindowFactory.kt @@ -8,6 +8,7 @@ import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindowFactory import cc.unitmesh.devins.idea.services.CoroutineScopeHolder import org.jetbrains.jewel.bridge.addComposeTab +import org.jetbrains.jewel.foundation.JewelFlags /** * Factory for creating the Agent ToolWindow with tab-based navigation. @@ -28,6 +29,10 @@ class IdeaAgentToolWindowFactory : ToolWindowFactory { } override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { + // Enable custom popup rendering to use JBPopup instead of default Compose implementation + // This fixes z-index issues when Compose Popup is used with SwingPanel (e.g., EditorTextField) + JewelFlags.useCustomPopupRenderer = true + createAgentPanel(project, toolWindow) } diff --git a/mpp-ui/build.gradle.kts b/mpp-ui/build.gradle.kts index b8009b0f66..c5f321e74c 100644 --- a/mpp-ui/build.gradle.kts +++ b/mpp-ui/build.gradle.kts @@ -497,6 +497,29 @@ tasks.register("runDocumentCli") { standardInput = System.`in` } +// Task to run Coding CLI +tasks.register("runCodingCli") { + group = "application" + description = "Run Coding Agent CLI for autonomous coding tasks" + + val jvmCompilation = kotlin.jvm().compilations.getByName("main") + classpath(jvmCompilation.output, configurations["jvmRuntimeClasspath"]) + mainClass.set("cc.unitmesh.server.cli.CodingCli") + + // Pass properties - use codingProjectPath to avoid conflict with Gradle's projectPath + if (project.hasProperty("codingProjectPath")) { + systemProperty("projectPath", project.property("codingProjectPath") as String) + } + if (project.hasProperty("codingTask")) { + systemProperty("task", project.property("codingTask") as String) + } + if (project.hasProperty("codingMaxIterations")) { + systemProperty("maxIterations", project.property("codingMaxIterations") as String) + } + + standardInput = System.`in` +} + // Task to run Review CLI tasks.register("runReviewCli") { group = "application" @@ -525,7 +548,7 @@ tasks.register("runReviewCli") { if (project.hasProperty("reviewLanguage")) { systemProperty("reviewLanguage", project.property("reviewLanguage") as String) } - + standardInput = System.`in` } @@ -548,7 +571,7 @@ tasks.register("runDomainDictCli") { if (project.hasProperty("domainFocusArea")) { systemProperty("domainFocusArea", project.property("domainFocusArea") as String) } - + standardInput = System.`in` } diff --git a/mpp-ui/package.json b/mpp-ui/package.json index 470c028bed..acd01a6ee0 100644 --- a/mpp-ui/package.json +++ b/mpp-ui/package.json @@ -1,6 +1,6 @@ { "name": "@autodev/cli", - "version": "0.3.2", + "version": "0.3.3", "description": "AutoDev CLI - Terminal UI for AI-powered development assistant", "type": "module", "bin": { diff --git a/mpp-ui/src/androidMain/kotlin/cc/unitmesh/devins/ui/compose/agent/LiveTerminalItem.android.kt b/mpp-ui/src/androidMain/kotlin/cc/unitmesh/devins/ui/compose/agent/LiveTerminalItem.android.kt index e7ba4ac61f..f04049280b 100644 --- a/mpp-ui/src/androidMain/kotlin/cc/unitmesh/devins/ui/compose/agent/LiveTerminalItem.android.kt +++ b/mpp-ui/src/androidMain/kotlin/cc/unitmesh/devins/ui/compose/agent/LiveTerminalItem.android.kt @@ -19,7 +19,10 @@ actual fun LiveTerminalItem( sessionId: String, command: String, workingDirectory: String?, - ptyHandle: Any? + ptyHandle: Any?, + exitCode: Int?, + executionTimeMs: Long?, + output: String? ) { Card( colors = diff --git a/mpp-ui/src/jvmMain/kotlin/cc/unitmesh/server/cli/CodingCli.kt b/mpp-ui/src/jvmMain/kotlin/cc/unitmesh/server/cli/CodingCli.kt new file mode 100644 index 0000000000..3d86f3be9a --- /dev/null +++ b/mpp-ui/src/jvmMain/kotlin/cc/unitmesh/server/cli/CodingCli.kt @@ -0,0 +1,254 @@ +package cc.unitmesh.server.cli + +import cc.unitmesh.agent.AgentTask +import cc.unitmesh.agent.CodingAgent +import cc.unitmesh.agent.config.McpToolConfigService +import cc.unitmesh.agent.config.ToolConfigFile +import cc.unitmesh.agent.render.CodingAgentRenderer +import cc.unitmesh.agent.tool.filesystem.DefaultToolFileSystem +import cc.unitmesh.llm.KoogLLMService +import cc.unitmesh.llm.LLMProviderType +import cc.unitmesh.llm.ModelConfig +import cc.unitmesh.llm.compression.TokenInfo +import com.charleskorn.kaml.Yaml +import kotlinx.coroutines.runBlocking +import java.io.File + +/** + * JVM CLI for testing CodingAgent with autonomous coding tasks + * + * Usage: + * ```bash + * ./gradlew :mpp-ui:runCodingCli -PcodingProjectPath=/path/to/project -PcodingTask="Add a hello world function" + * ``` + */ +object CodingCli { + + @JvmStatic + fun main(args: Array) { + println("=".repeat(80)) + println("AutoDev Coding Agent CLI (JVM)") + println("=".repeat(80)) + + // Parse arguments + val projectPath = System.getProperty("projectPath") ?: args.getOrNull(0) ?: run { + System.err.println("Usage: -PcodingProjectPath= -PcodingTask= [-PmaxIterations=100]") + return + } + + val task = System.getProperty("task") ?: args.getOrNull(1) ?: run { + System.err.println("Usage: -PcodingProjectPath= -PcodingTask= [-PmaxIterations=100]") + return + } + + val maxIterations = System.getProperty("maxIterations")?.toIntOrNull() ?: 100 + + println("📂 Project Path: $projectPath") + println("📝 Task: $task") + println("🔄 Max Iterations: $maxIterations") + println() + + runBlocking { + try { + val projectDir = File(projectPath).absoluteFile + if (!projectDir.exists()) { + System.err.println("❌ Project path does not exist: $projectPath") + return@runBlocking + } + + val startTime = System.currentTimeMillis() + + // Load configuration from ~/.autodev/config.yaml + val configFile = File(System.getProperty("user.home"), ".autodev/config.yaml") + if (!configFile.exists()) { + System.err.println("❌ Configuration file not found: ${configFile.absolutePath}") + System.err.println(" Please create ~/.autodev/config.yaml with your LLM configuration") + return@runBlocking + } + + val yamlContent = configFile.readText() + val yaml = Yaml(configuration = com.charleskorn.kaml.YamlConfiguration(strictMode = false)) + val config = yaml.decodeFromString(AutoDevConfig.serializer(), yamlContent) + + val activeName = config.active + val activeConfig = config.configs.find { it.name == activeName } + + if (activeConfig == null) { + System.err.println("❌ Active configuration '$activeName' not found in config.yaml") + System.err.println(" Available configs: ${config.configs.map { it.name }.joinToString(", ")}") + return@runBlocking + } + + println("📝 Using config: ${activeConfig.name} (${activeConfig.provider}/${activeConfig.model})") + + // Convert provider string to LLMProviderType + val providerType = when (activeConfig.provider.lowercase()) { + "openai" -> LLMProviderType.OPENAI + "anthropic" -> LLMProviderType.ANTHROPIC + "google" -> LLMProviderType.GOOGLE + "deepseek" -> LLMProviderType.DEEPSEEK + "ollama" -> LLMProviderType.OLLAMA + "openrouter" -> LLMProviderType.OPENROUTER + "glm" -> LLMProviderType.GLM + "qwen" -> LLMProviderType.QWEN + "kimi" -> LLMProviderType.KIMI + else -> LLMProviderType.CUSTOM_OPENAI_BASE + } + + val llmService = KoogLLMService( + ModelConfig( + provider = providerType, + modelName = activeConfig.model, + apiKey = activeConfig.apiKey, + temperature = activeConfig.temperature ?: 0.7, + maxTokens = activeConfig.maxTokens ?: 4096, + baseUrl = activeConfig.baseUrl ?: "" + ) + ) + + val renderer = CodingCliRenderer() + val mcpConfigService = McpToolConfigService(ToolConfigFile()) + + println("🧠 Creating CodingAgent...") + val agent = CodingAgent( + projectPath = projectDir.absolutePath, + llmService = llmService, + maxIterations = maxIterations, + renderer = renderer, + fileSystem = DefaultToolFileSystem(projectDir.absolutePath), + mcpToolConfigService = mcpConfigService, + enableLLMStreaming = true + ) + + println("✅ Agent created") + println() + println("🚀 Executing task...") + println() + + val result = agent.execute( + AgentTask(requirement = task, projectPath = projectDir.absolutePath), + onProgress = { progress -> println(" $progress") } + ) + + val totalTime = System.currentTimeMillis() - startTime + + println() + println("=".repeat(80)) + println("📊 Result:") + println("=".repeat(80)) + println(result.content) + println() + + if (result.success) { + println("✅ Task completed successfully") + } else { + println("❌ Task failed") + } + println("⏱️ Total time: ${totalTime}ms") + println("📈 Steps: ${result.metadata["steps"] ?: "N/A"}") + println("✏️ Edits: ${result.metadata["edits"] ?: "N/A"}") + + } catch (e: Exception) { + System.err.println("❌ Error: ${e.message}") + e.printStackTrace() + } + } + } +} + +/** + * Console renderer for CodingCli output + */ +class CodingCliRenderer : CodingAgentRenderer { + override fun renderIterationHeader(current: Int, max: Int) { + println("\n━━━ Iteration $current/$max ━━━") + } + + override fun renderLLMResponseStart() { + println("💭 ") + } + + override fun renderLLMResponseChunk(chunk: String) { + print(chunk) + System.out.flush() + } + + override fun renderLLMResponseEnd() { + println("\n") + } + + override fun renderToolCall(toolName: String, paramsStr: String) { + println("● $toolName") + if (paramsStr.isNotEmpty()) { + val formatted = formatCliParameters(paramsStr) + formatted.lines().forEach { line -> + println(" ⎿ $line") + } + } + } + + private fun formatCliParameters(params: String): String { + val trimmed = params.trim() + + // Handle JSON format + if (trimmed.startsWith("{") && trimmed.endsWith("}")) { + val lines = mutableListOf() + val jsonPattern = Regex(""""(\w+)"\s*:\s*("([^"]*)"|(\d+)|true|false|null)""") + jsonPattern.findAll(trimmed).forEach { match -> + val key = match.groups[1]?.value ?: "" + val value = match.groups[3]?.value + ?: match.groups[4]?.value + ?: match.groups[2]?.value?.removeSurrounding("\"") + ?: "" + lines.add("$key = $value") + } + return if (lines.isNotEmpty()) lines.joinToString(", ") else params + } + + return params + } + + override fun renderToolResult( + toolName: String, + success: Boolean, + output: String?, + fullOutput: String?, + metadata: Map + ) { + val statusSymbol = if (success) "✓" else "✗" + val preview = (output ?: fullOutput ?: "").lines().take(3).joinToString(" ").take(100) + println(" $statusSymbol ${if (preview.length < (output ?: fullOutput ?: "").length) "$preview..." else preview}") + } + + override fun renderTaskComplete() { + println("\n✓ Task marked as complete") + } + + override fun renderFinalResult(success: Boolean, message: String, iterations: Int) { + val symbol = if (success) "✅" else "❌" + println("\n$symbol Final result after $iterations iterations:") + println(message) + } + + override fun renderError(message: String) { + System.err.println("❌ Error: $message") + } + + override fun renderRepeatWarning(toolName: String, count: Int) { + println("⚠️ Warning: Tool '$toolName' called $count times") + } + + override fun renderRecoveryAdvice(recoveryAdvice: String) { + println("💡 Recovery advice: $recoveryAdvice") + } + + override fun updateTokenInfo(tokenInfo: TokenInfo) { + // Display token info in CLI + println("📊 Tokens: ${tokenInfo.inputTokens} in / ${tokenInfo.outputTokens} out") + } + + override fun renderUserConfirmationRequest(toolName: String, params: Map) { + println("❓ Confirmation required for: $toolName") + println(" Params: $params") + } +}