Skip to content

Commit 1cdcf65

Browse files
committed
fix(nano): Strip quotes from state variable default string values
The parser was keeping the raw quotes around string default values, causing empty strings like to be rendered literally as "" in the UI. Changes: - Update parseStateBlock() to strip surrounding quotes from string values - Add tests for state variable parsing with empty and non-empty strings - Verify IR conversion preserves the unquoted string values This fixes the LoginForm rendering issue where email and password inputs showed "" instead of being empty.
1 parent 236ab0b commit 1cdcf65

File tree

4 files changed

+130
-1
lines changed

4 files changed

+130
-1
lines changed

mpp-idea/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,10 @@ project(":") {
346346
}
347347

348348
testImplementation(kotlin("test"))
349+
testImplementation("cc.unitmesh:mpp-core:${prop("mppVersion")}") {
350+
exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-coroutines-core")
351+
exclude(group = "org.jetbrains.kotlinx", module = "kotlinx-coroutines-core-jvm")
352+
}
349353
}
350354

351355
tasks {

mpp-idea/src/main/kotlin/cc/unitmesh/devins/idea/renderer/JewelRenderer.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import cc.unitmesh.agent.render.BaseRenderer
66
import cc.unitmesh.devins.llm.MessageRole
77
import cc.unitmesh.agent.render.RendererUtils
88

9+
import cc.unitmesh.agent.render.TaskInfo
10+
import cc.unitmesh.agent.render.TaskStatus
911
import cc.unitmesh.agent.render.TimelineItem
1012
import cc.unitmesh.agent.render.ToolCallDisplayInfo
1113
import cc.unitmesh.agent.render.ToolCallInfo
@@ -82,6 +84,10 @@ class JewelRenderer : BaseRenderer() {
8284
private val _currentPlan = MutableStateFlow<AgentPlan?>(null)
8385
val currentPlan: StateFlow<AgentPlan?> = _currentPlan.asStateFlow()
8486

87+
// Task tracking (from task-boundary tool)
88+
private val _tasks = MutableStateFlow<List<TaskInfo>>(emptyList())
89+
val tasks: StateFlow<List<TaskInfo>> = _tasks.asStateFlow()
90+
8591
/**
8692
* Set the current plan directly.
8793
* Used to sync with PlanStateService from CodingAgent.
@@ -152,6 +158,11 @@ class JewelRenderer : BaseRenderer() {
152158

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

161+
// Handle task-boundary tool - update task list
162+
if (toolName == "task-boundary") {
163+
updateTaskFromToolCall(params)
164+
}
165+
155166
// Handle plan management tool - update plan state
156167
if (toolName == "plan") {
157168
jewelRendererLogger.info("renderToolCall: detected plan tool, calling updatePlanFromToolCall")
@@ -178,6 +189,11 @@ class JewelRenderer : BaseRenderer() {
178189
// Convert Map<String, Any> to Map<String, String> for internal use
179190
val stringParams = params.mapValues { it.value.toString() }
180191

192+
// Handle task-boundary tool - update task list
193+
if (toolName == "task-boundary") {
194+
updateTaskFromToolCall(stringParams)
195+
}
196+
181197
// Handle plan management tool - update plan state with original params
182198
if (toolName == "plan") {
183199
updatePlanFromToolCallWithAnyParams(params)
@@ -322,6 +338,37 @@ class JewelRenderer : BaseRenderer() {
322338
}
323339
}
324340

341+
/**
342+
* Update task list from task-boundary tool call
343+
*/
344+
private fun updateTaskFromToolCall(params: Map<String, String>) {
345+
val taskName = params["taskName"] ?: return
346+
val statusStr = params["status"] ?: "WORKING"
347+
val summary = params["summary"] ?: ""
348+
val status = TaskStatus.fromString(statusStr)
349+
350+
_tasks.update { currentTasks ->
351+
val existingIndex = currentTasks.indexOfFirst { it.taskName == taskName }
352+
if (existingIndex >= 0) {
353+
// Update existing task
354+
currentTasks.toMutableList().apply {
355+
this[existingIndex] = currentTasks[existingIndex].copy(
356+
status = status,
357+
summary = summary,
358+
timestamp = System.currentTimeMillis()
359+
)
360+
}
361+
} else {
362+
// Add new task
363+
currentTasks + TaskInfo(
364+
taskName = taskName,
365+
status = status,
366+
summary = summary
367+
)
368+
}
369+
}
370+
}
371+
325372
override fun renderToolResult(
326373
toolName: String,
327374
success: Boolean,

xuiper-ui/src/main/kotlin/cc/unitmesh/xuiper/parser/IndentParser.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,11 +194,18 @@ class IndentParser(
194194

195195
val match = STATE_VAR_REGEX.matchEntire(line)
196196
if (match != null) {
197+
val rawValue = match.groupValues[3].trim()
198+
// Strip quotes from string values (e.g., "" -> empty string, "hello" -> hello)
199+
val defaultValue = if (rawValue.startsWith("\"") && rawValue.endsWith("\"")) {
200+
rawValue.substring(1, rawValue.length - 1)
201+
} else {
202+
rawValue
203+
}
197204
variables.add(
198205
NanoNode.StateVariable(
199206
name = match.groupValues[1],
200207
type = match.groupValues[2],
201-
defaultValue = match.groupValues[3].trim()
208+
defaultValue = defaultValue
202209
)
203210
)
204211
}

xuiper-ui/src/test/kotlin/cc/unitmesh/xuiper/dsl/NanoDSLParserTest.kt

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package cc.unitmesh.xuiper.dsl
33
import cc.unitmesh.xuiper.action.HttpMethod
44
import cc.unitmesh.xuiper.action.NanoAction
55
import cc.unitmesh.xuiper.ast.NanoNode
6+
import kotlinx.serialization.json.jsonPrimitive
67
import org.junit.jupiter.api.Test
78
import kotlin.test.assertEquals
89
import kotlin.test.assertNotNull
@@ -390,4 +391,74 @@ component ContactForm:
390391
assertEquals("/api/contact", fetchAction!!.url)
391392
assertEquals(HttpMethod.POST, fetchAction.method)
392393
}
394+
395+
@Test
396+
fun `should parse state variables with empty string default values correctly`() {
397+
val source = """
398+
component LoginForm:
399+
state:
400+
email: str = ""
401+
password: str = ""
402+
username: str = "default"
403+
loading: bool = False
404+
count: int = 0
405+
406+
VStack:
407+
Text("Login")
408+
""".trimIndent()
409+
410+
val result = NanoDSL.parse(source)
411+
412+
// Check state was parsed
413+
assertNotNull(result.state)
414+
val state = result.state!!
415+
416+
// Check that empty string default values are correctly stripped of quotes
417+
val emailVar = state.variables.find { it.name == "email" }
418+
assertNotNull(emailVar)
419+
assertEquals("str", emailVar!!.type)
420+
assertEquals("", emailVar.defaultValue, "Empty string should have quotes stripped")
421+
422+
val passwordVar = state.variables.find { it.name == "password" }
423+
assertNotNull(passwordVar)
424+
assertEquals("", passwordVar!!.defaultValue, "Empty string should have quotes stripped")
425+
426+
// Check that non-empty string values are also correctly stripped
427+
val usernameVar = state.variables.find { it.name == "username" }
428+
assertNotNull(usernameVar)
429+
assertEquals("default", usernameVar!!.defaultValue, "String value should have quotes stripped")
430+
431+
// Check that non-string values are preserved as-is
432+
val loadingVar = state.variables.find { it.name == "loading" }
433+
assertNotNull(loadingVar)
434+
assertEquals("False", loadingVar!!.defaultValue)
435+
436+
val countVar = state.variables.find { it.name == "count" }
437+
assertNotNull(countVar)
438+
assertEquals("0", countVar!!.defaultValue)
439+
}
440+
441+
@Test
442+
fun `should render state with empty string correctly in IR`() {
443+
val source = """
444+
component LoginForm:
445+
state:
446+
email: str = ""
447+
password: str = ""
448+
449+
Card:
450+
Input(value:=state.email, placeholder="Email")
451+
""".trimIndent()
452+
453+
val result = NanoDSL.parse(source)
454+
val ir = NanoDSL.toIR(result)
455+
456+
// Check state in IR
457+
assertNotNull(ir.state)
458+
val emailState = ir.state!!.variables["email"]
459+
assertNotNull(emailState)
460+
assertEquals("str", emailState!!.type)
461+
// The default value should be an empty string, not ""
462+
assertEquals("", emailState.defaultValue?.jsonPrimitive?.content)
463+
}
393464
}

0 commit comments

Comments
 (0)