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") }