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
6 changes: 6 additions & 0 deletions mpp-codegraph/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ kotlin {
// npmPublish configuration disabled temporarily due to wasmJs incompatibility
// To publish JS package, manually configure npm package.json and use npm publish
//
// Disable wasmJs D8 tests due to missing npm dependencies (web-tree-sitter)
// The wasmJs library will still be built, but D8 tests are skipped
tasks.named("wasmJsD8Test") {
enabled = false
}

// npmPublish {
// organization.set("autodev")
// packages {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package cc.unitmesh.codegraph.parser
package cc.unitmesh.codegraph.parser.jvm

import cc.unitmesh.codegraph.CodeGraphFactory
import cc.unitmesh.codegraph.model.CodeElementType
import cc.unitmesh.codegraph.parser.Language
import kotlinx.coroutines.runBlocking
import kotlin.test.Test
import kotlin.test.assertTrue

Expand All @@ -12,11 +14,11 @@ import kotlin.test.assertTrue
class Utf8ParsingTest {

@Test
fun `should parse Kotlin file with emojis and UTF-8 characters without errors`() {
fun `should parse Kotlin file with emojis and UTF-8 characters without errors`() = runSuspend {
val sourceCode = """
// Test file with UTF-8 characters including emojis
package cc.unitmesh.test

/**
* 🤖 Auto-starting analysis with multiple UTF-8 characters
* This class tests parsing of files with emojis and other multi-byte UTF-8 characters.
Expand All @@ -27,14 +29,14 @@ class Utf8ParsingTest {
println("Hello 世界 🌍")
println("Testing emoji 🚀 parsing")
}

fun processData() {
// 处理数据
val message = "Success ✅"
val error = "Error ❌"
val warning = "Warning ⚠️"
}

/**
* Multi-line comment with emojis
* 🔍 Analyzing modified code structure...
Expand All @@ -46,33 +48,31 @@ class Utf8ParsingTest {
}
}
""".trimIndent()

val parser = CodeGraphFactory.createParser()

// This should not throw "Range out of bounds" error
val nodes = runSuspend {
parser.parseNodes(sourceCode, "TestClass.kt", Language.KOTLIN)
}

val nodes = parser.parseNodes(sourceCode, "TestClass.kt", Language.KOTLIN)

// Verify we successfully parsed the file without errors
// The main goal is to ensure no "Range out of bounds" error occurs
assertTrue(nodes.isNotEmpty(), "Should have parsed at least one node")

// Check that we found some code elements (the exact count may vary by platform)
val classNodes = nodes.filter { it.type == CodeElementType.CLASS }
val methodNodes = nodes.filter { it.type == CodeElementType.METHOD || it.type == CodeElementType.FUNCTION }
assertTrue(classNodes.isNotEmpty() || methodNodes.isNotEmpty(),

assertTrue(classNodes.isNotEmpty() || methodNodes.isNotEmpty(),
"Should have found at least some class or method nodes. Found ${nodes.size} nodes total")
}

@Test
fun `should parse Java file with UTF-8 characters`() {
fun `should parse Java file with UTF-8 characters`() = runSuspend {
val sourceCode = """
package com.example;

/**
* Test class with UTF-8
* Test class with UTF-8
* 测试类 with Chinese characters
*/
public class Example {
Expand All @@ -82,41 +82,37 @@ class Utf8ParsingTest {
}
}
""".trimIndent()

val parser = CodeGraphFactory.createParser()

val nodes = runSuspend {
parser.parseNodes(sourceCode, "Example.java", Language.JAVA)
}


val nodes = parser.parseNodes(sourceCode, "Example.java", Language.JAVA)

assertTrue(nodes.isNotEmpty(), "Should have parsed the file")
val classNode = nodes.find { it.type == CodeElementType.CLASS }
assertTrue(classNode != null, "Should have found the class")
}

@Test
fun `should correctly extract text content with emojis`() {
fun `should correctly extract text content with emojis`() = runSuspend {
// This test verifies that the content extraction works correctly
val sourceCode = """
fun emoji() {
println("🎉")
}
""".trimIndent()

val parser = CodeGraphFactory.createParser()

val nodes = runSuspend {
parser.parseNodes(sourceCode, "test.kt", Language.KOTLIN)
}


val nodes = parser.parseNodes(sourceCode, "test.kt", Language.KOTLIN)

val method = nodes.find { it.type == CodeElementType.METHOD }
assertTrue(method != null, "Should have found the method")
assertTrue(method.content.contains("🎉"), "Method content should contain the emoji")
}

private fun <T> runSuspend(block: suspend () -> T): T {
// Simple synchronous runner for tests
return kotlinx.coroutines.runBlocking {
// Use runBlocking for JVM tests
return runBlocking {
block()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import java.io.File
* - No special permissions required
*/
actual object ConfigManager {
private var appContext: Context? = null
var appContext: Context? = null

/**
* Initialize ConfigManager with Android Context
Expand Down Expand Up @@ -195,6 +195,10 @@ actual object ConfigManager {
return ""
}

actual fun getAcpLogsDir(): String {
return File(getConfigDir(), "acp-logs").absolutePath
}

actual suspend fun loadToolConfig(): ToolConfigFile =
withContext(Dispatchers.IO) {
try {
Expand Down
28 changes: 22 additions & 6 deletions mpp-core/src/commonMain/kotlin/cc/unitmesh/config/ConfigFile.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,16 @@ import kotlinx.serialization.Serializable
* args: "--acp"
* env: ""
* claude:
* name: "Claude CLI"
* name: "Claude Code"
* command: "claude"
* args: "--acp"
* args: "-p --output-format stream-json --input-format stream-json"
* env: ""
* activeAcpAgent: kimi
* auggie:
* name: "Auggie"
* command: "auggie"
* args: "--acp"
* env: "AUGGIE_API_KEY=xxx"
* activeAcpAgent: auggie
* ```
*/
@Serializable
Expand Down Expand Up @@ -138,6 +143,12 @@ data class CloudStorageConfig(
* Defines an external ACP-compliant agent that can be spawned as a child process
* and communicated with via JSON-RPC over stdio.
*
* Supported agents:
* - **Kimi CLI**: Chinese AI agent with strong coding capabilities
* - **Claude CLI**: Anthropic's Claude Code agent
* - **Auggie**: Augment Code's AI agent with ACP support
* - **Gemini CLI**: Google's Gemini agent (when available)
*
* Example config.yaml:
* ```yaml
* acpAgents:
Expand All @@ -147,11 +158,16 @@ data class CloudStorageConfig(
* args: "--acp"
* env: "KIMI_API_KEY=xxx"
* claude:
* name: "Claude CLI"
* name: "Claude Code"
* command: "claude"
* args: "--acp"
* args: "-p --output-format stream-json --input-format stream-json"
* env: ""
* activeAcpAgent: kimi
* auggie:
* name: "Auggie"
* command: "auggie"
* args: "--acp"
* env: "AUGGIE_API_KEY=xxx"
* activeAcpAgent: auggie
* ```
*/
@Serializable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,10 @@ actual object ConfigManager {
return ""
}

actual fun getAcpLogsDir(): String {
return "$configDir/acp-logs"
}

actual fun getToolConfigPath(): String = toolConfigFilePath

actual fun generateUniqueConfigName(baseName: String, existingNames: List<String>): String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,14 @@ actual object ConfigManager {
return ""
}

actual fun getAcpLogsDir(): String {
return if (isNodeJs) {
pathModule.join(configDir, "acp-logs") as String
} else {
"/tmp/.autodev/acp-logs"
}
}

private fun createEmpty(): AutoDevConfigWrapper {
return AutoDevConfigWrapper(ConfigFile(active = "", configs = emptyList()))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ actual object ConfigManager {
return ""
}

actual fun getAcpLogsDir(): String {
// ACP logs are stored in localStorage in WASM environment
return "$configDir/acp-logs"
}

actual suspend fun loadToolConfig(): ToolConfigFile {
return try {
val content = BrowserStorage.getItem(TOOL_CONFIG_KEY)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,45 +27,59 @@ class IdeaAgentToolWindowFactory : ToolWindowFactory {
}

override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
thisLogger().warn("createToolWindowContent called - project: ${project.name}")
thisLogger().warn("=== createToolWindowContent START === Project: ${project.name}")

// 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)
// See: JewelFlags.useCustomPopupRenderer in Jewel foundation
System.setProperty("jewel.customPopupRender", "true")
thisLogger().warn("jewel.customPopupRender property set")

createAgentPanel(project, toolWindow)
thisLogger().warn("=== createToolWindowContent END ===")
}

override fun shouldBeAvailable(project: Project): Boolean = true

private fun createAgentPanel(project: Project, toolWindow: ToolWindow) {
thisLogger().warn("createAgentPanel() called")
val contentManager = toolWindow.contentManager

// Check if Agent content already exists to prevent duplicate creation
// This can happen when the tool window is hidden and restored, or when squeezed by other windows
val existingContent = contentManager.findContent("Agent")
if (existingContent != null) {
thisLogger().warn("Agent content already exists - reusing existing content")
contentManager.setSelectedContent(existingContent)
return
}

thisLogger().warn("Creating new Agent content")
val toolWindowDisposable = toolWindow.disposable

// Create ViewModel OUTSIDE of Compose to prevent recreation when Compose tree is rebuilt
// Jewel's addComposeTab may rebuild the Compose tree multiple times during initialization
thisLogger().warn("Creating coroutine scope with Dispatchers.Main")
val coroutineScope = kotlinx.coroutines.CoroutineScope(
kotlinx.coroutines.SupervisorJob() + kotlinx.coroutines.Dispatchers.Main
)

thisLogger().warn("Creating IdeaAgentViewModel...")
val viewModel = IdeaAgentViewModel(project, coroutineScope)
thisLogger().warn("IdeaAgentViewModel created - registering disposable")
Disposer.register(toolWindowDisposable, viewModel)

Disposer.register(toolWindowDisposable) {
thisLogger().warn("ToolWindow disposable triggered - cancelling coroutine scope")
coroutineScope.cancel()
}

thisLogger().warn("Adding Compose tab to tool window...")
toolWindow.addComposeTab("Agent") {
thisLogger().warn("IdeaAgentApp composable invoked")
IdeaAgentApp(viewModel, project, coroutineScope)
}
Comment on lines 78 to 81
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Logging inside the Compose tab lambda may fire on every recomposition.

The lambda passed to addComposeTab is a @Composable function. thisLogger().warn(...) at Line 79 will execute every time the Compose tree is rebuilt (which Jewel may do multiple times during init and on window resize/restore). This will generate repeated log entries with no new information.

Move this log before the addComposeTab call (already present at Line 77), or guard it with a LaunchedEffect(Unit) if you need it inside the composable scope.

Proposed fix: remove the log inside the composable
         thisLogger().warn("Adding Compose tab to tool window...")
         toolWindow.addComposeTab("Agent") {
-            thisLogger().warn("IdeaAgentApp composable invoked")
             IdeaAgentApp(viewModel, project, coroutineScope)
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
toolWindow.addComposeTab("Agent") {
thisLogger().warn("IdeaAgentApp composable invoked")
IdeaAgentApp(viewModel, project, coroutineScope)
}
toolWindow.addComposeTab("Agent") {
IdeaAgentApp(viewModel, project, coroutineScope)
}
🤖 Prompt for AI Agents
In
`@mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/toolwindow/IdeaAgentToolWindowFactory.kt`
around lines 78 - 81, The warning log thisLogger().warn(...) is inside the
`@Composable` lambda passed to addComposeTab and will run on every recomposition;
remove that log from inside the lambda and either move the logging statement
before the addComposeTab call (where the surrounding non‑composable scope
exists) or, if you must log from within the composable scope, wrap the log in a
LaunchedEffect(Unit) so it only runs once; update references to IdeaAgentApp and
the lambda accordingly.

thisLogger().warn("Compose tab added successfully")
}

private fun kotlinx.coroutines.CoroutineScope.cancel() {
Expand Down
Loading
Loading