Files
pentagi/backend/docs/charm.md
T
2026-03-26 06:16:07 +03:00

48 KiB

Charm.sh Ecosystem - Personal Cheat Sheet

Personal reference for building TUI applications with the Charm stack.

📦 Core Libraries Overview

Core Packages

  • bubbletea: Event-driven TUI framework (MVU pattern)
  • lipgloss: Styling and layout engine
  • bubbles: Pre-built components (viewport, textinput, etc.)
  • huh: Advanced form builder
  • glamour: Markdown renderer

🫧 BubbleTea (MVU Pattern)

Model-View-Update Lifecycle

// Model holds all state
type Model struct {
    content string
    ready   bool
}

// Update handles events and returns new state
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.WindowSizeMsg:
        // Handle resize
        return m, nil
    case tea.KeyMsg:
        // Handle keyboard input
        return m, nil
    }
    return m, nil
}

// View renders current state
func (m Model) View() string {
    return "content"
}

// Init returns initial command
func (m Model) Init() tea.Cmd {
    return nil
}

Commands and Messages

// Commands return future messages
func loadDataCmd() tea.Msg {
    return DataLoadedMsg{data: "loaded"}
}

// Async operations
return m, tea.Cmd(func() tea.Msg {
    time.Sleep(time.Second)
    return TimerMsg{}
})

Critical Patterns

// Model interface implementation
type Model struct {
    styles *styles.Styles  // ALWAYS use shared styles
}

func (m Model) Init() tea.Cmd {
    // ALWAYS reset state completely
    m.content = ""
    m.ready = false
    return m.loadContent
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.WindowSizeMsg:
        // NEVER store dimensions in model - use styles.SetSize()
        // Model gets dimensions via m.styles.GetSize()
    case tea.KeyMsg:
        switch msg.String() {
        case "enter": return m, navigateCmd
        }
    }
}

🎨 Lipgloss (Styling & Layout)

Purpose: CSS-like styling for terminal interfaces Key Insight: Height() vs MaxHeight() behavior difference!

Critical Height Control

// ❌ WRONG: Height() sets MINIMUM height (can expand!)
style := lipgloss.NewStyle().Height(1).Border(lipgloss.NormalBorder())

// ✅ CORRECT: MaxHeight() + Inline() for EXACT height
style := lipgloss.NewStyle().MaxHeight(1).Inline(true)

// ✅ PRODUCTION: Background approach for consistent 1-line footers
footer := lipgloss.NewStyle().
    Width(width).
    Background(borderColor).
    Foreground(textColor).
    Padding(0, 1, 0, 1).  // Only horizontal padding
    Render(text)

// FOOTER APPROACH - PRODUCTION READY (✅ PROVEN SOLUTION)
// ❌ WRONG: Border approach (inconsistent height)
style.BorderTop(true).Height(1)

// ✅ CORRECT: Background approach (always 1 line)
style.Background(color).Foreground(textColor).Padding(0,1,0,1)

Layout Patterns

// LAYOUT COMPOSITION
lipgloss.JoinVertical(lipgloss.Left, header, content, footer)
lipgloss.JoinHorizontal(lipgloss.Top, left, right)
lipgloss.Place(width, height, lipgloss.Center, lipgloss.Top, content)

// Horizontal layout
left := lipgloss.NewStyle().Width(leftWidth).Render(leftContent)
right := lipgloss.NewStyle().Width(rightWidth).Render(rightContent)
combined := lipgloss.JoinHorizontal(lipgloss.Top, left, right)

// Vertical layout with consistent spacing
sections := []string{header, content, footer}
combined := lipgloss.JoinVertical(lipgloss.Left, sections...)

// Centering content
centered := lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, content)

// Responsive design
verticalStyle := lipgloss.NewStyle().Width(width).Padding(0, 2, 0, 2)
if width < 80 {
    // Vertical layout for narrow screens
}

Responsive Patterns

// Breakpoint-based layout
width, height := m.styles.GetSize()  // ALWAYS from styles
if width < 80 {
    return lipgloss.JoinVertical(lipgloss.Left, panels...)
} else {
    return lipgloss.JoinHorizontal(lipgloss.Top, panels...)
}

// Dynamic width allocation
leftWidth := width / 3
rightWidth := width - leftWidth - 4

📺 Bubbles (Interactive Components)

Purpose: Pre-built interactive components Key Components: viewport, textinput, list, table

Viewport - Critical for Scrolling

import "github.com/charmbracelet/bubbles/viewport"

// Setup
viewport := viewport.New(width, height)
viewport.Style = lipgloss.NewStyle() // Clean style prevents conflicts

// Modern scroll methods (use these!)
viewport.ScrollUp(1)     // Replaces LineUp()
viewport.ScrollDown(1)   // Replaces LineDown()
viewport.ScrollLeft(2)   // Horizontal, 2 steps for forms
viewport.ScrollRight(2)

// Deprecated (avoid)
vp.LineUp(lines)       // ❌ Deprecated
vp.LineDown(lines)     // ❌ Deprecated

// Status tracking
viewport.ScrollPercent() // 0.0 to 1.0
viewport.AtBottom()      // bool
viewport.AtTop()         // bool

// State checking
isScrollable := !(vp.AtTop() && vp.AtBottom())
progress := vp.ScrollPercent()

// Content management
viewport.SetContent(content)
viewport.View() // Renders visible portion

// Update in message handling
var cmd tea.Cmd
m.viewport, cmd = m.viewport.Update(msg)

TextInput

import "github.com/charmbracelet/bubbles/textinput"

ti := textinput.New()
ti.Placeholder = "Enter text..."
ti.Focus()
ti.EchoMode = textinput.EchoPassword  // For masked input
ti.CharLimit = 100

📝 Huh (Forms)

Purpose: Advanced form builder for complex user input

import "github.com/charmbracelet/huh"

form := huh.NewForm(
    huh.NewGroup(
        huh.NewInput().
            Key("api_key").
            Title("API Key").
            Password().  // Masked input
            Validate(func(s string) error {
                if len(s) < 10 {
                    return errors.New("API key too short")
                }
                return nil
            }),

        huh.NewSelect[string]().
            Key("provider").
            Title("Provider").
            Options(
                huh.NewOption("OpenAI", "openai"),
                huh.NewOption("Anthropic", "anthropic"),
            ),
    ),
)

// Integration with bubbletea
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    var cmd tea.Cmd
    m.form, cmd = m.form.Update(msg)

    if m.form.State == huh.StateCompleted {
        // Form submitted - access values
        apiKey := m.form.GetString("api_key")
        provider := m.form.GetString("provider")
    }

    return m, cmd
}

Glamour (Markdown Rendering)

Purpose: Beautiful markdown rendering in terminal CRITICAL: Create renderer ONCE in styles.New(), reuse everywhere

// ✅ CORRECT: Single renderer instance (prevents freezing)
// styles.go
type Styles struct {
    renderer *glamour.TermRenderer
}

func New() *Styles {
    renderer, _ := glamour.NewTermRenderer(
        glamour.WithAutoStyle(),
        glamour.WithWordWrap(80),
    )
    return &Styles{renderer: renderer}
}

func (s *Styles) GetRenderer() *glamour.TermRenderer {
    return s.renderer
}

// Usage in models
rendered, err := m.styles.GetRenderer().Render(markdown)

// ❌ WRONG: Creating new renderer each time (can freeze!)
renderer, _ := glamour.NewTermRenderer(...)

Safe Rendering with Fallback

// Safe rendering with fallback
rendered, err := renderer.Render(content)
if err != nil {
    // Fallback to plain text
    rendered = fmt.Sprintf("# Content\n\n%s\n\n*Render error: %v*", content, err)
}

🏗️ Production Architecture Patterns

1. Centralized Styles & Dimensions

CRITICAL: Never store width/height in models - use styles singleton

// ✅ CORRECT: Centralized in styles
type Styles struct {
    width    int
    height   int
    renderer *glamour.TermRenderer
    // ... all styles
}

func (s *Styles) SetSize(width, height int) {
    s.width = width
    s.height = height
    s.updateStyles()  // Recalculate responsive styles
}

func (s *Styles) GetSize() (int, int) {
    return s.width, s.height
}

// Models use styles for dimensions
func (m *Model) updateViewport() {
    width, height := m.styles.GetSize()
    if width <= 0 || height <= 0 {
        return  // Graceful handling
    }
    // ... viewport setup
}

2. TUI-Safe Logging System

Problem: fmt.Printf breaks TUI rendering Solution: File-based logger

// logger.Log() writes to log.json
logger.Log("[Component] ACTION: details %v", value)
logger.Errorf("[Component] ERROR: %v", err)

// Development monitoring (separate terminal)
tail -f log.json

// ❌ WRONG: Console output in TUI
fmt.Printf("Debug: %v\n", value)  // Breaks rendering

3. Unified Header/Footer Management

// app.go - Central layout control
func (a *App) View() string {
    header := a.renderHeader()
    footer := a.renderFooter()
    content := a.currentModel.View()

    contentHeight := max(height - headerHeight - footerHeight, 0)
    contentArea := a.styles.Content.Height(contentHeight).Render(content)

    return lipgloss.JoinVertical(lipgloss.Left, header, contentArea, footer)
}

func (a *App) renderHeader() string {
    switch a.navigator.Current() {
    case WelcomeScreen:
        return a.styles.RenderASCIILogo()
    default:
        return a.styles.Header.Render(title)
    }
}

func (a *App) renderFooter() string {
    actions := []string{}
    // Dynamic actions based on screen state
    if canContinue {
        actions = append(actions, "Enter: Continue")
    }
    if hasScrollableContent {
        actions = append(actions, "↑↓: Scroll")
    }

    return lipgloss.NewStyle().
        Width(width).
        Background(borderColor).
        Foreground(textColor).
        Padding(0, 1, 0, 1).
        Render(strings.Join(actions, " • "))
}

// locale.go - Helper functions
func BuildCommonActions() []string {
    return []string{NavBack, NavExit}
}

func BuildEULAActions(atEnd bool) []string {
    if !atEnd {
        return []string{EULANavScrollInstructions}
    }
    return []string{EULANavAcceptReject}
}

// Usage
actions := locale.BuildCommonActions()
actions = append(actions, specificActions...)

4. Type-Safe Navigation with Composite ScreenIDs

Critical Pattern: Use typed screen IDs with argument support

type ScreenID string
const (
    WelcomeScreen ScreenID = "welcome"
    EULAScreen   ScreenID = "eula"
    MainMenuScreen ScreenID = "main_menu"
    LLMProviderFormScreen ScreenID = "llm_provider_form"
)

// ScreenID methods for composite support
func (s ScreenID) GetScreen() string {
    parts := strings.Split(string(s), "§")
    return parts[0]
}

func (s ScreenID) GetArgs() []string {
    parts := strings.Split(string(s), "§")
    if len(parts) <= 1 {
        return []string{}
    }
    return parts[1:]
}

func CreateScreenID(screen string, args ...string) ScreenID {
    if len(args) == 0 {
        return ScreenID(screen)
    }
    parts := append([]string{screen}, args...)
    return ScreenID(strings.Join(parts, "§"))
}

type NavigationMsg struct {
    Target ScreenID  // Can be simple or composite!
    GoBack bool
}

// Usage - Simple screen
return m, func() tea.Msg {
    return NavigationMsg{Target: EULAScreen}
}

// Usage - Composite screen with arguments
return m, func() tea.Msg {
    return NavigationMsg{Target: CreateScreenID("llm_provider_form", "openai")}
}

5. Model State Management

Pattern: Complete reset on Init() for predictable behavior

func (m *Model) Init() tea.Cmd {
    logger.Log("[Model] INIT")
    // ALWAYS reset ALL state
    m.content = ""
    m.ready = false
    m.scrolled = false
    m.scrolledToEnd = false
    m.error = nil
    return m.loadContent
}

// Force re-render after async operations
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case ContentLoadedMsg:
        m.content = msg.Content
        m.ready = true
        // Force view update
        return m, func() tea.Msg { return nil }
    }
    return m, nil
}

🐛 Key Debugging Techniques

1. TUI-Safe Debug Output

// ❌ NEVER: Breaks TUI rendering
fmt.Println("debug")
log.Println("debug")

// ✅ ALWAYS: File-based logging
logger.Log("[Component] Event: %v", msg)
logger.Log("[Model] UPDATE: key=%s", msg.String())
logger.Log("[Model] VIEWPORT: %dx%d ready=%v", width, height, m.ready)

// Monitor in separate terminal
tail -f log.json

2. Dimension Handling

func (m Model) View() string {
    width, height := m.styles.GetSize()
    if width <= 0 || height <= 0 {
        return "Loading..." // Graceful fallback
    }
    // ... normal rendering
}

// Log dimension changes
logger.Log("[Model] RESIZE: %dx%d", width, height)

3. Content Loading Debug

func (m *Model) loadContent() tea.Msg {
    logger.Log("[Model] LOAD: start")
    content, err := source.GetContent()
    if err != nil {
        logger.Errorf("[Model] LOAD: error: %v", err)
        return ErrorMsg{err}
    }
    logger.Log("[Model] LOAD: success (%d chars)", len(content))
    return ContentLoadedMsg{content}
}

🎯 Advanced Navigation with Composite ScreenIDs

Composite ScreenID Pattern

Problem: Need to pass parameters to screens (e.g., which provider to configure) Solution: Composite ScreenIDs with § separator

// Format: "screen§arg1§arg2§..."
type ScreenID string

// Methods for parsing composite IDs
func (s ScreenID) GetScreen() string {
    parts := strings.Split(string(s), "§")
    return parts[0]
}

func (s ScreenID) GetArgs() []string {
    parts := strings.Split(string(s), "§")
    if len(parts) <= 1 {
        return []string{}
    }
    return parts[1:]
}

// Helper for creating composite IDs
func CreateScreenID(screen string, args ...string) ScreenID {
    if len(args) == 0 {
        return ScreenID(screen)
    }
    parts := append([]string{screen}, args...)
    return ScreenID(strings.Join(parts, "§"))
}

Usage Examples

// Simple screen (no arguments)
welcome := WelcomeScreen  // "welcome"

// Composite screen (with arguments)
providerForm := CreateScreenID("llm_provider_form", "openai")  // "llm_provider_form§openai"

// Navigation with arguments
return m, func() tea.Msg {
    return NavigationMsg{
        Target: CreateScreenID("llm_provider_form", "anthropic"),
        Data:   FormData{ProviderID: "anthropic"},
    }
}

// In createModelForScreen - extract arguments
func (a *App) createModelForScreen(screenID ScreenID, data any) tea.Model {
    baseScreen := screenID.GetScreen()
    args := screenID.GetArgs()

    switch ScreenID(baseScreen) {
    case LLMProviderFormScreen:
        providerID := "openai" // default
        if len(args) > 0 {
            providerID = args[0]
        }
        return NewLLMProviderFormModel(providerID, ...)
    }
}

State Persistence

// Stack automatically preserves composite IDs
navigator.Push(CreateScreenID("llm_provider_form", "gemini"))

// State contains: ["welcome", "main_menu", "llm_providers", "llm_provider_form§gemini"]
// On restore: user returns to Gemini provider form, not default OpenAI

🎯 Advanced Form Scrolling with Viewport

Auto-Scrolling Forms Pattern

Problem: Forms with many fields don't fit on smaller terminals, focused fields go off-screen Solution: Viewport component with automatic scroll-to-focus behavior

import "github.com/charmbracelet/bubbles/viewport"

type FormModel struct {
    fields       []FormField
    focusedIndex int
    viewport     viewport.Model
    formContent  string
    fieldHeights []int // Heights of each field for scroll calculation
}

// Initialize viewport
func New() *FormModel {
    return &FormModel{
        viewport: viewport.New(0, 0),
    }
}

// Update viewport dimensions on resize
func (m *FormModel) updateViewport() {
    contentWidth, contentHeight := m.getContentSize()
    m.viewport.Width = contentWidth - 4  // padding
    m.viewport.Height = contentHeight - 2 // header/footer space
    m.viewport.SetContent(m.formContent)
}

// Render form content and track field positions
func (m *FormModel) updateFormContent() {
    var sections []string
    m.fieldHeights = []int{}

    for i, field := range m.fields {
        fieldHeight := 4 // title + description + input + spacing
        m.fieldHeights = append(m.fieldHeights, fieldHeight)

        sections = append(sections, field.Title)
        sections = append(sections, field.Description)
        sections = append(sections, field.Input.View())
        sections = append(sections, "") // spacing
    }

    m.formContent = strings.Join(sections, "\n")
    m.viewport.SetContent(m.formContent)
}

// Auto-scroll to focused field
func (m *FormModel) ensureFocusVisible() {
    if m.focusedIndex >= len(m.fieldHeights) {
        return
    }

    // Calculate Y position of focused field
    focusY := 0
    for i := 0; i < m.focusedIndex; i++ {
        focusY += m.fieldHeights[i]
    }

    visibleRows := m.viewport.Height
    offset := m.viewport.YOffset

    // Scroll up if field is above visible area
    if focusY < offset {
        m.viewport.YOffset = focusY
    }

    // Scroll down if field is below visible area
    if focusY+m.fieldHeights[m.focusedIndex] >= offset+visibleRows {
        m.viewport.YOffset = focusY + m.fieldHeights[m.focusedIndex] - visibleRows + 1
    }
}

// Navigation with auto-scroll
func (m *FormModel) focusNext() {
    m.fields[m.focusedIndex].Input.Blur()
    m.focusedIndex = (m.focusedIndex + 1) % len(m.fields)
    m.fields[m.focusedIndex].Input.Focus()
    m.updateFormContent()
    m.ensureFocusVisible() // Key addition!
}

// Render scrollable form
func (m *FormModel) View() string {
    return m.viewport.View() // Viewport handles clipping and scrolling
}

Key Benefits of Viewport Forms

  • Automatic Clipping: Viewport handles content that exceeds available space
  • Smooth Scrolling: Fields slide into view without jarring jumps
  • Focus Preservation: Focused field always remains visible
  • No Extra Hotkeys: Uses standard navigation (Tab, arrows)
  • Terminal Friendly: Works on any terminal size

Critical Implementation Details

  1. Field Height Tracking: Must calculate actual rendered height of each field
  2. Scroll Timing: Call ensureFocusVisible() after every focus change
  3. Content Updates: Re-render form content when input values change
  4. Viewport Sizing: Account for padding, headers, footers in size calculation

This pattern is essential for professional TUI applications with complex forms.

⚠️ Common Pitfalls & Solutions

1. Glamour Renderer Freezing

Problem: Creating new renderer instances can freeze Solution: Single shared renderer in styles.New()

// ❌ WRONG: New renderer each time
func (m *Model) renderMarkdown(content string) string {
    renderer, _ := glamour.NewTermRenderer(...)  // Can freeze!
    return renderer.Render(content)
}

// ✅ CORRECT: Shared renderer instance
func (m *Model) renderMarkdown(content string) string {
    return m.styles.GetRenderer().Render(content)
}

Problem: Border-based footers vary in height Solution: Background approach with padding

// ❌ WRONG: Border approach (height varies)
footer := lipgloss.NewStyle().
    Height(1).
    Border(lipgloss.Border{Top: true}).
    Render(text)

// ✅ CORRECT: Background approach (exactly 1 line)
footer := lipgloss.NewStyle().
    Background(borderColor).
    Foreground(textColor).
    Padding(0, 1, 0, 1).
    Render(text)

3. Dimension Synchronization

Problem: Models store their own width/height, get out of sync Solution: Centralize dimensions in styles singleton

// ❌ WRONG: Models managing their own dimensions
type Model struct {
    width, height int
}

// ✅ CORRECT: Centralized dimension management
type Model struct {
    styles *styles.Styles  // Access via styles.GetSize()
}

4. TUI Rendering Corruption

Problem: Console output breaks rendering Solution: File-based logger, never fmt.Printf

// ❌ NEVER: Use tea.ClearScreen during navigation
return a, tea.Batch(cmd, tea.ClearScreen)

// ✅ CORRECT: Let model Init() handle clean state
return a, a.currentModel.Init()

5. Navigation State Issues

Problem: Models retain state between visits Solution: Complete state reset in Init()

// ❌ WRONG: String-based navigation (typo-prone)
return NavigationMsg{Target: "main_menu"}

// ❌ WRONG: Manual string concatenation for arguments
return NavigationMsg{Target: ScreenID("llm_provider_form/openai")}

// ✅ CORRECT: Type-safe constants
return NavigationMsg{Target: MainMenuScreen}

// ✅ CORRECT: Composite ScreenID with helper
return NavigationMsg{Target: CreateScreenID("llm_provider_form", "openai")}

🚀 Performance & Best Practices

Proven Patterns

// ✅ DO: Shared renderer
rendered, _ := m.styles.GetRenderer().Render(content)

// ✅ DO: Centralized dimensions
width, height := m.styles.GetSize()

// ✅ DO: File logging
logger.Log("[Component] ACTION: %v", data)

// ✅ DO: Complete state reset
func (m *Model) Init() tea.Cmd {
    m.resetAllState()
    return m.loadContent
}

// ✅ DO: Graceful dimension handling
if width <= 0 || height <= 0 {
    return "Loading..."
}

Anti-Patterns to Avoid

// ❌ DON'T: New renderer instances
renderer, _ := glamour.NewTermRenderer(...)

// ❌ DON'T: Model dimensions
type Model struct {
    width  int  // Store in styles instead
    height int  // Store in styles instead
}

// ❌ DON'T: Console output
fmt.Printf("Debug: %v\n", value)

// ❌ DON'T: Partial state reset
func (m *Model) Init() tea.Cmd {
    // Only resetting some fields - incomplete!
    m.content = ""
    // Missing: m.ready, m.scrolled, etc.
}

Key Best Practices Summary

  • Single glamour renderer: Prevents freezing, faster rendering
  • Centralized dimensions: Eliminates sync issues, simplifies models
  • Background footer: Consistent height, modern appearance
  • Type-safe navigation: Compile-time error prevention
  • File-based logging: Debug without breaking TUI
  • Complete state reset: Predictable model behavior
  • Graceful fallbacks: Handle edge cases elegantly
  • Resource estimation: Real-time calculation of token/memory usage
  • Environment integration: Proper EnvVar handling with cleanup
  • Value formatting: Consistent human-readable displays (formatBytes, formatNumber)

This cheat sheet contains battle-tested solutions for TUI development in the Charm ecosystem, proven in production use.

🎯 Advanced Form Field Patterns

Boolean Fields with Tab Completion

Innovation: Auto-completion for boolean values with suggestions

import "github.com/charmbracelet/bubbles/textinput"

func createBooleanField() textinput.Model {
    input := textinput.New()
    input.Prompt = ""
    input.ShowSuggestions = true
    input.SetSuggestions([]string{"true", "false"})  // Enable tab completion

    // Show default value in placeholder
    input.Placeholder = "true (default)"  // Or "false (default)"

    return input
}

// Tab completion handler in Update()
func (m *FormModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyMsg:
        switch msg.String() {
        case "tab":
            // Complete boolean suggestion
            if m.focusedField.Input.ShowSuggestions {
                suggestion := m.focusedField.Input.CurrentSuggestion()
                if suggestion != "" {
                    m.focusedField.Input.SetValue(suggestion)
                    m.focusedField.Input.CursorEnd()
                    return m, nil
                }
            }
        }
    }
    return m, nil
}

Integer Fields with Range Validation

Innovation: Real-time validation with human-readable formatting

type IntegerFieldConfig struct {
    Key         string
    Title       string
    Description string
    Min         int
    Max         int
    Default     int
}

func (m *FormModel) addIntegerField(config IntegerFieldConfig) {
    input := textinput.New()
    input.Prompt = ""
    input.PlaceholderStyle = m.styles.FormPlaceholder

    // Human-readable placeholder with default
    input.Placeholder = fmt.Sprintf("%s (%s default)",
        formatNumber(config.Default), formatBytes(config.Default))

    // Add validation range to description
    fullDescription := fmt.Sprintf("%s (Range: %s - %s)",
        config.Description, formatBytes(config.Min), formatBytes(config.Max))

    field := FormField{
        Key:         config.Key,
        Title:       config.Title,
        Description: fullDescription,
        Input:       input,
        Min:         config.Min,
        Max:         config.Max,
    }

    m.fields = append(m.fields, field)
}

// Real-time validation
func (m *FormModel) validateIntegerField(field *FormField) {
    value := field.Input.Value()

    if value == "" {
        field.Input.Placeholder = fmt.Sprintf("%s (default)", formatNumber(field.Default))
        return
    }

    if intVal, err := strconv.Atoi(value); err != nil {
        field.Input.Placeholder = "Enter a valid number or leave empty for default"
    } else {
        if intVal < field.Min || intVal > field.Max {
            field.Input.Placeholder = fmt.Sprintf("Range: %s - %s",
                formatBytes(field.Min), formatBytes(field.Max))
        } else {
            field.Input.Placeholder = "" // Clear error
        }
    }
}

Value Formatting Utilities

Critical: Consistent formatting across all forms

// Universal byte formatting for configuration values
func formatBytes(bytes int) string {
    if bytes >= 1048576 {
        return fmt.Sprintf("%.1fMB", float64(bytes)/1048576)
    } else if bytes >= 1024 {
        return fmt.Sprintf("%.1fKB", float64(bytes)/1024)
    }
    return fmt.Sprintf("%d bytes", bytes)
}

// Universal number formatting for display
func formatNumber(num int) string {
    if num >= 1000000 {
        return fmt.Sprintf("%.1fM", float64(num)/1000000)
    } else if num >= 1000 {
        return fmt.Sprintf("%.1fK", float64(num)/1000)
    }
    return strconv.Itoa(num)
}

// Usage in forms and info panels
sections = append(sections, fmt.Sprintf("• Memory Limit: %s", formatBytes(memoryLimit)))
sections = append(sections, fmt.Sprintf("• Estimated tokens: ~%s", formatNumber(tokenCount)))

Environment Variable Integration Pattern

Innovation: Direct EnvVar integration with presence detection

// EnvVar wrapper (from loader package)
type EnvVar struct {
    Key     string
    Value   string  // Current value in environment
    Default string  // Default value from config
}

func (e EnvVar) IsPresent() bool {
    return e.Value != "" // Check if actually set in environment
}

// Form field creation from EnvVar
func (m *FormModel) addFieldFromEnvVar(envVarName, fieldKey, title, description string) {
    envVar, _ := m.controller.GetVar(envVarName)

    // Track initially set fields for cleanup logic
    m.initiallySetFields[fieldKey] = envVar.IsPresent()

    input := textinput.New()
    input.Prompt = ""

    // Show default in placeholder if not set
    if !envVar.IsPresent() {
        input.Placeholder = fmt.Sprintf("%s (default)", envVar.Default)
    } else {
        input.SetValue(envVar.Value) // Set current value
    }

    field := FormField{
        Key:         fieldKey,
        Title:       title,
        Description: description,
        Input:       input,
        EnvVarName:  envVarName,
    }

    m.fields = append(m.fields, field)
}

Smart Field Cleanup Pattern

Innovation: Environment variable cleanup for empty values

func (m *FormModel) saveConfiguration() error {
    // First pass: Remove cleared fields from environment
    for _, field := range m.fields {
        value := strings.TrimSpace(field.Input.Value())

        // If field was initially set but now empty, remove it
        if value == "" && m.initiallySetFields[field.Key] {
            if err := m.controller.SetVar(field.EnvVarName, ""); err != nil {
                return fmt.Errorf("failed to clear %s: %w", field.EnvVarName, err)
            }
            logger.Log("[FormModel] SAVE: cleared %s", field.EnvVarName)
        }
    }

    // Second pass: Save only non-empty values
    for _, field := range m.fields {
        value := strings.TrimSpace(field.Input.Value())
        if value == "" {
            continue // Skip empty - use defaults
        }

        // Validate before saving
        if err := m.validateFieldValue(field, value); err != nil {
            return fmt.Errorf("validation failed for %s: %w", field.Key, err)
        }

        // Save validated value
        if err := m.controller.SetVar(field.EnvVarName, value); err != nil {
            return fmt.Errorf("failed to set %s: %w", field.EnvVarName, err)
        }
        logger.Log("[FormModel] SAVE: set %s=%s", field.EnvVarName, value)
    }

    return nil
}

Resource Estimation Pattern

Innovation: Real-time calculation of resource usage

func (m *ConfigFormModel) calculateResourceEstimate() string {
    // Get current form values or defaults
    maxMemory := m.getIntValueOrDefault("max_memory")
    maxConnections := m.getIntValueOrDefault("max_connections")
    cacheSize := m.getIntValueOrDefault("cache_size")

    // Algorithm-specific calculations
    var estimatedMemory int
    switch m.configType {
    case "database":
        estimatedMemory = maxMemory + (maxConnections * 1024) + cacheSize
    case "worker":
        estimatedMemory = maxMemory * maxConnections
    default:
        estimatedMemory = maxMemory
    }

    // Convert to human-readable format
    return fmt.Sprintf("~%s RAM", formatBytes(estimatedMemory))
}

// Helper to get form value or default
func (m *FormModel) getIntValueOrDefault(fieldKey string) int {
    // First check current form input
    for _, field := range m.fields {
        if field.Key == fieldKey {
            if value := strings.TrimSpace(field.Input.Value()); value != "" {
                if intVal, err := strconv.Atoi(value); err == nil {
                    return intVal
                }
            }
        }
    }

    // Fall back to environment default
    envVar, _ := m.controller.GetVar(m.getEnvVarName(fieldKey))
    if defaultVal, err := strconv.Atoi(envVar.Default); err == nil {
        return defaultVal
    }

    return 0
}

// Display in form content
func (m *FormModel) updateFormContent() {
    // ... form fields ...

    // Resource estimation section
    sections = append(sections, "")
    sections = append(sections, m.styles.Subtitle.Render("Resource Estimation"))
    sections = append(sections, m.styles.Paragraph.Render("Estimated usage: "+m.calculateResourceEstimate()))

    m.formContent = strings.Join(sections, "\n")
    m.viewport.SetContent(m.formContent)
}

Current Configuration Preview Pattern

Innovation: Live display of current settings in info panel

func (m *TypeSelectionModel) renderConfigurationPreview() string {
    selectedType := m.types[m.selectedIndex]
    var sections []string

    // Helper to get current environment values
    getValue := func(suffix string) string {
        envVar, _ := m.controller.GetVar(m.getEnvVarName(selectedType.ID, suffix))
        if envVar.Value != "" {
            return envVar.Value
        }
        return envVar.Default + " (default)"
    }

    getIntValue := func(suffix string) int {
        envVar, _ := m.controller.GetVar(m.getEnvVarName(selectedType.ID, suffix))
        if envVar.Value != "" {
            if val, err := strconv.Atoi(envVar.Value); err == nil {
                return val
            }
        }
        if val, err := strconv.Atoi(envVar.Default); err == nil {
            return val
        }
        return 0
    }

    // Display current configuration
    sections = append(sections, m.styles.Subtitle.Render("Current Configuration"))
    sections = append(sections, "")

    maxMemory := getIntValue("MAX_MEMORY")
    timeout := getIntValue("TIMEOUT")
    enabled := getValue("ENABLED")

    sections = append(sections, fmt.Sprintf("• Max Memory: %s", formatBytes(maxMemory)))
    sections = append(sections, fmt.Sprintf("• Timeout: %d seconds", timeout))
    sections = append(sections, fmt.Sprintf("• Enabled: %s", enabled))

    // Type-specific configuration
    if selectedType.ID == "advanced" {
        retries := getIntValue("MAX_RETRIES")
        sections = append(sections, fmt.Sprintf("• Max Retries: %d", retries))
    }

    return strings.Join(sections, "\n")
}

Type-Based Dynamic Forms

Innovation: Conditional field generation based on selection

func (m *FormModel) buildDynamicForm() {
    m.fields = []FormField{} // Reset

    // Common fields for all types
    m.addFieldFromEnvVar("ENABLED", "enabled", "Enable Service", "Enable or disable this service")
    m.addFieldFromEnvVar("MAX_MEMORY", "max_memory", "Memory Limit", "Maximum memory usage in bytes")

    // Type-specific fields
    switch m.configType {
    case "database":
        m.addFieldFromEnvVar("MAX_CONNECTIONS", "max_connections", "Max Connections", "Maximum database connections")
        m.addFieldFromEnvVar("CACHE_SIZE", "cache_size", "Cache Size", "Database cache size in bytes")

    case "worker":
        m.addFieldFromEnvVar("WORKER_COUNT", "worker_count", "Worker Count", "Number of worker processes")
        m.addFieldFromEnvVar("QUEUE_SIZE", "queue_size", "Queue Size", "Maximum queue size")

    case "api":
        m.addFieldFromEnvVar("RATE_LIMIT", "rate_limit", "Rate Limit", "API requests per minute")
        m.addFieldFromEnvVar("TIMEOUT", "timeout", "Request Timeout", "Request timeout in seconds")
    }

    // Set focus on first field
    if len(m.fields) > 0 {
        m.fields[0].Input.Focus()
    }
}

// Environment variable naming helper
func (m *FormModel) getEnvVarName(configType, suffix string) string {
    prefix := strings.ToUpper(configType) + "_"
    return prefix + suffix
}

These advanced patterns enable:

  • Smart Validation: Real-time feedback with user-friendly error messages
  • Resource Awareness: Live estimation of memory, CPU, or token usage
  • Environment Integration: Proper handling of defaults, presence detection, and cleanup
  • Type Safety: Compile-time validation and runtime error handling
  • User Experience: Auto-completion, formatting, and intuitive navigation

🎯 Production Form Architecture Patterns

Form Model Structure (Latest Pattern)

Based on successful llm_provider_form.go and summarizer_form.go implementations

type FormModel struct {
    controller *controllers.StateController
    styles     *styles.Styles
    window     *window.Window

    // Core form state
    fields       []FormField
    focusedIndex int
    showValues   bool
    hasChanges   bool
    args         []string // Arguments from composite ScreenID

    // Enhanced state tracking (from summarizer implementation)
    initialized        bool
    configType        string
    typeName          string
    initiallySetFields map[string]bool // Track fields for cleanup

    // Viewport as permanent property for forms
    viewport     viewport.Model
    formContent  string
    fieldHeights []int
}

// Constructor pattern - args from composite ScreenID
func NewFormModel(
    controller *controllers.StateController, styles *styles.Styles,
    window *window.Window, args []string,
) *FormModel {
    // Extract primary argument (e.g., provider ID)
    primaryArg := "default"
    if len(args) > 0 && args[0] != "" {
        primaryArg = args[0]
    }

    return &FormModel{
        controller: controller,
        styles:     styles,
        window:     window,
        args:       args,
        viewport:   viewport.New(window.GetContentSize()), // Permanent viewport
    }
}

Key Form Implementation Patterns

1. Proper Navigation Hotkeys

// Modern form navigation (Production Pattern - from summarizer_form.go)
switch msg.String() {
case "down":           // ↓: Next field
    m.focusNext()
    m.ensureFocusVisible()

case "up":             // ↑: Previous field
    m.focusPrev()
    m.ensureFocusVisible()

case "tab":            // Tab: Complete suggestion (boolean auto-complete)
    m.completeSuggestion()

case "ctrl+h":         // Ctrl+H: Toggle show/hide masked values
    m.toggleShowValues()

case "ctrl+s":         // Ctrl+S: Save configuration only
    return m.saveConfiguration()

case "ctrl+r":         // Ctrl+R: Reset form to defaults
    m.resetForm()
    return m, nil

case "enter":          // Enter: Save and return (GoBack navigation)
    return m.saveAndReturn()
}

// Enhanced field navigation with auto-scroll
func (m *FormModel) focusNext() {
    if len(m.fields) == 0 {
        return
    }
    m.fields[m.focusedIndex].Input.Blur()
    m.focusedIndex = (m.focusedIndex + 1) % len(m.fields)
    m.fields[m.focusedIndex].Input.Focus()
    m.updateFormContent()
}

2. Suggestions and Auto-completion

// Boolean field with suggestions (from summarizer_form.go)
func (m *FormModel) addBooleanField(key, title, description string, envVar loader.EnvVar) {
    input := textinput.New()
    input.Prompt = ""
    input.PlaceholderStyle = m.styles.FormPlaceholder
    input.ShowSuggestions = true
    input.SetSuggestions([]string{"true", "false"})

    // Show default in placeholder
    if envVar.Default == "true" {
        input.Placeholder = "true (default)"
    } else {
        input.Placeholder = "false (default)"
    }

    // Set value only if actually present in environment
    if envVar.Value != "" && envVar.IsPresent() {
        input.SetValue(envVar.Value)
    }
}

// Tab completion handler
func (m *FormModel) completeSuggestion() {
    if m.focusedIndex < len(m.fields) {
        suggestion := m.fields[m.focusedIndex].Input.CurrentSuggestion()
        if suggestion != "" {
            m.fields[m.focusedIndex].Input.SetValue(suggestion)
            m.fields[m.focusedIndex].Input.CursorEnd()
            m.fields[m.focusedIndex].Value = suggestion
            m.hasChanges = true
            m.updateFormContent()
        }
    }
}

3. Dynamic Input Width Calculation

// Adaptive input sizing
func (m *FormModel) getInputWidth() int {
    viewportWidth, _ := m.getViewportSize()
    inputWidth := viewportWidth - 6  // Account for padding
    if m.isVerticalLayout() {
        inputWidth = viewportWidth - 4  // Less padding in vertical
    }
    return inputWidth
}

func (m *FormModel) getViewportSize() (int, int) {
    contentWidth, contentHeight := m.window.GetContentSize()
    if contentWidth <= 0 || contentHeight <= 0 {
        return 0, 0
    }

    if m.isVerticalLayout() {
        return contentWidth - PaddingWidth/2, contentHeight - PaddingHeight
    } else {
        leftWidth := MinMenuWidth
        extraWidth := contentWidth - leftWidth - MinInfoWidth - PaddingWidth
        if extraWidth > 0 {
            leftWidth = min(leftWidth+extraWidth/2, MaxMenuWidth)
        }
        return leftWidth, contentHeight - PaddingHeight
    }
}

4. Viewport as Permanent Form Property

// ✅ CORRECT: Viewport as permanent property for forms
type FormModel struct {
    viewport viewport.Model  // Permanent - preserves scroll position
}

// Update viewport dimensions on resize
func (m *FormModel) updateViewport() {
    formContentHeight := lipgloss.Height(m.formContent) + 2
    viewportWidth, viewportHeight := m.getViewportSize()
    m.viewport.Width = viewportWidth
    m.viewport.Height = min(viewportHeight, formContentHeight)
    m.viewport.SetContent(m.formContent)
}

// ❌ WRONG: Creating viewport in View() - loses scroll state
func (m *FormModel) View() string {
    vp := viewport.New(width, height) // State lost on re-render!
    return vp.View()
}

Layout Architecture (Two-Column Pattern)

1. Layout Constants (Production Values)

const (
    MinMenuWidth  = 38  // Minimum left panel width
    MaxMenuWidth  = 66  // Maximum left panel width (prevents too wide)
    MinInfoWidth  = 34  // Minimum right panel width
    PaddingWidth  = 8   // Total horizontal padding
    PaddingHeight = 2   // Vertical padding
)

2. Adaptive Layout Logic

func (m *Model) isVerticalLayout() bool {
    contentWidth := m.window.GetContentWidth()
    return contentWidth < (MinMenuWidth + MinInfoWidth + PaddingWidth)
}

// Horizontal layout with dynamic width allocation
func (m *Model) renderHorizontalLayout(leftPanel, rightPanel string, width, height int) string {
    leftWidth, rightWidth := MinMenuWidth, MinInfoWidth
    extraWidth := width - leftWidth - rightWidth - PaddingWidth

    // Distribute extra space, but cap left panel at MaxMenuWidth
    if extraWidth > 0 {
        leftWidth = min(leftWidth+extraWidth/2, MaxMenuWidth)
        rightWidth = width - leftWidth - PaddingWidth/2
    }

    leftStyled := lipgloss.NewStyle().Width(leftWidth).Padding(0, 2, 0, 2).Render(leftPanel)
    rightStyled := lipgloss.NewStyle().Width(rightWidth).PaddingLeft(2).Render(rightPanel)

    // Use viewport for final layout rendering
    viewport := viewport.New(width, height-PaddingHeight)
    viewport.SetContent(lipgloss.JoinHorizontal(lipgloss.Top, leftStyled, rightStyled))
    return viewport.View()
}

3. Content Hiding When Space Insufficient

// Vertical layout with conditional content hiding
func (m *Model) renderVerticalLayout(leftPanel, rightPanel string, width, height int) string {
    verticalStyle := lipgloss.NewStyle().Width(width).Padding(0, 4, 0, 2)

    leftStyled := verticalStyle.Render(leftPanel)
    rightStyled := verticalStyle.Render(rightPanel)

    // Hide right panel if both don't fit
    if lipgloss.Height(leftStyled)+lipgloss.Height(rightStyled)+2 < height {
        return lipgloss.JoinVertical(lipgloss.Left,
            leftStyled,
            verticalStyle.Height(1).Render(""),
            rightStyled,
        )
    }

    // Show only essential left panel
    return leftStyled
}

Composite ScreenID Navigation (Production Pattern)

1. Proper ScreenID Creation for Navigation

// Navigation from menu with argument preservation
func (m *MenuModel) handleSelection() (tea.Model, tea.Cmd) {
    selectedItem := m.getSelectedItem()

    // Create composite ScreenID with current selection for stack preservation
    return m, func() tea.Msg {
        return NavigationMsg{
            Target: CreateScreenID(string(targetScreen), selectedItem.ID),
        }
    }
}

// Form navigation back - use GoBack to avoid stack loops
func (m *FormModel) saveAndReturn() (tea.Model, tea.Cmd) {
    model, cmd := m.saveConfiguration()
    if cmd != nil {
        return model, cmd
    }

    // ✅ CORRECT: Use GoBack to return to previous screen
    return m, func() tea.Msg {
        return NavigationMsg{GoBack: true}
    }
}

// ❌ WRONG: Direct navigation creates stack loops
return m, func() tea.Msg {
    return NavigationMsg{Target: LLMProvidersScreen} // Creates loop!
}

2. Constructor with Args Pattern

// Model constructor receives args from composite ScreenID
func NewModel(
    controller *controllers.StateController, styles *styles.Styles,
    window *window.Window, args []string,
) *Model {
    // Initialize with selection from args
    selectedIndex := 0
    if len(args) > 1 && args[1] != "" {
        // Find matching item and set selectedIndex
        for i, item := range items {
            if item.ID == args[1] {
                selectedIndex = i
                break
            }
        }
    }

    return &Model{
        controller:    controller,
        selectedIndex: selectedIndex,
        args:          args,
    }
}

// No separate SetSelected* methods needed
func (m *Model) Init() tea.Cmd {
    logger.Log("[Model] INIT: args=%s", strings.Join(m.args, " § "))

    // Selection already set in constructor from args
    m.loadData()
    return nil
}

Viewport Usage Patterns

1. Forms: Permanent Viewport Property

// ✅ For forms with user interaction and scroll state
type FormModel struct {
    viewport viewport.Model  // Permanent - preserves scroll position
}

func (m *FormModel) ensureFocusVisible() {
    // Auto-scroll to focused field
    focusY := m.calculateFieldPosition(m.focusedIndex)
    if focusY < m.viewport.YOffset {
        m.viewport.YOffset = focusY
    }
    // ... scroll logic
}

2. Layout: Temporary Viewport Creation

// ✅ For final layout rendering only
func (m *Model) renderHorizontalLayout(left, right string, width, height int) string {
    content := lipgloss.JoinHorizontal(lipgloss.Top, leftStyled, rightStyled)

    // Create viewport just for layout rendering
    vp := viewport.New(width, height-PaddingHeight)
    vp.SetContent(content)
    return vp.View()
}

Screen Architecture (App.go Integration)

1. Content Area Only Pattern

// Screen models ONLY handle content area
func (m *Model) View() string {
    // ✅ CORRECT: Only content, no header/footer
    leftPanel := m.renderForm()
    rightPanel := m.renderHelp()

    if m.isVerticalLayout() {
        return m.renderVerticalLayout(leftPanel, rightPanel, width, height)
    }
    return m.renderHorizontalLayout(leftPanel, rightPanel, width, height)
}

// ❌ WRONG: Handling header/footer in screen
func (m *Model) View() string {
    header := m.renderHeader()    // App.go handles this!
    footer := m.renderFooter()    // App.go handles this!
    // ...
}

2. App.go Layout Management

// App.go manages complete layout structure
func (a *App) View() string {
    header := a.renderHeader()    // Screen-specific header
    footer := a.renderFooter()    // Dynamic footer with actions
    content := a.currentModel.View()  // Content from model

    // Calculate content area size
    contentWidth, contentHeight := a.window.GetContentSize()
    contentArea := a.styles.Content.
        Width(contentWidth).
        Height(contentHeight).
        Render(content)

    return lipgloss.JoinVertical(lipgloss.Left, header, contentArea, footer)
}

Field Configuration Best Practices

1. Clean Input Setup

// Modern input field setup
func (m *FormModel) addInputField(config *Config, fieldType string) {
    input := textinput.New()
    input.Prompt = ""  // Clean appearance
    input.PlaceholderStyle = m.styles.FormPlaceholder

    // Dynamic width - set during rendering
    // input.Width NOT set here - calculated in updateFormContent()

    if fieldType == "password" {
        input.EchoMode = textinput.EchoPassword
    }

    if fieldType == "boolean" {
        input.ShowSuggestions = true
        input.SetSuggestions([]string{"true", "false"})
    }

    // Set value from config
    if config != nil {
        input.SetValue(config.GetValue(fieldType))
    }
}

2. Dynamic Width Application

// Apply width during content update
func (m *FormModel) updateFormContent() {
    inputWidth := m.getInputWidth()

    for i, field := range m.fields {
        // Apply dynamic width to input
        field.Input.Width = inputWidth - 3  // Account for borders
        field.Input.SetValue(field.Input.Value())  // Trigger width update

        // Render with consistent styling
        inputStyle := m.styles.FormInput.Width(inputWidth)
        if i == m.focusedIndex {
            inputStyle = inputStyle.BorderForeground(styles.Primary)
        }

        renderedInput := inputStyle.Render(field.Input.View())
        sections = append(sections, renderedInput)
    }
}

State Management Best Practices

1. Configuration vs Status Separation

// ✅ SIMPLIFIED: Single status field
type ProviderInfo struct {
    ID          string
    Name        string
    Description string
    Configured  bool  // Single status - provider has required fields
}

// Load status logic
func (m *Model) loadProviders() {
    configs := m.controller.GetLLMProviders()

    provider := ProviderInfo{
        ID:         "openai",
        Name:       locale.LLMProviderOpenAI,
        Configured: configs["openai"].Configured,  // From controller
    }
}

// ❌ COMPLEX: Multiple status fields (removed)
type ProviderInfo struct {
    Configured bool
    Enabled    bool  // Removed - controller handles this
}

2. GoBack Navigation Pattern

// ✅ CORRECT: GoBack prevents navigation loops
func (m *FormModel) saveAndReturn() (tea.Model, tea.Cmd) {
    if err := m.saveConfiguration(); err != nil {
        return m, nil  // Stay on form if save fails
    }

    // Return to previous screen (from navigation stack)
    return m, func() tea.Msg {
        return NavigationMsg{GoBack: true}
    }
}

// Navigation stack automatically maintained:
// ["main_menu§llm_providers", "llm_providers§openai", "llm_provider_form§openai"]
// GoBack removes current and returns to: "llm_providers§openai"

This production architecture ensures:

  • Clean separation: Forms handle content, app.go handles layout
  • Persistent state: Viewport scroll positions maintained
  • Adaptive design: Content hides gracefully when space insufficient
  • Type-safe navigation: Arguments preserved in composite ScreenIDs
  • No navigation loops: GoBack pattern prevents stack corruption