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")
+ }
+}