Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,22 @@ interface CodingAgentRenderer {

fun updateTokenInfo(tokenInfo: TokenInfo) {}

/**
* Handle task-boundary tool call to update task progress display.
* Called when the agent uses the task-boundary tool to mark task status.
*
* This is an optional method primarily used by UI renderers that display
* task progress visually. Console and server renderers typically don't need
* to implement this.
*
* @param taskName The name of the task
* @param status The task status (e.g., "WORKING", "DONE", "FAILED")
* @param summary Optional summary of the task progress
*/
fun handleTaskBoundary(taskName: String, status: String, summary: String = "") {
// Default: no-op for renderers that don't display task progress
}

/**
* Render a compact plan summary bar.
* Called when plan is created or updated to show progress in a compact format.
Expand Down
2 changes: 2 additions & 0 deletions mpp-idea/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,8 @@ project(":") {
exclude(group = "cc.unitmesh.viewer.web", module = "mpp-viewer-web")
exclude(group = "cc.unitmesh", module = "mpp-viewer-web")
}

testImplementation(kotlin("test"))
}

tasks {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import cc.unitmesh.agent.render.BaseRenderer
import cc.unitmesh.devins.llm.MessageRole
import cc.unitmesh.agent.render.RendererUtils

import cc.unitmesh.agent.render.TaskInfo
import cc.unitmesh.agent.render.TaskStatus
import cc.unitmesh.agent.render.TimelineItem
import cc.unitmesh.agent.render.ToolCallDisplayInfo
import cc.unitmesh.agent.render.ToolCallInfo
Expand Down Expand Up @@ -82,6 +84,10 @@ class JewelRenderer : BaseRenderer() {
private val _currentPlan = MutableStateFlow<AgentPlan?>(null)
val currentPlan: StateFlow<AgentPlan?> = _currentPlan.asStateFlow()

// Task tracking (from task-boundary tool)
private val _tasks = MutableStateFlow<List<TaskInfo>>(emptyList())
val tasks: StateFlow<List<TaskInfo>> = _tasks.asStateFlow()

/**
* Set the current plan directly.
* Used to sync with PlanStateService from CodingAgent.
Expand Down Expand Up @@ -152,6 +158,11 @@ class JewelRenderer : BaseRenderer() {

jewelRendererLogger.info("renderToolCall: parsed params keys=${params.keys}")

// Handle task-boundary tool - update task list
if (toolName == "task-boundary") {
updateTaskFromToolCall(params)
}

Comment on lines +161 to +165
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all task-boundary references in Kotlin renderer files
echo "=== Task-boundary references ==="
rg -n "task-boundary" --type=kt

echo -e "\n=== Looking for all Renderer implementations ==="
# Find all *Renderer.kt files to identify the implementations
fd "Renderer\.kt$" --type f

echo -e "\n=== Searching for CodingAgentRenderer interface/class ==="
rg -n "class.*CodingAgentRenderer|interface.*CodingAgentRenderer" --type=kt -A 5

Repository: phodal/auto-dev

Length of output: 4405


🏁 Script executed:

#!/bin/bash
# Search for task-boundary without file type filter
echo "=== Task-boundary references ==="
rg "task-boundary" -C 3

echo -e "\n=== CodingAgentRenderer interface ==="
cat mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/render/CodingAgentRenderer.kt

echo -e "\n=== Check DefaultCodingAgentRenderer for task-boundary ==="
rg "task-boundary" mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/render/DefaultCodingAgentRenderer.kt -C 3 || echo "Not found in DefaultCodingAgentRenderer"

echo -e "\n=== Check ComposeRenderer for task-boundary ==="
rg "task-boundary" mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/agent/ComposeRenderer.kt -C 3 || echo "Not found in ComposeRenderer"

echo -e "\n=== Check ServerSideRenderer for task-boundary ==="
rg "task-boundary" mpp-server/src/main/kotlin/cc/unitmesh/server/render/ServerSideRenderer.kt -C 3 || echo "Not found in ServerSideRenderer"

echo -e "\n=== Check JewelRenderer for task-boundary ==="
rg "task-boundary" mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/JewelRenderer.kt -C 3

Repository: phodal/auto-dev

Length of output: 13916


🏁 Script executed:

#!/bin/bash
echo "=== DefaultCodingAgentRenderer full implementation ==="
cat mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/render/DefaultCodingAgentRenderer.kt

echo -e "\n=== ServerSideRenderer full implementation ==="
wc -l mpp-server/src/main/kotlin/cc/unitmesh/server/render/ServerSideRenderer.kt
head -100 mpp-server/src/main/kotlin/cc/unitmesh/server/render/ServerSideRenderer.kt

echo -e "\n=== Search for JsRendererAdapter ==="
fd "JsRendererAdapter" --type f
rg "JsRendererAdapter" -l

Repository: phodal/auto-dev

Length of output: 7681


🏁 Script executed:

#!/bin/bash
echo "=== BaseRenderer implementation ==="
cat mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/render/BaseRenderer.kt

echo -e "\n=== RendererExports.kt (JS adapter) ==="
cat mpp-core/src/jsMain/kotlin/cc/unitmesh/agent/RendererExports.kt

echo -e "\n=== Check if task-boundary is in BaseRenderer ==="
rg "task-boundary" mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/render/BaseRenderer.kt || echo "Not found"

Repository: phodal/auto-dev

Length of output: 9537


Task-boundary handling is inconsistent across CodingAgentRenderer implementations

Only ComposeRenderer and JewelRenderer handle the task-boundary tool in their renderToolCall methods. The other three implementations—DefaultCodingAgentRenderer, ServerSideRenderer, and JsRendererAdapter—lack this handler. Since CodingAgentRenderer does not define task-boundary as a required interface method, either all implementations should add consistent task-boundary handling, or it should be removed from the two that currently have it to avoid platform discrepancies.

🤖 Prompt for AI Agents
In mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/JewelRenderer.kt
around lines 161-165 the file contains a task-boundary handler
(updateTaskFromToolCall(params)) but other renderer implementations lack it;
make handling consistent by adding a common hook in the CodingAgentRenderer
interface and wiring it into all renderers: add a default fun
handleTaskBoundary(params: Map<String, Any?>) = Unit to CodingAgentRenderer (or
an abstract method if you want it required), ensure updateTaskFromToolCall is
refactored into a shared/internal helper accessible from all renderers (or call
that helper from the new interface method), then call handleTaskBoundary(params)
from each renderer’s renderToolCall (update DefaultCodingAgentRenderer,
ServerSideRenderer, JsRendererAdapter, and keep ComposeRenderer/JewelRenderer
implementations delegating to the new method); alternatively, if you prefer not
to support task-boundary, remove the handling from ComposeRenderer and
JewelRenderer instead—but pick one approach and apply it across all renderer
implementations for consistency.

// Handle plan management tool - update plan state
if (toolName == "plan") {
jewelRendererLogger.info("renderToolCall: detected plan tool, calling updatePlanFromToolCall")
Expand All @@ -178,6 +189,11 @@ class JewelRenderer : BaseRenderer() {
// Convert Map<String, Any> to Map<String, String> for internal use
val stringParams = params.mapValues { it.value.toString() }

// Handle task-boundary tool - update task list
if (toolName == "task-boundary") {
updateTaskFromToolCall(stringParams)
}

// Handle plan management tool - update plan state with original params
if (toolName == "plan") {
updatePlanFromToolCallWithAnyParams(params)
Expand Down Expand Up @@ -322,6 +338,49 @@ class JewelRenderer : BaseRenderer() {
}
}

/**
* Update task list from task-boundary tool call
*/
private fun updateTaskFromToolCall(params: Map<String, String>) {
val taskName = params["taskName"] ?: return
val statusStr = params["status"] ?: "WORKING"
val summary = params["summary"] ?: ""
val status = TaskStatus.fromString(statusStr)

_tasks.update { currentTasks ->
val existingIndex = currentTasks.indexOfFirst { it.taskName == taskName }
if (existingIndex >= 0) {
// Update existing task
currentTasks.toMutableList().apply {
this[existingIndex] = currentTasks[existingIndex].copy(
status = status,
summary = summary,
timestamp = System.currentTimeMillis()
)
}
} else {
// Add new task
currentTasks + TaskInfo(
taskName = taskName,
status = status,
summary = summary
)
}
}
}

/**
* Handle task-boundary tool call to update task progress display.
* Overrides the interface method to provide UI-specific task tracking.
*/
override fun handleTaskBoundary(taskName: String, status: String, summary: String) {
updateTaskFromToolCall(mapOf(
"taskName" to taskName,
"status" to status,
"summary" to summary
))
}

override fun renderToolResult(
toolName: String,
success: Boolean,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package cc.unitmesh.devins.idea.renderer

import cc.unitmesh.devins.llm.MessageRole
import cc.unitmesh.agent.render.TaskStatus
import cc.unitmesh.agent.render.TimelineItem
import cc.unitmesh.llm.compression.TokenInfo
import kotlinx.coroutines.flow.first
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,18 @@ class ComposeRenderer : BaseRenderer() {
}
}

/**
* Handle task-boundary tool call to update task progress display.
* Overrides the interface method to provide UI-specific task tracking.
*/
override fun handleTaskBoundary(taskName: String, status: String, summary: String) {
updateTaskFromToolCall(mapOf(
"taskName" to taskName,
"status" to status,
"summary" to summary
))
}

/**
* Update plan state from plan management tool call (string params version)
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package cc.unitmesh.devins.ui.nano

import cc.unitmesh.xuiper.action.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.awt.Desktop
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import javax.swing.JOptionPane

/**
* Compose/Desktop implementation of NanoActionHandler
*
* Handles NanoUI actions in a Compose Desktop environment.
* Provides platform-specific implementations for navigation, toast, and fetch.
*
* Example:
* ```kotlin
* val handler = ComposeActionHandler(
* scope = rememberCoroutineScope(),
* onNavigate = { route -> navController.navigate(route) },
* onToast = { message -> snackbarHostState.showSnackbar(message) }
* )
*
* handler.registerCustomAction("AddTask") { payload, context ->
* val title = payload["title"] as? String ?: ""
* taskRepository.add(Task(title))
* ActionResult.Success
* }
* ```
*/
class ComposeActionHandler(
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO),
private val onNavigate: ((String) -> Unit)? = null,
private val onToast: ((String) -> Unit)? = null,
private val onFetchComplete: ((String, Boolean, String?) -> Unit)? = null
) : BaseNanoActionHandler() {

private val httpClient = HttpClient.newBuilder().build()

override fun handleNavigate(
navigate: NanoAction.Navigate,
context: NanoActionContext
): ActionResult {
return try {
if (onNavigate != null) {
onNavigate.invoke(navigate.to)
} else {
// Default: open in browser if it's a URL
if (navigate.to.startsWith("http://") || navigate.to.startsWith("https://")) {
Desktop.getDesktop().browse(URI(navigate.to))
Comment on lines +53 to +54
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Insufficient URL validation: The navigation handler only checks if the URL starts with "http://" or "https://" using string prefix matching (line 53). This is insufficient because:

  1. It doesn't validate URL structure (e.g., http://[invalid] would pass)
  2. It allows navigation to any external domain without restriction
  3. URLs without these prefixes are silently ignored, which could confuse users

Recommendation: Use proper URL parsing and validation:

try {
    val uri = URI(navigate.to)
    if (uri.scheme in listOf("http", "https")) {
        Desktop.getDesktop().browse(uri)
    } else {
        return ActionResult.Error("Unsupported URL scheme: ${uri.scheme}")
    }
} catch (e: URISyntaxException) {
    return ActionResult.Error("Invalid URL: ${navigate.to}", e)
}
Suggested change
if (navigate.to.startsWith("http://") || navigate.to.startsWith("https://")) {
Desktop.getDesktop().browse(URI(navigate.to))
try {
val uri = URI(navigate.to)
if (uri.scheme == "http" || uri.scheme == "https") {
Desktop.getDesktop().browse(uri)
} else {
return ActionResult.Error("Unsupported URL scheme: ${uri.scheme}")
}
} catch (e: Exception) {
return ActionResult.Error("Invalid URL: ${navigate.to}", e)

Copilot uses AI. Check for mistakes.
}
}
ActionResult.Success
} catch (e: Exception) {
ActionResult.Error("Navigation failed: ${e.message}", e)
}
}

override fun handleFetch(
fetch: NanoAction.Fetch,
context: NanoActionContext
): ActionResult {
// Set loading state if specified
fetch.loadingState?.let { path ->
context.set(path, true)
}

scope.launch {
try {
val requestBuilder = HttpRequest.newBuilder()
.uri(URI(fetch.url))

// Set method
when (fetch.method) {
HttpMethod.GET -> requestBuilder.GET()
HttpMethod.POST -> {
val body = buildRequestBody(fetch.body, context)
requestBuilder.POST(HttpRequest.BodyPublishers.ofString(body))
requestBuilder.header("Content-Type", fetch.contentType.mimeType)
}
HttpMethod.PUT -> {
val body = buildRequestBody(fetch.body, context)
requestBuilder.PUT(HttpRequest.BodyPublishers.ofString(body))
requestBuilder.header("Content-Type", fetch.contentType.mimeType)
}
HttpMethod.DELETE -> requestBuilder.DELETE()
else -> requestBuilder.GET()
}

// Add headers
fetch.headers?.forEach { (key, value) ->
requestBuilder.header(key, value)
}

val response = httpClient.send(
requestBuilder.build(),
HttpResponse.BodyHandlers.ofString()
)

// Update loading state
fetch.loadingState?.let { path ->
context.set(path, false)
}

if (response.statusCode() in 200..299) {
// Success
fetch.responseBinding?.let { path ->
context.set(path, response.body())
}

fetch.onSuccess?.let { successAction ->
handleAction(successAction, context)
}

onFetchComplete?.invoke(fetch.url, true, response.body())
} else {
// Error
val errorMsg = "HTTP ${response.statusCode()}: ${response.body()}"
fetch.errorBinding?.let { path ->
context.set(path, errorMsg)
}

fetch.onError?.let { errorAction ->
handleAction(errorAction, context)
}

onFetchComplete?.invoke(fetch.url, false, errorMsg)
}

} catch (e: Exception) {
fetch.loadingState?.let { path ->
context.set(path, false)
}
fetch.errorBinding?.let { path ->
context.set(path, e.message)
}
fetch.onError?.let { errorAction ->
handleAction(errorAction, context)
}
onFetchComplete?.invoke(fetch.url, false, e.message)
}
}

return ActionResult.Pending { /* async operation */ }
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ActionResult.Pending is returned with an empty callback (line 148: ActionResult.Pending { /* async operation */ }), but the callback is never invoked when the async fetch operation completes. This means code waiting on the Pending result will never be notified of completion.

Either:

  1. Store the callback and invoke it when the async operation completes (success or error), or
  2. If async completion notification isn't needed, document that Pending results don't support completion callbacks in this implementation

The same issue exists in the base class where Pending results aren't being properly tracked.

Copilot uses AI. Check for mistakes.
}

override fun handleShowToast(
toast: NanoAction.ShowToast,
context: NanoActionContext
): ActionResult {
return try {
if (onToast != null) {
onToast.invoke(toast.message)
} else {
// Default: use Swing dialog (for desktop)
javax.swing.SwingUtilities.invokeLater {
JOptionPane.showMessageDialog(null, toast.message)
}
}
ActionResult.Success
} catch (e: Exception) {
ActionResult.Error("Toast failed: ${e.message}", e)
}
}

private fun buildRequestBody(
body: Map<String, BodyField>?,
context: NanoActionContext
): String {
if (body == null) return ""

val resolvedBody = body.mapValues { (_, field) ->
when (field) {
is BodyField.Literal -> field.value
is BodyField.StateBinding -> context.get(field.path)?.toString() ?: ""
}
}

// Simple JSON serialization with proper escaping
return buildString {
append("{")
resolvedBody.entries.forEachIndexed { index, (key, value) ->
if (index > 0) append(",")
append("\"$key\":")
when (value) {
is String -> append("\"${value.replace("\\", "\\\\").replace("\"", "\\\"")}\"")
is Number, is Boolean -> append(value)
null -> append("null")
else -> append("\"${value.toString().replace("\\", "\\\\").replace("\"", "\\\"")}\"")
}
}
append("}")
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ fun NanoDSLDemo(
.verticalScroll(rememberScrollState())
) {
parsedIR?.let { ir ->
ComposeNanoRenderer.Render(ir)
StatefulNanoRenderer.Render(ir)
} ?: run {
Box(
modifier = Modifier.fillMaxSize(),
Expand Down Expand Up @@ -305,13 +305,18 @@ component ShoppingItem:

private val COUNTER_DSL = """
component Counter:
state:
count: int = 0

Card(padding="lg", shadow="md"):
VStack(spacing="md"):
Text("Counter Example", style="h2")
HStack(spacing="md", align="center", justify="center"):
Button("-", intent="secondary")
Text("0", style="h1")
Button("+", intent="primary")
Button("-", intent="secondary"):
on_click: state.count -= 1
Text(content << state.count, style="h1")
Button("+", intent="primary"):
on_click: state.count += 1
Divider
Text("Click buttons to change value", style="caption")
""".trimIndent()
Expand Down
Loading
Loading