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
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ actual object ConfigManager {
val wrapper = load()
return wrapper.getIssueTracker()
}

actual fun getKcefInstallDir(): String {
// KCEF is not available on Android
return ""
}

actual suspend fun loadToolConfig(): ToolConfigFile =
withContext(Dispatchers.IO) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,5 +134,12 @@ expect object ConfigManager {
* @return IssueTrackerConfig (returns default config if not set)
*/
suspend fun getIssueTracker(): IssueTrackerConfig

/**
* Get KCEF (Chromium Embedded Framework) installation directory
*
* @return Path to KCEF installation directory (e.g., ~/.autodev/kcef-bundle)
*/
fun getKcefInstallDir(): String
}

Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,11 @@ actual object ConfigManager {
val wrapper = load()
return wrapper.getIssueTracker()
}

actual fun getKcefInstallDir(): String {
// KCEF is not available on iOS
return ""
}

actual fun getToolConfigPath(): String = toolConfigFilePath

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,11 @@ actual object ConfigManager {
val wrapper = load()
return wrapper.getIssueTracker()
}

actual fun getKcefInstallDir(): String {
// KCEF is not available on Node.js
return ""
}

private fun createEmpty(): AutoDevConfigWrapper {
return AutoDevConfigWrapper(ConfigFile(active = "", configs = emptyList()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,5 +239,10 @@ actual object ConfigManager {
val wrapper = load()
return wrapper.getIssueTracker()
}

actual fun getKcefInstallDir(): String {
val kcefDir = File(configDir, "kcef-bundle")
return kcefDir.absolutePath
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,11 @@ actual object ConfigManager {
val wrapper = load()
return wrapper.getIssueTracker()
}

actual fun getKcefInstallDir(): String {
// KCEF is not available in WASM browser environment
return ""
}

actual suspend fun loadToolConfig(): ToolConfigFile {
return try {
Expand Down
2 changes: 2 additions & 0 deletions mpp-idea/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,8 @@ project(":") {

// Note: mpp-ui dependency removed - configuration management moved to mpp-core
// Color definitions moved to IdeaAutoDevColors in mpp-idea
// Note: ComposeCharts library cannot be used due to ClassLoader conflicts with IntelliJ's Compose runtime
// Chart rendering is implemented manually using Compose Canvas API

testImplementation(kotlin("test"))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import cc.unitmesh.agent.render.TimelineItem
import cc.unitmesh.devins.idea.renderer.sketch.chart.IdeaChartRenderer
import cc.unitmesh.devins.idea.renderer.sketch.IdeaMermaidRenderer
import com.intellij.openapi.Disposable
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.component.Text

Expand Down Expand Up @@ -91,18 +95,21 @@ fun IdeaTimelineItemView(
}
is TimelineItem.AgentSketchBlockItem -> {
// Agent-generated sketch block (chart, nanodsl, mermaid, etc.)
IdeaAgentSketchBlockBubble(item)
IdeaAgentSketchBlockBubble(item, project = project)
}
}
}

/**
* Agent-generated sketch block bubble (chart, nanodsl, mermaid, etc.)
* Uses appropriate renderer based on language type.
*/
@Composable
fun IdeaAgentSketchBlockBubble(item: TimelineItem.AgentSketchBlockItem) {
val lines = item.code.lines()

fun IdeaAgentSketchBlockBubble(
item: TimelineItem.AgentSketchBlockItem,
project: Project? = null,
parentDisposable: Disposable? = null
) {
Box(
modifier = Modifier
.fillMaxWidth()
Expand Down Expand Up @@ -144,59 +151,89 @@ fun IdeaAgentSketchBlockBubble(item: TimelineItem.AgentSketchBlockItem) {

Spacer(modifier = Modifier.height(8.dp))

// Code content
Box(
modifier = Modifier
.fillMaxWidth()
.background(
JewelTheme.globalColors.panelBackground,
shape = RoundedCornerShape(4.dp)
// Render content based on language type
when (item.language.lowercase()) {
"chart", "graph" -> {
IdeaChartRenderer(
chartCode = item.code,
modifier = Modifier.fillMaxWidth()
)
.padding(8.dp)
) {
Column {
lines.take(20).forEachIndexed { index, line ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "${index + 1}",
style = JewelTheme.defaultTextStyle.copy(
fontSize = 10.sp,
color = JewelTheme.globalColors.text.info.copy(alpha = 0.4f)
),
modifier = Modifier.width(24.dp)
)
Text(
text = line,
style = JewelTheme.defaultTextStyle.copy(fontSize = 11.sp)
)
}
}
if (lines.size > 20) {
Text(
text = "... (${lines.size - 20} more lines)",
style = JewelTheme.defaultTextStyle.copy(
fontSize = 10.sp,
color = JewelTheme.globalColors.text.info.copy(alpha = 0.5f)
)
)
}
}
"mermaid", "mmd" -> {
val disposable = parentDisposable ?: Disposer.newDisposable("AgentSketchBlock")
IdeaMermaidRenderer(
mermaidCode = item.code,
project = project,
isDarkTheme = true,
parentDisposable = disposable,
modifier = Modifier.fillMaxWidth()
)
Comment on lines +162 to +170
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Resource leak: orphan Disposable is never disposed.

When parentDisposable is null, a new Disposable is created but never registered for cleanup. This leaks resources on every recomposition. The disposable should be tied to the composable's lifecycle using DisposableEffect or require a non-null parentDisposable.

                 "mermaid", "mmd" -> {
-                    val disposable = parentDisposable ?: Disposer.newDisposable("AgentSketchBlock")
+                    val disposable = parentDisposable
+                        ?: return@Column // Skip rendering if no parent disposable available
                     IdeaMermaidRenderer(
                         mermaidCode = item.code,
                         project = project,
                         isDarkTheme = true,
                         parentDisposable = disposable,
                         modifier = Modifier.fillMaxWidth()
                     )
                 }

Alternatively, manage the disposable lifecycle properly:

val disposable = remember { parentDisposable ?: Disposer.newDisposable("AgentSketchBlock") }
DisposableEffect(Unit) {
    onDispose {
        if (parentDisposable == null) Disposer.dispose(disposable)
    }
}
πŸ€– Prompt for AI Agents
In
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/components/timeline/IdeaTimelineContent.kt
around lines 162 to 170, a new Disposable is created when parentDisposable is
null but never registered or disposed, leaking resources on recomposition;
instead, ensure the disposable is tied to the composable lifecycle by
remembering the disposable (e.g., using remember { parentDisposable ?:
Disposer.newDisposable(...) }) and disposing it in a DisposableEffect onDispose
(only dispose if parentDisposable was null), or require a non-null
parentDisposable to be passed so no orphan Disposable is created.

}
Comment on lines +162 to +171
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

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

Potential resource leak: A new Disposable is created when parentDisposable is null, but it's never disposed. This can lead to memory leaks. The disposable should be properly managed.

Consider either:

  1. Requiring parentDisposable to not be null (since this is in the IDEA plugin context where disposables are always available)
  2. Using DisposableEffect to properly manage the lifecycle:
"mermaid", "mmd" -> {
    DisposableEffect(Unit) {
        val disposable = parentDisposable ?: Disposer.newDisposable("AgentSketchBlock")
        onDispose {
            if (parentDisposable == null) {
                Disposer.dispose(disposable)
            }
        }
    }
    IdeaMermaidRenderer(
        mermaidCode = item.code,
        project = project,
        isDarkTheme = true,
        parentDisposable = disposable,
        modifier = Modifier.fillMaxWidth()
    )
}

Copilot uses AI. Check for mistakes.
else -> {
// Fallback: display raw code
RenderCodeFallback(item.code, item.language)
}
}
}
}
}

// Footer with line count
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "${lines.size} lines of ${item.language} code",
style = JewelTheme.defaultTextStyle.copy(
fontSize = 10.sp,
color = JewelTheme.globalColors.text.info.copy(alpha = 0.5f)
)
/**
* Fallback renderer for code content when no specific renderer is available.
*/
@Composable
private fun RenderCodeFallback(code: String, language: String) {
val lines = code.lines()
Box(
modifier = Modifier
.fillMaxWidth()
.background(
JewelTheme.globalColors.panelBackground,
shape = RoundedCornerShape(4.dp)
)
.padding(8.dp)
) {
Column {
lines.take(20).forEachIndexed { index, line ->
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = "${index + 1}",
style = JewelTheme.defaultTextStyle.copy(
fontSize = 10.sp,
color = JewelTheme.globalColors.text.info.copy(alpha = 0.4f)
),
modifier = Modifier.width(24.dp)
)
Text(
text = line,
style = JewelTheme.defaultTextStyle.copy(fontSize = 11.sp)
)
}
}
if (lines.size > 20) {
Text(
text = "... (${lines.size - 20} more lines)",
style = JewelTheme.defaultTextStyle.copy(
fontSize = 10.sp,
color = JewelTheme.globalColors.text.info.copy(alpha = 0.5f)
)
)
}
}
}

// Footer with line count
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "${lines.size} lines of $language code",
style = JewelTheme.defaultTextStyle.copy(
fontSize = 10.sp,
color = JewelTheme.globalColors.text.info.copy(alpha = 0.5f)
)
)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

import cc.unitmesh.devins.idea.renderer.markdown.JewelMarkdownRenderer
import cc.unitmesh.devins.idea.renderer.sketch.chart.IdeaChartRenderer
import cc.unitmesh.devins.parser.CodeFence
import com.intellij.openapi.Disposable
import com.intellij.openapi.project.Project
Expand Down Expand Up @@ -135,6 +136,16 @@ object IdeaSketchRenderer {
}
}

"chart", "graph" -> {
if (fence.text.isNotBlank()) {
IdeaChartRenderer(
chartCode = fence.text,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
}
}

"devin" -> {
if (fence.text.isNotBlank()) {
IdeaDevInBlockRenderer(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package cc.unitmesh.devins.idea.renderer.sketch.chart

/**
* Chart type enumeration
*/
enum class ChartType {
PIE,
LINE,
COLUMN,
ROW
}

/**
* Base chart configuration
*/
data class ChartConfig(
val type: ChartType,
val title: String? = null,
val width: Int? = null,
val height: Int? = null,
val data: ChartDataContent
)

/**
* Chart data content - polymorphic based on chart type
*/
sealed class ChartDataContent {
data class PieData(
val items: List<PieItem>,
val style: PieStyle = PieStyle.FILL
) : ChartDataContent()

data class LineData(
val lines: List<LineItem>,
val showDots: Boolean = true,
val curvedEdges: Boolean = true,
val minValue: Double? = null,
val maxValue: Double? = null
) : ChartDataContent()

data class ColumnData(
val bars: List<BarGroup>,
val minValue: Double? = null,
val maxValue: Double? = null
) : ChartDataContent()

data class RowData(
val bars: List<BarGroup>,
val minValue: Double? = null,
val maxValue: Double? = null
) : ChartDataContent()
}

data class PieItem(
val label: String,
val value: Double,
val color: String? = null
)

enum class PieStyle {
FILL,
STROKE
}

data class LineItem(
val label: String,
val values: List<Double>,
val color: String? = null
)

data class BarGroup(
val label: String,
val values: List<BarValue>
)

data class BarValue(
val label: String? = null,
val value: Double,
val color: String? = null
)

Loading
Loading