diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/sketch/IdeaDevInBlockRenderer.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/sketch/IdeaDevInBlockRenderer.kt new file mode 100644 index 0000000000..f0739b5348 --- /dev/null +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/sketch/IdeaDevInBlockRenderer.kt @@ -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 = "\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 = 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 { + 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") +} + diff --git a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/sketch/IdeaSketchRenderer.kt b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/sketch/IdeaSketchRenderer.kt index db517348d2..209768341d 100644 --- a/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/sketch/IdeaSketchRenderer.kt +++ b/mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/sketch/IdeaSketchRenderer.kt @@ -22,6 +22,7 @@ import org.jetbrains.jewel.ui.component.CircularProgressIndicator * - Thinking -> IdeaThinkingBlockRenderer * - Walkthrough -> IdeaWalkthroughBlockRenderer * - Mermaid -> MermaidDiagramView + * - DevIn -> IdeaDevInBlockRenderer */ object IdeaSketchRenderer { @@ -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(