Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 8 additions & 30 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,39 +64,17 @@ 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
}
}
```
## VSCode Plugin (mpp-vscode)

`mpp-vscode` is a standalone npm package with `devDependencies` on parent project.

## Release

1. modify version in `gradle.properties`
2. publish cli version: `cd mpp-ui && npm publish:remote`
3. publish Desktop: `git tag compose-vVersion` (same in `gradle.properties`), `git push origin compose-vVersion`
4. draft release in GitHub, run gh cli: `gh release create compose-vVersion --draft`

### Desktop Compose App

1. publish Desktop: `git tag compose-vVersion` (same in `gradle.properties`), `git push origin compose-vVersion`
2. draft release in GitHub, run gh cli: `gh release create compose-vVersion --draft`

57 changes: 52 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,58 @@
> 🧙‍AutoDev: The AI-powered coding wizard with multilingual support 🌐, auto code generation 🏗️, and a helpful
> bug-slaying assistant 🐞! Customizable prompts 🎨 and a magic Auto Dev/Testing/Document/Agent feature 🧪 included! 🚀

## AutoDev 3.0 - Multiplatform Plane (Doing)

- Multiplatform Agent: Android/iOS, Web, Desktop, IDEs/VSCode,CLI/TUI
- Web: https://unit-mesh.github.io/auto-dev/
- Built-in Coding Agent
## AutoDev 3.0 - Multiplatform Revolution 🚀

Rebuilt with Kotlin Multiplatform (KMP) to deliver a truly cross-platform AI Coding Agent ecosystem.

**Current Versions**:
- IntelliJ Plugin: `v2.4.6`
- New Intellij Plugin: https://plugins.jetbrains.com/plugin/29223-autodev-experiment
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent capitalization: "Intellij" should be "IntelliJ" (with capital J) to match JetBrains' official product naming.

Suggested change
- New Intellij Plugin: https://plugins.jetbrains.com/plugin/29223-autodev-experiment
- New IntelliJ Plugin: https://plugins.jetbrains.com/plugin/29223-autodev-experiment

Copilot uses AI. Check for mistakes.
- MPP Modules (Core/UI/Server): `v0.3.4`
- VSCode Extension: `v0.5.x`

### 📦 Core Modules

| Module | Platform Support | Description |
|--------|------------------|-------------|
| **mpp-core** | JVM, JS, WASM, Android, iOS | AI Agent engine, DevIns compiler, tool system, LLM integration, MCP protocol |
| **mpp-codegraph** | JVM, JS | TreeSitter-based code parsing & graph building (8+ languages) |

### 🖥️ Client Applications

| Module | Platform | Status | Description |
|--------|----------|--------|-------------|
| **mpp-idea** | IntelliJ IDEA | ✅ Production | Jewel UI, Agent toolwindow, code review, remote agent |
| **mpp-vscode** | VSCode | ✅ Production | CodeLens, auto test/doc, MCP protocol, Tree-sitter |
| **mpp-ui** (Desktop) | macOS/Windows/Linux | ✅ Production | Compose Multiplatform desktop app |
| **mpp-ui** (CLI) | Terminal (Node.js) | ✅ Production | Terminal UI (React/Ink), local/server mode |
| **mpp-ui** (Android) | Android | 🚧 In Progress | Native Android app |
| **mpp-web** (Web) | Web | 🚧 In Progress | Web app |
| **mpp-ios** | iOS | 🚧 In Progress | Native iOS app (SwiftUI + Compose) |

### ⚙️ Server & Tools

| Module | Platform | Features |
|--------|----------|----------|
| **mpp-server** | JVM (Ktor) | HTTP API, SSE streaming, remote project management |
| **mpp-viewer** | Multiplatform | Universal viewer API (code, Markdown, images, PDF, etc.) |
| **mpp-viewer-web** | JVM, Android, iOS | WebView implementation, Monaco Editor integration |

### 🌟 Key Features

- **Unified Codebase**: Core logic shared across all platforms - write once, run everywhere
- **Native Performance**: Compiled natively for each platform with zero overhead
- **Full AI Agent**: Built-in Coding Agent, tool system, multi-LLM support (OpenAI, Anthropic, Google, DeepSeek, Ollama, etc.)
- **DevIns Language**: Executable AI Agent scripting language
- **MCP Protocol**: Model Context Protocol support for extensible tool ecosystem
- **Code Understanding**: TreeSitter-based multi-language parsing (Java, Kotlin, Python, JS, TS, Go, Rust, C#)
- **Internationalization**: Chinese/English UI support

### 🔗 Links

- **Web Demo**: https://unit-mesh.github.io/auto-dev/
- **VSCode Extension**: [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=Phodal.autodev)
- **CLI Tool**: `npm install -g @autodev/cli`

## AutoDev 2.0 - the Cursor Composer in Intellij IDEA

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,8 @@ import com.intellij.openapi.components.service
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import com.jayway.jsonpath.JsonPath
import io.reactivex.rxjava3.core.BackpressureStrategy
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.FlowableEmitter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
Expand Down Expand Up @@ -71,26 +69,22 @@ open class CustomSSEProcessor(private val project: Project) {

@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
fun streamSSE(call: Call, promptText: String, keepHistory: Boolean = false, messages: MutableList<Message>): Flow<String> {
var emit: FlowableEmitter<SSE>? = null
val sseFlowable = Flowable
.create({ emitter: FlowableEmitter<SSE> ->
emit = emitter.apply { call.enqueue(ResponseBodyCallback(emitter, true)) }
}, BackpressureStrategy.BUFFER)
var producerScope: ProducerScope<SSE>? = null
val sseFlow = callbackFlow {
producerScope = this
call.enqueue(ResponseBodyCallback(this, true))
awaitClose { }
}

try {
var output = ""
var reasonerOutput = ""
return CustomFlowWrapper(callbackFlow {
withContext(Dispatchers.IO) {
sseFlowable
.doOnError {
it.printStackTrace()
trySend(it.message ?: "Error occurs")
close()
}
.blockingForEach { sse ->
try {
sseFlow.collect { sse ->
if (sse.data == "[DONE]") {
return@blockingForEach
return@collect
}

if (responseFormat.isNotEmpty()) {
Expand Down Expand Up @@ -148,6 +142,11 @@ open class CustomSSEProcessor(private val project: Project) {
}
}
}
} catch (e: Exception) {
e.printStackTrace()
trySend(e.message ?: "Error occurs")
close()
}

// when stream finished, check if any response parsed succeeded
// if not, notice user check response format
Expand Down Expand Up @@ -176,7 +175,7 @@ open class CustomSSEProcessor(private val project: Project) {
close()
}
awaitClose()
}).also { it.cancelCallback { emit?.onComplete() } }
}).also { it.cancelCallback { producerScope?.close() } }
} catch (e: Exception) {
if (hasSuccessRequest) {
logger.info("Failed to stream", e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
package cc.unitmesh.devti.llms.custom

import com.intellij.openapi.diagnostic.logger
import io.reactivex.rxjava3.core.FlowableEmitter
import kotlinx.coroutines.channels.ProducerScope
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Response
Expand All @@ -39,10 +39,9 @@ class AutoDevHttpException(error: String, private val statusCode: Int) : Runtime

/**
* Callback to parse Server Sent Events (SSE) from raw InputStream and
* emit the events with io.reactivex.FlowableEmitter to allow streaming of
* SSE.
* emit the events with ProducerScope to allow streaming of SSE.
*/
class ResponseBodyCallback(private val emitter: FlowableEmitter<SSE>, private val emitDone: Boolean) : Callback {
class ResponseBodyCallback(private val emitter: ProducerScope<SSE>, private val emitDone: Boolean) : Callback {
val logger = logger<ResponseBodyCallback>()

override fun onResponse(call: Call, response: Response) {
Expand All @@ -59,7 +58,7 @@ class ResponseBodyCallback(private val emitter: FlowableEmitter<SSE>, private va
reader = BufferedReader(InputStreamReader(inputStream, StandardCharsets.UTF_8))
var line: String? = null
var sse: SSE? = null
while (!emitter.isCancelled && reader.readLine().also { line = it } != null) {
while (!emitter.isClosedForSend && reader.readLine().also { line = it } != null) {
sse = when {
line!!.startsWith("data:") -> {
val data = line!!.substring(5).trim { it <= ' ' }
Expand All @@ -69,11 +68,11 @@ class ResponseBodyCallback(private val emitter: FlowableEmitter<SSE>, private va
line == "" && sse != null -> {
if (sse.isDone) {
if (emitDone) {
emitter.onNext(sse)
emitter.trySend(sse)
}
break
}
emitter.onNext(sse)
emitter.trySend(sse)
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using trySend() without checking the result can silently drop messages if the channel buffer is full. Unlike RxJava's onNext() which would handle backpressure, trySend() returns a ChannelResult that should be checked. Consider using send() (which suspends) or handling the trySend() result to detect message loss, especially for critical SSE events.

Suggested change
emitter.trySend(sse)
val result = emitter.trySend(sse)
if (!result.isSuccess) {
logger.warn("SSE message dropped: $sse (reason: ${result.exceptionOrNull()?.message})")
}

Copilot uses AI. Check for mistakes.
null
}
// starts with event:
Expand All @@ -82,8 +81,8 @@ class ResponseBodyCallback(private val emitter: FlowableEmitter<SSE>, private va
val eventName = line!!.substring(6).trim { it <= ' ' }
if (eventName == "ping") {
// skip ping event and data
emitter.onNext(sse ?: SSE(""))
emitter.onNext(sse ?: SSE(""))
emitter.trySend(sse ?: SSE(""))
emitter.trySend(sse ?: SSE(""))
Comment on lines +84 to +85
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using trySend() without checking the result can silently drop messages if the channel buffer is full. Unlike RxJava's onNext() which would handle backpressure, trySend() returns a ChannelResult that should be checked. Consider using send() (which suspends) or handling the trySend() result to detect message loss.

Copilot uses AI. Check for mistakes.
}

null
Expand All @@ -108,8 +107,8 @@ class ResponseBodyCallback(private val emitter: FlowableEmitter<SSE>, private va
}

line.startsWith("{") && line.endsWith("}") -> {
emitter.onNext(SSE(line))
emitter.onComplete()
emitter.trySend(SSE(line))
Copy link

Copilot AI Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using trySend() without checking the result can silently drop messages if the channel buffer is full. Unlike RxJava's onNext() which would handle backpressure, trySend() returns a ChannelResult that should be checked. Consider using send() (which suspends) or handling the trySend() result to detect message loss.

Suggested change
emitter.trySend(SSE(line))
val sendResult = emitter.trySend(SSE(line))
if (!sendResult.isSuccess) {
logger.error("Failed to send SSE event: $line. Reason: ${sendResult.exceptionOrNull()}")
}

Copilot uses AI. Check for mistakes.
emitter.close()
return
}

Expand All @@ -121,7 +120,7 @@ class ResponseBodyCallback(private val emitter: FlowableEmitter<SSE>, private va
}
}

emitter.onComplete()
emitter.close()
} catch (t: Throwable) {
logger<ResponseBodyCallback>().error("Error while reading SSE", t)
logger<ResponseBodyCallback>().error("Request: ${call.request()}")
Expand All @@ -138,6 +137,6 @@ class ResponseBodyCallback(private val emitter: FlowableEmitter<SSE>, private va
}

override fun onFailure(call: Call, e: IOException) {
emitter.onError(e)
emitter.close(e)
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,18 @@
package cc.unitmesh.devti.mcp.editor

import com.intellij.openapi.fileEditor.AsyncFileEditorProvider
import com.intellij.openapi.fileEditor.FileEditor
import com.intellij.openapi.fileEditor.FileEditorPolicy
import com.intellij.openapi.fileEditor.WeighedFileEditorProvider
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile

class McpPreviewEditorProvider : WeighedFileEditorProvider(), AsyncFileEditorProvider {
class McpPreviewEditorProvider : WeighedFileEditorProvider() {
override fun accept(project: Project, file: VirtualFile) = file.name.contains(".mcp.json")

override fun createEditor(project: Project, virtualFile: VirtualFile): FileEditor {
return McpPreviewEditor(project, virtualFile)
}

override fun createEditorAsync(project: Project, file: VirtualFile): AsyncFileEditorProvider.Builder {
return object : AsyncFileEditorProvider.Builder() {
override fun build(): FileEditor {
return McpPreviewEditor(project, file)
}
}
}

override fun getEditorTypeId(): String = "mcp-preview-editor"

override fun getPolicy(): FileEditorPolicy = FileEditorPolicy.PLACE_AFTER_DEFAULT_EDITOR
Expand Down
36 changes: 21 additions & 15 deletions core/src/main/kotlin/cc/unitmesh/devti/mcp/host/BuiltinMcpTools.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.intellij.ide.DataManager
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.ex.ActionManagerEx
import com.intellij.openapi.actionSystem.ex.ActionUtil
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.invokeLater
Expand All @@ -20,7 +21,7 @@ import com.intellij.openapi.editor.Document
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.fileEditor.FileEditorManager.getInstance
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.progress.impl.CoreProgressManager
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.guessProjectDir
import com.intellij.openapi.roots.OrderEnumerator
Expand Down Expand Up @@ -611,13 +612,13 @@ class ListAvailableActionsTool : AbstractMcpTool<NoArgs>() {

// Create event and presentation to check if action is enabled
val event = AnActionEvent.createFromAnAction(action, null, "", dataContext)
val presentation = action.templatePresentation.clone()

// Update presentation to check if action is available
action.update(event)

// Use ActionUtil to update the presentation properly instead of calling update directly
ActionUtil.performDumbAwareUpdate(action, event, false)

// Only include actions that have text and are enabled
if (event.presentation.isEnabledAndVisible && !presentation.text.isNullOrBlank()) {
val presentation = event.presentation
if (presentation.isEnabledAndVisible && !presentation.text.isNullOrBlank()) {
"""{"id": "$actionId", "text": "${presentation.text.replace("\"", "\\\"")}"}"""
} else {
null
Expand Down Expand Up @@ -652,13 +653,15 @@ class ExecuteActionByIdTool : AbstractMcpTool<ExecuteActionArgs>() {
}

ApplicationManager.getApplication().invokeLater({
val dataContext = DataManager.getInstance().dataContext
val event = AnActionEvent.createFromAnAction(
action,
null,
"",
DataManager.getInstance().dataContext
dataContext
)
action.actionPerformed(event)
// Use ActionUtil to properly invoke the action instead of calling actionPerformed directly
ActionUtil.performActionDumbAwareWithCallbacks(action, event)
}, ModalityState.NON_MODAL)

return Response("ok")
Expand All @@ -668,26 +671,29 @@ class ExecuteActionByIdTool : AbstractMcpTool<ExecuteActionArgs>() {
class GetProgressIndicatorsTool : AbstractMcpTool<NoArgs>() {
override val name: String = "get_progress_indicators"
override val description: String = """
Retrieves the status of all running progress indicators in JetBrains IDE editor.
Returns a JSON array of objects containing progress information:
Retrieves the status of current progress indicator in JetBrains IDE editor.
Returns a JSON object containing progress information:
- text: The progress text/description
- fraction: The progress ratio (0.0 to 1.0)
- indeterminate: Whether the progress is indeterminate
Returns an empty array if no progress indicators are running.
Returns empty object if no progress indicator is running.
Note: Only returns the current thread's progress indicator.
""".trimIndent()

override fun handle(project: Project, args: NoArgs): Response {
val runningIndicators = CoreProgressManager.getCurrentIndicators()
val progressManager = ProgressManager.getInstance()
val indicator = progressManager.progressIndicator

val progressInfos = runningIndicators.map { indicator ->
if (indicator != null) {
val text = indicator.text ?: ""
val fraction = if (indicator.isIndeterminate) -1.0 else indicator.fraction
val indeterminate = indicator.isIndeterminate

"""{"text": "${text.replace("\"", "\\\"")}", "fraction": $fraction, "indeterminate": $indeterminate}"""
val progressInfo = """{"text": "${text.replace("\"", "\\\"")}", "fraction": $fraction, "indeterminate": $indeterminate}"""
return Response(progressInfo)
}

return Response(progressInfos.joinToString(",\n", prefix = "[", postfix = "]"))
return Response("{}")
}
}

Expand Down
Loading
Loading