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.idea.theme.IdeaAutoDevColors
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 = "\n$devinContent\n"
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,
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 = IdeaAutoDevColors.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 {
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 {
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("}")
}
}
}
/**
* 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")
}