Skip to content

Refactor NanoRenderer to use component-specific render methods for better cross-platform support #487

@phodal

Description

@phodal

Problem

The current NanoRenderer<T> interface design makes it difficult to ensure complete cross-platform component implementation:

Current Implementation Issues

  1. Generic render dispatch via when statement:

    • Both HtmlRenderer and ComposeNanoRenderer use a when (ir.type) pattern to dispatch rendering
    • This relies on string matching ("VStack", "Card", etc.) which has no compile-time safety
  2. Silent failures with else branch:

    // HtmlRenderer.kt
    override fun renderComponent(ir: NanoIR): String {
        return when (ir.type) {
            "Component" -> renderComponentNode(ir)
            "VStack" -> renderVStack(ir)
            // ... more cases
            else -> "<!-- Unknown component: ${ir.type} -->"  // Silent failure!
        }
    }
    
    // ComposeNanoRenderer.kt  
    when (ir.type) {
        // ... cases
        else -> RenderUnknown(ir, modifier)  // Silent failure!
    }
  3. No compiler help for missing implementations:

    • When adding a new component (e.g., TextArea, Select, Form), there is no compiler error if a renderer does not implement it
    • Developers must manually check all renderer implementations

Proposed Solution

Follow the pattern used in CodingAgentRenderer, which defines specific methods for each operation:

interface CodingAgentRenderer {
    fun renderIterationHeader(current: Int, max: Int)
    fun renderLLMResponseStart()
    fun renderLLMResponseChunk(chunk: String)
    fun renderToolCall(toolName: String, paramsStr: String)
    fun renderToolResult(...)
    fun renderError(message: String)
    // ... each operation has its own method
}

Proposed New Interface

/**
 * Component-specific NanoUI Renderer interface
 * 
 * Each component type has its own render method, ensuring compile-time
 * checking when new components are added.
 */
interface NanoRenderer<T> {
    // Full document rendering
    fun render(ir: NanoIR): T
    
    // Layout components
    fun renderVStack(ir: NanoIR): T
    fun renderHStack(ir: NanoIR): T
    
    // Container components
    fun renderCard(ir: NanoIR): T
    fun renderForm(ir: NanoIR): T
    
    // Content components
    fun renderText(ir: NanoIR): T
    fun renderImage(ir: NanoIR): T
    fun renderBadge(ir: NanoIR): T
    fun renderDivider(ir: NanoIR): T
    
    // Input components
    fun renderButton(ir: NanoIR): T
    fun renderInput(ir: NanoIR): T
    fun renderCheckbox(ir: NanoIR): T
    fun renderTextArea(ir: NanoIR): T
    fun renderSelect(ir: NanoIR): T
    
    // Control flow
    fun renderConditional(ir: NanoIR): T
    fun renderForLoop(ir: NanoIR): T
    
    // Component wrapper
    fun renderComponent(ir: NanoIR): T
    
    // Unknown/unsupported component handling
    fun renderUnknown(ir: NanoIR): T
}

Benefits

  1. Compile-time safety: Adding a new component to the interface immediately shows errors in all implementations
  2. Explicit contract: Clear which components each platform must support
  3. Discoverable: Easy to see which components need implementation
  4. Default implementations possible: Can provide sensible defaults for optional components

Files to Update

Core Interface

  • xuiper-ui/src/main/kotlin/cc/unitmesh/xuiper/render/NanoRenderer.kt - Add component-specific methods

Implementations

  • xuiper-ui/src/main/kotlin/cc/unitmesh/xuiper/render/HtmlRenderer.kt - Implement new interface
  • mpp-ui/src/jvmMain/kotlin/cc/unitmesh/devins/ui/nano/ComposeNanoRenderer.kt - Implement new interface

Components to Support

Based on NanoSpecV1:

  • Layout: VStack, HStack
  • Container: Card, Form
  • Content: Text, Image, Badge, Divider
  • Input: Button, Input, Checkbox, TextArea, Select
  • Control Flow: Conditional (if), ForLoop (for)
  • Meta: Component (wrapper)

Alternative Approach (Sealed Interface)

Could also consider a sealed interface approach with visitor pattern, but the explicit method approach is simpler and matches the existing CodingAgentRenderer pattern.

Related Code References

  • mpp-core/src/commonMain/kotlin/cc/unitmesh/agent/render/CodingAgentRenderer.kt - Example of method-per-operation pattern
  • xuiper-ui/src/main/kotlin/cc/unitmesh/xuiper/spec/v1/NanoSpecV1.kt - Component definitions

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions