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
@@ -0,0 +1,245 @@
package cc.unitmesh.devins.idea.renderer.sketch

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import cc.unitmesh.agent.parser.ToolCallParser
import cc.unitmesh.devins.idea.toolwindow.IdeaComposeIcons
import cc.unitmesh.devins.ui.compose.theme.AutoDevColors
import kotlinx.serialization.json.Json
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.component.Icon
import org.jetbrains.jewel.ui.component.Text

/**
* Reusable Json instance with pretty print configuration
*/
private val PrettyJson = Json { prettyPrint = true }

/**
* DevIn Block Renderer for IntelliJ IDEA with Jewel styling.
*
* Parses devin blocks (language id = "devin") and renders them as tool call items
* when the block is complete. Similar to DevInBlockRenderer in mpp-ui but using
* Jewel theming.
*/
@Composable
fun IdeaDevInBlockRenderer(
devinContent: String,
isComplete: Boolean,
modifier: Modifier = Modifier
) {
Column(modifier = modifier) {
if (isComplete) {
// Parse the devin block to extract tool calls
val parser = remember { ToolCallParser() }
val wrappedContent = "<devin>\n$devinContent\n</devin>"
val toolCalls = remember(devinContent) { parser.parseToolCalls(wrappedContent) }

if (toolCalls.isNotEmpty()) {
toolCalls.forEach { toolCall ->
val toolName = toolCall.toolName
val params = toolCall.params

// Format details string (for display)
val details = formatToolCallDetails(params)

IdeaDevInToolItem(
toolName = toolName,
details = details,
params = params,
modifier = Modifier.fillMaxWidth()
)

Spacer(modifier = Modifier.height(8.dp))
}
} else {
// If no tool calls found, render as code block
IdeaCodeBlockRenderer(
code = devinContent,
language = "devin",
modifier = Modifier.fillMaxWidth()
)
}
} else {
// If not complete, show as code block (streaming)
IdeaCodeBlockRenderer(
code = devinContent,
language = "devin",
modifier = Modifier.fillMaxWidth()
)
}
}
}

/**
* Tool item display for DevIn block parsing results.
* Shows tool name, type icon, and parameters in a compact expandable format.
*/
@Composable
private fun IdeaDevInToolItem(
toolName: String,
details: String,
params: Map<String, String>,
modifier: Modifier = Modifier
) {
var expanded by remember { mutableStateOf(false) }
val hasParams = params.isNotEmpty()

Box(
modifier = modifier
.background(
color = JewelTheme.globalColors.panelBackground.copy(alpha = 0.5f),
shape = RoundedCornerShape(4.dp)
)
.padding(8.dp)
) {
Column {
// Header row: Tool icon + Tool name + Details
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) { if (hasParams) expanded = !expanded },
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
// Tool type icon
Icon(
imageVector = IdeaComposeIcons.Build,
contentDescription = "Tool",
modifier = Modifier.size(16.dp),
tint = AutoDevColors.Blue.c400
)

// Tool name
Text(
text = toolName,
style = JewelTheme.defaultTextStyle.copy(
fontWeight = FontWeight.Bold,
fontSize = 12.sp
)
)

// Details (truncated parameters)
if (details.isNotEmpty() && !expanded) {
Text(
text = details.take(60) + if (details.length > 60) "..." else "",
style = JewelTheme.defaultTextStyle.copy(
fontSize = 11.sp,
color = JewelTheme.globalColors.text.info.copy(alpha = 0.7f)
),
modifier = Modifier.weight(1f),
maxLines = 1
)
} else {
Spacer(modifier = Modifier.weight(1f))
}

// Expand/collapse icon
if (hasParams) {
Icon(
imageVector = if (expanded) IdeaComposeIcons.ExpandLess else IdeaComposeIcons.ExpandMore,
contentDescription = if (expanded) "Collapse" else "Expand",
modifier = Modifier.size(16.dp),
tint = JewelTheme.globalColors.text.normal.copy(alpha = 0.7f)
)
}
}

// Expanded parameters section
if (expanded && hasParams) {
Spacer(modifier = Modifier.height(8.dp))

Box(
modifier = Modifier
.fillMaxWidth()
.background(
color = JewelTheme.globalColors.panelBackground.copy(alpha = 0.3f),
shape = RoundedCornerShape(4.dp)
)
.padding(8.dp)
) {
Text(
text = formatParamsAsJson(params),
style = JewelTheme.defaultTextStyle.copy(
fontSize = 11.sp,
fontFamily = FontFamily.Monospace
)
)
}
}
}
}
}

/**
* Format tool call parameters as a human-readable details string
*/
private fun formatToolCallDetails(params: Map<String, String>): String {
return params.entries.joinToString(", ") { (key, value) ->
"$key=${truncateValue(value)}"
}
}

/**
* Truncate long values for display
*/
private fun truncateValue(value: String, maxLength: Int = 100): String {
return if (value.length > maxLength) {
value.take(maxLength) + "..."
} else {
value
}
}

/**
* Format parameters as JSON string for full display
*/
private fun formatParamsAsJson(params: Map<String, String>): String {
return try {
PrettyJson.encodeToString(
kotlinx.serialization.serializer(),
params
)
} catch (e: Exception) {
// Fallback to manual formatting with proper JSON escaping
buildString {
appendLine("{")
params.entries.forEachIndexed { index, (key, value) ->
append(" \"${escapeJsonString(key)}\": ")
append("\"${escapeJsonString(value)}\"")
if (index < params.size - 1) {
appendLine(",")
} else {
appendLine()
}
}
append("}")
}
}
}
Comment on lines +210 to +232
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix incorrect serializer usage.

Lines 212-215 call kotlinx.serialization.serializer() without a type parameter, which will fail at runtime. The encodeToString function with an explicit serializer parameter expects the serializer to match the value type.

Apply this diff to fix the serialization:

 private fun formatParamsAsJson(params: Map<String, String>): String {
     return try {
-        PrettyJson.encodeToString(
-            kotlinx.serialization.serializer(),
-            params
-        )
+        // Use the inline reified version which infers the serializer
+        PrettyJson.encodeToString(params)
     } catch (e: Exception) {
         // Fallback to manual formatting with proper JSON escaping
         buildString {

Alternatively, if an explicit serializer is needed:

+import kotlinx.serialization.builtins.MapSerializer
+import kotlinx.serialization.builtins.serializer
+
 private fun formatParamsAsJson(params: Map<String, String>): String {
     return try {
         PrettyJson.encodeToString(
-            kotlinx.serialization.serializer(),
+            MapSerializer(String.serializer(), String.serializer()),
             params
         )
📝 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
private fun formatParamsAsJson(params: Map<String, String>): String {
return try {
PrettyJson.encodeToString(
kotlinx.serialization.serializer(),
params
)
} catch (e: Exception) {
// Fallback to manual formatting with proper JSON escaping
buildString {
appendLine("{")
params.entries.forEachIndexed { index, (key, value) ->
append(" \"${escapeJsonString(key)}\": ")
append("\"${escapeJsonString(value)}\"")
if (index < params.size - 1) {
appendLine(",")
} else {
appendLine()
}
}
append("}")
}
}
}
private fun formatParamsAsJson(params: Map<String, String>): String {
return try {
// Use the inline reified version which infers the serializer
PrettyJson.encodeToString(params)
} catch (e: Exception) {
// Fallback to manual formatting with proper JSON escaping
buildString {
appendLine("{")
params.entries.forEachIndexed { index, (key, value) ->
append(" \"${escapeJsonString(key)}\": ")
append("\"${escapeJsonString(value)}\"")
if (index < params.size - 1) {
appendLine(",")
} else {
appendLine()
}
}
append("}")
}
}
}
🤖 Prompt for AI Agents
In
mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/sketch/IdeaDevInBlockRenderer.kt
around lines 210 to 232, the call to kotlinx.serialization.serializer() is
untyped and will fail at runtime; replace it with an explicit Map<String,
String> serializer and pass that to PrettyJson.encodeToString (for example use
MapSerializer(String.serializer(), String.serializer()) or the equivalent typed
serializer for Map<String, String>), keeping the same params argument and
imports for MapSerializer and String.serializer(); keep the existing catch
fallback unchanged.


/**
* Escape special characters for valid JSON string
*/
private fun escapeJsonString(value: String): String {
return value
.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t")
}

Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import org.jetbrains.jewel.ui.component.CircularProgressIndicator
* - Thinking -> IdeaThinkingBlockRenderer
* - Walkthrough -> IdeaWalkthroughBlockRenderer
* - Mermaid -> MermaidDiagramView
* - DevIn -> IdeaDevInBlockRenderer
*/
object IdeaSketchRenderer {

Expand Down Expand Up @@ -98,6 +99,17 @@ object IdeaSketchRenderer {
}
}

"devin" -> {
if (fence.text.isNotBlank()) {
IdeaDevInBlockRenderer(
devinContent = fence.text,
isComplete = blockIsComplete,
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
}
}

else -> {
if (fence.text.isNotBlank()) {
IdeaCodeBlockRenderer(
Expand Down
Loading