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
2 changes: 1 addition & 1 deletion .idea/runConfigurations/RunIDEA.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package cc.unitmesh.agent.parser

import cc.unitmesh.agent.logging.getLogger

private val logger = getLogger("NanoDSLValidator.Android")

/**
* Android implementation of NanoDSLValidator.
*
* Attempts to use xuiper-ui's NanoParser if available via reflection,
* otherwise falls back to basic validation.
*
* Note: Android shares the JVM runtime but xuiper-ui may not be bundled
* in Android apps due to size concerns.
*/
actual class NanoDSLValidator actual constructor() {

private val fullParserAvailable: Boolean
private var nanoDSLClass: Class<*>? = null
private var validateMethod: java.lang.reflect.Method? = null
private var toJsonMethod: java.lang.reflect.Method? = null

init {
// Try to load xuiper-ui's NanoDSL class via reflection
fullParserAvailable = try {
nanoDSLClass = Class.forName("cc.unitmesh.xuiper.dsl.NanoDSL")
validateMethod = nanoDSLClass!!.getDeclaredMethod("validate", String::class.java)
toJsonMethod = nanoDSLClass!!.getDeclaredMethod("toJson", String::class.java, Boolean::class.javaPrimitiveType)
logger.info { "NanoDSL parser available via xuiper-ui" }
true
} catch (e: Exception) {
logger.debug { "NanoDSL parser not available: ${e.message}" }
false
}
}

actual fun validate(source: String): NanoDSLValidationResult {
if (source.isBlank()) {
return NanoDSLValidationResult(
isValid = false,
errors = listOf(ValidationError("Empty source code", 0))
)
}

// Try to use full parser if available
if (fullParserAvailable && validateMethod != null) {
return try {
val result = validateMethod!!.invoke(nanoDSLClass!!.kotlin.objectInstance, source)
convertValidationResult(result)
} catch (e: Exception) {
logger.warn(e) { "Full validation failed, falling back to basic validation" }
performBasicValidation(source)
}
}

return performBasicValidation(source)
}

actual fun parse(source: String): NanoDSLParseResult {
val validationResult = validate(source)
if (!validationResult.isValid) {
return NanoDSLParseResult.Failure(validationResult.errors)
}

// Try to use full parser if available
if (fullParserAvailable && toJsonMethod != null) {
return try {
val irJson = toJsonMethod!!.invoke(nanoDSLClass!!.kotlin.objectInstance, source, true) as String
NanoDSLParseResult.Success(irJson)
} catch (e: Exception) {
logger.warn(e) { "Full parsing failed: ${e.message}" }
val errorMessage = e.cause?.message ?: e.message ?: "Unknown parse error"
NanoDSLParseResult.Failure(listOf(ValidationError(errorMessage, 0)))
}
}

// Fallback: return a minimal IR wrapper
return NanoDSLParseResult.Success(createMinimalIR(source))
}

private fun convertValidationResult(result: Any): NanoDSLValidationResult {
return try {
val isValid = result::class.java.getMethod("isValid").invoke(result) as Boolean
val errorsField = result::class.java.getMethod("getErrors").invoke(result) as List<*>
val warningsField = result::class.java.getMethod("getWarnings").invoke(result) as List<*>

val errors = errorsField.mapNotNull { error ->
if (error == null) return@mapNotNull null
val message = error::class.java.getMethod("getMessage").invoke(error) as String
val line = error::class.java.getMethod("getLine").invoke(error) as Int
ValidationError(message, line)
}

val warnings = warningsField.mapNotNull { warning ->
if (warning == null) return@mapNotNull null
warning::class.java.getMethod("getMessage").invoke(warning) as String
}

NanoDSLValidationResult(isValid, errors, warnings)
} catch (e: Exception) {
logger.warn(e) { "Failed to convert validation result" }
NanoDSLValidationResult(isValid = true)
}
}

private fun performBasicValidation(source: String): NanoDSLValidationResult {
val errors = mutableListOf<ValidationError>()
val warnings = mutableListOf<String>()
val lines = source.lines()

// Check for component definition
val hasComponentDef = lines.any { it.trim().startsWith("component ") && it.trim().endsWith(":") }
if (!hasComponentDef) {
errors.add(ValidationError(
"Missing component definition. Expected 'component Name:'",
0,
"Add 'component YourComponentName:' at the start"
))
}

// Check indentation
for ((lineNum, line) in lines.withIndex()) {
if (line.isBlank()) continue

if (line.contains('\t')) {
errors.add(ValidationError(
"Tabs are not allowed. Use 4 spaces for indentation.",
lineNum,
"Replace tabs with 4 spaces"
))
}
}

// Check for unclosed blocks
var parenCount = 0
var braceCount = 0
for (line in lines) {
for (char in line) {
when (char) {
'(' -> parenCount++
')' -> parenCount--
'{' -> braceCount++
'}' -> braceCount--
}
}
}

if (parenCount != 0) {
errors.add(ValidationError("Unbalanced parentheses", 0))
}

if (braceCount != 0) {
errors.add(ValidationError("Unbalanced braces", 0))
}

return NanoDSLValidationResult(
isValid = errors.isEmpty(),
errors = errors,
warnings = warnings
)
}

private fun createMinimalIR(source: String): String {
val componentNameMatch = Regex("""component\s+(\w+):""").find(source)
val componentName = componentNameMatch?.groupValues?.get(1) ?: "UnknownComponent"

// Escape the source for JSON
val escapedSource = source
.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t")

return """{"type":"Component","props":{"name":"$componentName"},"source":"$escapedSource"}"""
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package cc.unitmesh.agent.parser

/**
* Platform-agnostic NanoDSL validation result
*/
data class NanoDSLValidationResult(
val isValid: Boolean,
val errors: List<ValidationError> = emptyList(),
val warnings: List<String> = emptyList()
)

/**
* Validation error with location information
*/
data class ValidationError(
val message: String,
val line: Int,
val suggestion: String? = null
)

/**
* Parse result for NanoDSL source code
*/
sealed class NanoDSLParseResult {
/**
* Successful parse with IR JSON representation
*/
data class Success(val irJson: String) : NanoDSLParseResult()

/**
* Failed parse with error details
*/
data class Failure(val errors: List<ValidationError>) : NanoDSLParseResult()

fun isSuccess(): Boolean = this is Success
fun getIrJsonOrNull(): String? = (this as? Success)?.irJson
}

/**
* NanoDSL validator interface - cross-platform
*
* JVM platforms perform full AST parsing via xuiper-ui's NanoParser.
* Non-JVM platforms perform lightweight syntax checks.
*
* Usage:
* ```kotlin
* val validator = NanoDSLValidator()
* val result = validator.validate(source)
* if (result.isValid) {
* val parseResult = validator.parse(source)
* // Use IR JSON for rendering
* }
* ```
*/
expect class NanoDSLValidator() {
/**
* Validate NanoDSL source code without full parsing.
*
* @param source The NanoDSL source code to validate
* @return Validation result with errors and warnings
*/
fun validate(source: String): NanoDSLValidationResult

/**
* Parse NanoDSL source code and convert to IR JSON.
*
* On JVM platforms, this performs full AST parsing and converts to NanoIR.
* On non-JVM platforms, this performs basic validation only and returns
* a simple wrapper or failure.
*
* @param source The NanoDSL source code to parse
* @return Parse result with IR JSON or errors
*/
fun parse(source: String): NanoDSLParseResult
}

Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,23 @@ interface CodingAgentRenderer {
// Default: return error for renderers that don't support async sessions
return ToolResult.Error("Async session not supported by this renderer")
}

/**
* Render generated NanoDSL UI code.
* Called when NanoDSLAgent generates UI code.
*
* JVM platforms (Compose, Jewel) can render a live UI preview using NanoIR.
* Non-JVM platforms (CLI, VSCode) show the source code with syntax highlighting.
*
* @param source The generated NanoDSL source code
* @param irJson Optional IR JSON for rendering (available on JVM platforms)
* @param metadata Additional metadata (componentName, attempts, isValid, etc.)
*/
fun renderNanoDSL(
source: String,
irJson: String? = null,
metadata: Map<String, String> = emptyMap()
) {
// Default: no-op for renderers that don't support NanoDSL rendering
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,36 @@ sealed class TimelineItem(
fun isSuccess(): Boolean = exitCode == 0
}

/**
* NanoDSL generated UI item - displays generated NanoDSL code with optional live preview.
*
* On JVM platforms with xuiper-ui available, the irJson can be used to render
* a live UI preview using NanoRenderer.
*
* On non-JVM platforms or when irJson is null, shows the source code only.
*/
data class NanoDSLItem(
/** The generated NanoDSL source code */
val source: String,
/** Optional IR JSON for rendering (JVM platforms only) */
val irJson: String? = null,
/** Component name extracted from source */
val componentName: String? = null,
/** Number of generation attempts (1 = first try succeeded) */
val generationAttempts: Int = 1,
/** Whether the generated code passed validation */
val isValid: Boolean = true,
/** Validation warnings (if any) */
val warnings: List<String> = emptyList(),
override val timestamp: Long = Platform.getCurrentTimestamp(),
override val id: String = generateId()
) : TimelineItem(timestamp, id) {
/**
* Check if live preview is available (requires IR JSON)
*/
fun hasLivePreview(): Boolean = irJson != null
}

companion object {
/**
* Thread-safe counter for generating unique IDs.
Expand Down
Loading
Loading