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