package cc.unitmesh.devins.ui.compose.sketch import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import cc.unitmesh.agent.parser.ToolCallParser import cc.unitmesh.agent.tool.ToolType import cc.unitmesh.devins.ui.compose.agent.ToolItem import cc.unitmesh.devins.workspace.WorkspaceManager import kotlinx.serialization.json.Json /** * DevIn Block Renderer - renders devin blocks as tool calls * * Parses devin blocks (language id = "devin") and renders them as CombinedToolItem * when the block is complete. */ @Composable fun DevInBlockRenderer( devinContent: String, isComplete: Boolean, modifier: Modifier = Modifier ) { Column(modifier = modifier) { if (isComplete) { // Parse the devin block to extract tool calls val parser = ToolCallParser() val wrappedContent = "\n$devinContent\n" val toolCalls = parser.parseToolCalls(wrappedContent) if (toolCalls.isNotEmpty()) { // Get workspace root path for resolving relative paths val workspaceRoot = WorkspaceManager.currentWorkspace?.rootPath toolCalls.forEach { toolCall -> // Extract details for rendering val toolName = toolCall.toolName val params = toolCall.params // Format details string (for display) val details = formatToolCallDetails(toolName, params) // Format full params as JSON (for expansion) val fullParams = formatParamsAsJson(params) // Extract file path if available (for ReadFile/WriteFile) // Resolve relative path to absolute path using workspace root val relativePath = params["path"] as? String val filePath = resolveAbsolutePath(relativePath, workspaceRoot) // Map tool name to ToolType val toolType = ToolType.fromName(toolName) ToolItem( toolName = toolName, details = details, fullParams = fullParams, filePath = filePath, toolType = toolType, success = null, // null means still executing or waiting summary = null, output = null, fullOutput = null, executionTimeMs = null ) Spacer(modifier = Modifier.height(8.dp)) } } else { // If no tool calls found, render as code block CodeBlockRenderer( code = devinContent, language = "devin", displayName = "DevIn" ) } } else { // If not complete, show as code block (streaming) CodeBlockRenderer( code = devinContent, language = "devin", displayName = "DevIn" ) } } } /** * Resolve relative path to absolute path using workspace root */ private fun resolveAbsolutePath(relativePath: String?, workspaceRoot: String?): String? { if (relativePath == null) return null if (workspaceRoot == null) return relativePath // If already an absolute path, return as-is if (relativePath.startsWith("/") || relativePath.matches(Regex("^[A-Za-z]:.*"))) { return relativePath } // Combine workspace root with relative path val separator = if (workspaceRoot.endsWith("/") || workspaceRoot.endsWith("\\")) "" else "/" return "$workspaceRoot$separator$relativePath" } /** * Format tool call parameters as a human-readable details string */ private fun formatToolCallDetails(toolName: String, 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 { Json { prettyPrint = true }.encodeToString( kotlinx.serialization.serializer(), params ) } catch (e: Exception) { // Fallback to manual formatting buildString { appendLine("{") params.entries.forEachIndexed { index, (key, value) -> append(" \"$key\": ") append("\"${value.replace("\"", "\\\"")}\"") if (index < params.size - 1) { appendLine(",") } else { appendLine() } } append("}") } } }