# PentAGI Installer Documentation

## Overview

The PentAGI installer provides a robust Terminal User Interface (TUI) for configuring the application. Built using the [Charm](https://charm.sh/) tech stack (bubbletea, lipgloss, bubbles), it implements modern responsive design patterns optimized for terminal environments.

## ⚠️ Development Constraints & TUI Workflow

### Core Workflow Principles

1. **Build-Only Development**: NEVER run TUI apps during development - breaks terminal session
2. **Test Cycle**: Build → Run separately → Return to development session
3. **Debug Output**: All debug MUST go to `logger.Log()` (writes to `log.json`) - never `fmt.Printf`
4. **Development Monitoring**: Use `tail -f log.json` in separate terminal

## Advanced Form Patterns

### **Boolean Field Pattern with Suggestions**

```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"})  // Tab completion

    // 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.hasChanges = true
            m.updateFormContent()
        }
    }
}
```

### **Integer Field with Range Validation**

```go
func (m *FormModel) addIntegerField(key, title, description string, envVar loader.EnvVar, min, max int) {
    input := textinput.New()
    input.Prompt = ""
    input.PlaceholderStyle = m.styles.FormPlaceholder

    // Parse and format default value
    defaultValue := 0
    if envVar.Default != "" {
        if val, err := strconv.Atoi(envVar.Default); err == nil {
            defaultValue = val
        }
    }

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

    // Add validation range to description
    fullDescription := fmt.Sprintf("%s (Range: %s - %s)",
        description, m.formatBytes(min), m.formatBytes(max))
}

// Real-time validation
func (m *FormModel) validateField(index int) {
    field := &m.fields[index]
    value := field.Input.Value()

    if intVal, err := strconv.Atoi(value); err != nil {
        field.Input.Placeholder = "Enter a valid number or leave empty for default"
    } else {
        // Check ranges with human-readable feedback
        if intVal < min || intVal > max {
            field.Input.Placeholder = fmt.Sprintf("Range: %s - %s",
                m.formatBytes(min), m.formatBytes(max))
        } else {
            field.Input.Placeholder = "" // Clear error
        }
    }
}
```

### **Value Formatting Helpers**

```go
// Universal byte formatting
func (m *FormModel) 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
func (m *FormModel) 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)
}
```

### **EnvVar Integration Pattern**

```go
// Helper to create field from EnvVar
addFieldFromEnvVar := func(suffix, key, title, description string) {
    envVar, _ := m.controller.GetVar(m.getEnvVarName(suffix))

    // Track if field was initially set for cleanup logic
    m.initiallySetFields[key] = envVar.Value != ""

    if key == "preserve_last" || key == "use_qa" {
        m.addBooleanField(key, title, description, envVar)
    } else {
        // Determine validation ranges
        var min, max int
        switch key {
        case "last_sec_bytes", "max_qa_bytes":
            min, max = 1024, 1048576 // 1KB to 1MB
        case "max_bp_bytes":
            min, max = 1024, 524288 // 1KB to 512KB
        default:
            min, max = 0, 999999
        }
        m.addIntegerField(key, title, description, envVar, min, max)
    }
}
```

### **Field Cleanup Pattern**

```go
func (m *FormModel) saveConfiguration() (tea.Model, tea.Cmd) {
    // First pass: Handle fields that were cleared (remove from environment)
    for _, field := range m.fields {
        value := field.Input.Value()

        // If field was initially set but now empty, remove it
        if value == "" && m.initiallySetFields[field.Key] {
            envVarName := m.getEnvVarName(getEnvSuffixFromKey(field.Key))

            // Remove the environment variable
            if err := m.controller.SetVar(envVarName, ""); err != nil {
                logger.Errorf("[FormModel] SAVE: error clearing %s: %v", envVarName, err)
                return m, nil
            }
            logger.Log("[FormModel] SAVE: cleared %s", envVarName)
        }
    }

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

        // Validate and save
        envVarName := m.getEnvVarName(getEnvSuffixFromKey(field.Key))
        if err := m.controller.SetVar(envVarName, value); err != nil {
            logger.Errorf("[FormModel] SAVE: error setting %s: %v", envVarName, err)
            return m, nil
        }
    }

    m.hasChanges = false
    return m, nil
}
```

### **Token Estimation Pattern**

```go
func (m *FormModel) calculateTokenEstimate() string {
    // Get current form values or defaults
    useQAVal := m.getBoolValueOrDefault("use_qa")
    lastSecBytesVal := m.getIntValueOrDefault("last_sec_bytes")
    maxQABytesVal := m.getIntValueOrDefault("max_qa_bytes")
    keepQASectionsVal := m.getIntValueOrDefault("keep_qa_sections")

    var estimatedBytes int

    // Algorithm-specific calculations
    if m.summarizerType == "assistant" {
        estimatedBytes = keepQASectionsVal * lastSecBytesVal
    } else {
        if useQAVal {
            basicSize := keepQASectionsVal * lastSecBytesVal
            if basicSize > maxQABytesVal {
                estimatedBytes = maxQABytesVal
            } else {
                estimatedBytes = basicSize
            }
        } else {
            estimatedBytes = keepQASectionsVal * lastSecBytesVal
        }
    }

    // Convert to tokens with overhead
    estimatedTokens := int(float64(estimatedBytes) * 1.1 / 4) // 4 bytes per token + 10% overhead

    return fmt.Sprintf("~%s tokens", m.formatNumber(estimatedTokens))
}

// Helper methods to get form values or defaults
func (m *FormModel) getBoolValueOrDefault(key string) bool {
    // First check form field value
    for _, field := range m.fields {
        if field.Key == key && field.Input.Value() != "" {
            return field.Input.Value() == "true"
        }
    }

    // Return default value from EnvVar
    envVar, _ := m.controller.GetVar(m.getEnvVarName(getEnvSuffixFromKey(key)))
    return envVar.Default == "true"
}
```

### **Current Configuration Display Pattern**

```go
func (m *TypesModel) renderTypeInfo() string {
    selectedType := m.types[m.selectedIndex]

    // Helper functions for value retrieval
    getIntValue := func(varName string) int {
        var prefix string
        if selectedType.ID == "assistant" {
            prefix = "ASSISTANT_SUMMARIZER_"
        } else {
            prefix = "SUMMARIZER_"
        }

        envVar, _ := m.controller.GetVar(prefix + varName)
        if envVar.Value != "" {
            if val, err := strconv.Atoi(envVar.Value); err == nil {
                return val
            }
        }
        // Use default if value is empty or invalid
        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, "")

    lastSecBytes := getIntValue("LAST_SEC_BYTES")
    maxBPBytes := getIntValue("MAX_BP_BYTES")
    preserveLast := getBoolValue("PRESERVE_LAST")

    sections = append(sections, fmt.Sprintf("• Last Section Size: %s", formatBytes(lastSecBytes)))
    sections = append(sections, fmt.Sprintf("• Max Body Pair Size: %s", formatBytes(maxBPBytes)))
    sections = append(sections, fmt.Sprintf("• Preserve Last: %t", preserveLast))

    // Type-specific fields
    if selectedType.ID == "general" {
        useQA := getBoolValue("USE_QA")
        sections = append(sections, fmt.Sprintf("• Use QA Pairs: %t", useQA))
    }

    // Token estimation in info panel
    sections = append(sections, "")
    sections = append(sections, m.styles.Subtitle.Render("Token Estimation"))
    sections = append(sections, fmt.Sprintf("• Estimated context size: ~%s tokens", formatNumber(estimatedTokens)))

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

### **Enhanced Localization Pattern**

```go
// Field-specific descriptions with validation hints
const (
    SummarizerFormLastSecBytes     = "Last Section Size (bytes)"
    SummarizerFormLastSecBytesDesc = "Maximum byte size for each preserved conversation section"

    // Enhanced help with practical guidance
    SummarizerFormGeneralHelp = `Balance information depth vs model performance.

Reduce these settings if:
• Using models with ≤64K context (Open Source Reasoning Models)
• Getting "context too long" errors
• Responses become vague or unfocused with long conversations

Key Settings Impact:
• Last Section Size: Larger = more detail, but uses more tokens
• Keep QA Sections: More sections = better continuity, higher token usage

Recommended Adjustments:
• Open Source Reasoning Models: Reduce Last Section to 25-35KB, Keep QA to 1
• OpenAI/Anthropic/Google: Default settings work well`
)
```

### **Type-Based Dynamic Field Generation**

```go
func (m *FormModel) buildForm() {
    // Set type-specific name
    switch m.summarizerType {
    case "general":
        m.typeName = locale.SummarizerTypeGeneralName
    case "assistant":
        m.typeName = locale.SummarizerTypeAssistantName
    }

    // Common fields for all types
    addFieldFromEnvVar("PRESERVE_LAST", "preserve_last", locale.SummarizerFormPreserveLast, locale.SummarizerFormPreserveLastDesc)

    // Type-specific fields
    if m.summarizerType == "general" {
        addFieldFromEnvVar("USE_QA", "use_qa", locale.SummarizerFormUseQA, locale.SummarizerFormUseQADesc)
        addFieldFromEnvVar("SUM_MSG_HUMAN_IN_QA", "sum_human_in_qa", locale.SummarizerFormSumHumanInQA, locale.SummarizerFormSumHumanInQADesc)
    }

    // Common configuration fields
    addFieldFromEnvVar("LAST_SEC_BYTES", "last_sec_bytes", locale.SummarizerFormLastSecBytes, locale.SummarizerFormLastSecBytesDesc)
    // ... additional fields
}
```

These patterns provide a robust foundation for implementing advanced configuration forms with:
- **Type Safety**: Validation at input time
- **User Experience**: Auto-completion, formatting, real-time feedback
- **Resource Awareness**: Token estimation and optimization guidance
- **Environment Integration**: Proper handling of defaults and cleanup
- **Maintainability**: Centralized helpers and consistent patterns

### **Implementation Guidelines for Future Screens**

#### **Langfuse Integration Forms**

**Ready Patterns**: Based on locale constants, implement:
- **Deployment Type Selection**: Embedded/External/Disabled pattern (similar to summarizer types)
- **Conditional Fields**: Show admin fields only for embedded deployment
- **Connection Testing**: Validate external server connectivity
- **Environment Variables**: `LANGFUSE_*` prefix pattern with cleanup

```go
// Implementation pattern
func (m *LangfuseFormModel) buildForm() {
    // Deployment type field (radio-style selection)
    m.addDeploymentTypeField("deployment_type", locale.LangfuseDeploymentType, locale.LangfuseDeploymentTypeDesc)

    // Conditional fields based on deployment type
    if m.deploymentType == "external" {
        m.addFieldFromEnvVar("LANGFUSE_BASE_URL", "base_url", locale.LangfuseBaseURL, locale.LangfuseBaseURLDesc)
        m.addFieldFromEnvVar("LANGFUSE_PROJECT_ID", "project_id", locale.LangfuseProjectID, locale.LangfuseProjectIDDesc)
        m.addFieldFromEnvVar("LANGFUSE_PUBLIC_KEY", "public_key", locale.LangfusePublicKey, locale.LangfusePublicKeyDesc)
        m.addMaskedFieldFromEnvVar("LANGFUSE_SECRET_KEY", "secret_key", locale.LangfuseSecretKey, locale.LangfuseSecretKeyDesc)
    } else if m.deploymentType == "embedded" {
        // Admin configuration for embedded instance
        m.addFieldFromEnvVar("LANGFUSE_ADMIN_EMAIL", "admin_email", locale.LangfuseAdminEmail, locale.LangfuseAdminEmailDesc)
        m.addMaskedFieldFromEnvVar("LANGFUSE_ADMIN_PASSWORD", "admin_password", locale.LangfuseAdminPassword, locale.LangfuseAdminPasswordDesc)
        m.addFieldFromEnvVar("LANGFUSE_ADMIN_NAME", "admin_name", locale.LangfuseAdminName, locale.LangfuseAdminNameDesc)
    }
}
```

#### **Observability Integration Forms**

**Ready Patterns**: Monitoring stack configuration with similar architecture:
- **Deployment Selection**: Embedded/External/Disabled (reuse pattern)
- **External Collector**: OpenTelemetry endpoint configuration
- **Service Selection**: Enable/disable individual monitoring components
- **Resource Estimation**: Calculate monitoring overhead

```go
// Environment variables pattern
func (m *ObservabilityFormModel) getEnvVarName(suffix string) string {
    if m.deploymentType == "external" {
        return "OTEL_" + suffix
    }
    return "OBSERVABILITY_" + suffix
}
```

#### **Security Configuration**
**Potential Patterns**: Based on established architecture:
- **Certificate Management**: File path inputs with validation
- **Access Control**: Boolean enable/disable with role configuration
- **Network Security**: Port ranges, IP allowlists with validation
- **Encryption Settings**: Key generation, algorithm selection

#### **Enhanced Troubleshooting**
**AI-Powered Diagnostics**:
- **System Analysis**: Real-time health checks with recommendations
- **Log Analysis**: Parse error logs and suggest solutions
- **Configuration Validation**: Cross-check settings for conflicts
- **Interactive Fixes**: Guided repair workflows

### **Screen Development Template**

#### **Type Selection Screen Pattern**

```go
type TypesModel struct {
    controller *controllers.StateController
    types      []TypeInfo
    selectedIndex int
    args       []string
}

// Universal type info structure
type TypeInfo struct {
    ID          string
    Name        string
    Description string
}

// Current configuration display (reusable pattern)
func (m *TypesModel) renderCurrentConfiguration(selectedType TypeInfo) string {
    sections = append(sections, m.styles.Subtitle.Render("Current Configuration"))

    // Type-specific value retrieval
    getValue := func(suffix string) string {
        envVar, _ := m.controller.GetVar(m.getEnvVarName(selectedType.ID, suffix))
        if envVar.Value != "" {
            return envVar.Value
        }
        return envVar.Default + " (default)"
    }

    // Display current settings with formatting
    sections = append(sections, fmt.Sprintf("• Setting 1: %s", getValue("SETTING_1")))
    sections = append(sections, fmt.Sprintf("• Setting 2: %s", formatBytes(getIntValue("SETTING_2"))))

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

#### **Form Screen Pattern**

```go
type FormModel struct {
    // Standard form architecture
    controller         *controllers.StateController
    configType        string
    fields            []FormField
    initiallySetFields map[string]bool
    viewport          viewport.Model

    // Pattern-specific additions
    resourceEstimation string
    validationErrors   map[string]string
}

// Universal form building
func (m *FormModel) buildForm() {
    m.fields = []FormField{}
    m.initiallySetFields = make(map[string]bool)

    // Type-specific field generation
    switch m.configType {
    case "type1":
        m.addCommonFields()
        m.addType1SpecificFields()
    case "type2":
        m.addCommonFields()
        m.addType2SpecificFields()
    }

    // Focus and content update
    if len(m.fields) > 0 {
        m.fields[0].Input.Focus()
    }
    m.updateFormContent()
}

// Resource calculation (reusable pattern)
func (m *FormModel) calculateResourceEstimate() string {
    // Get current values from form or defaults
    setting1 := m.getIntValueOrDefault("setting1")
    setting2 := m.getBoolValueOrDefault("setting2")

    // Algorithm-specific calculation
    var estimate int
    if setting2 {
        estimate = setting1 * 2
    } else {
        estimate = setting1
    }

    return fmt.Sprintf("~%s", m.formatNumber(estimate))
}
```

These templates ensure consistency across all future configuration screens while leveraging the proven patterns from summarizer and LLM provider implementations.

## New Implementation Architecture

### **Controller Layer Design**

- **StateController**: Central bridge between TUI forms and state persistence
- **Purpose**: Abstracts environment variable management from UI components
- **Benefits**: Type-safe configuration, automatic validation, dirty state tracking
- **Integration**: All form screens use controller instead of direct state access

### **Adaptive Layout Strategy**

- **Right Panel Hiding**: Main innovation for responsive design
- **Breakpoint Logic**: `contentWidth < (MinMenuWidth + MinInfoWidth + 8)`
- **Graceful Degradation**: Information still accessible, just condensed
- **Performance**: No complex re-rendering, simple layout switching

### **Form Architecture with Bubbles**

- **textinput.Model**: Used for all form inputs with consistent styling
- **Masked Input Toggle**: Ctrl+H to show/hide sensitive values
- **Field Navigation**: Tab/Shift+Tab for keyboard-only navigation
- **Real-time Validation**: Immediate feedback and dirty state tracking
- **Provider-Specific Forms**: Dynamic field generation based on provider type
- **State Persistence**: Composite ScreenIDs with `§` separator (`llm_provider_form§openai`)

### **Composite ScreenID Architecture**

- **Format**: `"screen"` or `"screen§arg1§arg2§..."` for parameterized screens
- **Helper Methods**: `GetScreen()`, `GetArgs()`, `CreateScreenID()`
- **State Persistence**: Complete navigation stack with arguments preserved
- **Benefits**: Type-safe parameter passing, automatic state restoration

```go
// Example: LLM Provider Form with specific provider
targetScreen := CreateScreenID("llm_provider_form", "gemini")
// Results in: "llm_provider_form§gemini"

// Navigation preserves arguments
return NavigationMsg{Target: targetScreen}

// On app restart, user returns to Gemini form, not default OpenAI
```

### **File Organization Pattern**

- **One Model Per File**: `welcome.go`, `eula.go`, `main_menu.go`, etc.
- **Shared Constants**: All in `types.go` for type safety
- **Locale Centralization**: All user-visible text in `locale/locale.go`
- **Controller Separation**: Business logic isolated from presentation

## Architecture & Design Patterns

### 1. Unified App Architecture

**Central Orchestrator (`app.go`)**:
- **Navigation Management**: Stack-based navigation with step persistence
- **Screen Lifecycle**: Model creation, initialization, and cleanup
- **Unified Layout**: Header and footer rendering for all screens
- **Global Event Handling**: ESC, Ctrl+C, window resize
- **Dimension Management**: Terminal size distribution to models

```go
// UNIFIED RENDERING - All screens follow this pattern:
func (a *App) View() string {
    header := a.renderHeader()  // Screen-specific header
    footer := a.renderFooter()  // Dynamic footer with actions
    content := a.currentModel.View()  // Model provides content only

    // App.go calculates and enforces layout constraints
    contentHeight := max(height - headerHeight - footerHeight, 0)
    contentArea := a.styles.Content.Height(contentHeight).Render(content)

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

### 2. Navigation & State Management

#### Navigation Rules (Universal)

- **ESC Behavior**: ALWAYS returns to Welcome screen from any screen (never nested back navigation)
- **Type Safety**: Use `ScreenID` with `CreateScreenID()` for parameterized screens
- **Composite Support**: Screens can carry arguments via `§` separator
- **State Persistence**: Complete navigation stack with arguments preserved
- **EULA Consent**: Check `GetEulaConsent()` on Welcome→EULA transition, call `SetEulaConsent()` on acceptance

```go
// Type-safe navigation structure with composite support
type NavigationMsg struct {
    Target ScreenID  // Can be simple or composite
    GoBack bool
}

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:]
}

// Navigation with parameters
targetScreen := CreateScreenID("llm_provider_form", "anthropic")
return NavigationMsg{Target: targetScreen}

// Universal ESC implementation
case "esc":
    if a.navigator.Current().GetScreen() != string(models.WelcomeScreen) {
        a.navigator.stack = []models.ScreenID{models.WelcomeScreen}
        a.navigator.stateManager.SetStack([]string{"welcome"})
        a.currentModel = a.createModelForScreen(models.WelcomeScreen, nil)
        return a, a.currentModel.Init()
    }
```

#### State Integration

- `state.State` remains authoritative for env variables
- Controllers translate between TUI models and state operations
- Complete state reset in `Init()` for predictable behavior

### 3. Layout & Responsive Design

#### Constants & Breakpoints

```go
// Layout Constants
const (
    SmallScreenThreshold = 30    // Height threshold for viewport mode
    MinTerminalWidth = 80        // Minimum width for horizontal layout
    MinPanelWidth = 25           // Panel width constraints
    WelcomeHeaderHeight = 8      // Fixed by ASCII Art Logo (8 lines)
    EULAHeaderHeight = 3         // Title + subtitle + spacing
    FooterHeight = 1             // Always 1 line with background approach
)
```

#### Responsive Breakpoints

- **Small screens**: < 30 rows → viewport mode for scrolling
- **Large screens**: ≥ 30 rows → normal layout mode
- **Narrow terminals**: < 80 cols → vertical stacking
- **Wide terminals**: ≥ 80 cols → horizontal panel layout

#### Height Control (CRITICAL)

```go
// ❌ WRONG - Height() sets MINIMUM height, can expand
style.Height(1).Border(lipgloss.Border{Top: true})

// ✅ CORRECT - Background approach ensures exactly 1 line
style.Background(borderColor).Foreground(textColor).Padding(0,1,0,1)
```

### 4. Footer & Header Systems

#### Unified Footer Strategy

**Background Approach (Production-Ready)**:
- Always exactly 1 line regardless of terminal size
- Modern appearance with background color
- Reliable height calculations
- Dynamic actions based on screen state

```go
// Footer pattern implementation
actions := locale.BuildCommonActions()
if specificCondition {
    actions = append(actions, locale.SpecificAction)
}
footerText := strings.Join(actions, locale.NavSeparator)

return lipgloss.NewStyle().
    Width(width).
    Background(styles.Border).
    Foreground(styles.Foreground).
    Padding(0, 1, 0, 1).
    Render(footerText)
```

#### Header Strategy

- **Welcome Screen**: ASCII Art Logo (8 lines height)
- **Other Screens**: Text title with consistent styling
- **Responsive**: Always present, managed by `app.go`

### 5. Scrolling & Input Handling

#### Modern Scroll Methods

```go
viewport.ScrollUp(1)     // Replaces deprecated LineUp()
viewport.ScrollDown(1)   // Replaces deprecated LineDown()
viewport.ScrollLeft(2)   // Horizontal scroll (2 steps for faster navigation)
viewport.ScrollRight(2)  // Horizontal scroll (2 steps for faster navigation)
```

#### Essential Key Handling

- **↑/↓**: Vertical scrolling (1 line per press)
- **←/→**: Horizontal scrolling (2 steps per press for faster navigation)
- **PgUp/PgDn**: Page-level scrolling
- **Home/End**: Jump to beginning/end

### 6. Content & Resource Management

#### Shared Renderer (Prevents Freezing)
```go
// Single renderer instance in styles.New()
type Styles struct {
    renderer *glamour.TermRenderer
    width    int
    height   int
}

// Usage pattern
rendered, err := m.styles.GetRenderer().Render(markdown)
if err != nil {
    // Fallback to plain text
    rendered = fmt.Sprintf("# Content\n\n%s\n\n*Render error: %v*", content, err)
}
```

#### Content Loading Strategy

- Single renderer instance prevents glamour freezing
- Reset model state completely in `Init()` for clean transitions
- Force view update after content loading with no-op command
- Use embedded files via `files.GetContent()` - handles working directory variations

### 7. Component Architecture

#### Component Types

1. **Welcome Screen**: ASCII art, system checks, info display
2. **EULA Screen**: Markdown viewer with scroll-to-accept
3. **Menu Screen**: Main navigation with dynamic availability
4. **Form Screens**: Configuration input with validation
5. **Status Screens**: Progress and result display

#### Key Components

- **StatusIndicator**: System check results with green checkmarks/red X's
- **MarkdownViewer**: EULA and help text display with scroll support
- **FormController**: Bridges huh forms with state package
- **MenuList**: Dynamic menu with availability checking

### 8. Localization & Styling

#### Localization Structure

```
wizard/locale/
└── locale.go    # All user-visible text constants
```

**Naming Convention**:
- `Welcome*`, `EULA*`, `Menu*`, `LLM*`, `Checks*` - Screen-specific
- `Nav*`, `Status*`, `Error*`, `UI*` - Functional prefixes

#### Styles Centralization

- Single styles instance with shared renderer and dimensions
- Prevents glamour freezing, centralizes terminal size management
- All models access dimensions via `m.styles.GetSize()`

## Development Guidelines

### Screen Model Requirements

#### Required Implementation Pattern

```go
// REQUIRED: State reset in Init()
func (m *Model) Init() tea.Cmd {
    logger.Log("[Model] INIT")
    m.content = ""
    m.ready = false
    // ... reset ALL state
    return m.loadContent
}

// REQUIRED: Dimension handling via styles
func (m *Model) updateViewport() {
    width, height := m.styles.GetSize()
    if width <= 0 || height <= 0 {
        return
    }
    // ... viewport logic
}

// REQUIRED: Adaptive layout methods
func (m *Model) isVerticalLayout() bool {
    return m.styles.GetWidth() < MinTerminalWidth
}
```

### Screen Development Checklist

**For each new screen:**
- [ ] Type-safe `ScreenID` defined in `types.go`
- [ ] State reset in `Init()` method with logger
- [ ] Dimension handling via `m.styles.GetSize()`
- [ ] Modern `Scroll*` methods for navigation
- [ ] Arrow key handling (↑/↓/←/→) with 2-step horizontal
- [ ] Background footer approach using locale helpers
- [ ] Shared renderer from `styles.GetRenderer()`
- [ ] ESC navigation to Welcome screen
- [ ] Logger integration for debug output

### Code Style Guidelines

#### Compact vs Expanded Style

```go
// ✅ Compact where appropriate:
leftWidth = max(leftWidth, MinPanelWidth)
return lipgloss.NewStyle().Width(width).Padding(0, 2, 0, 2).Render(content)

// ✅ Expanded where needed:
coreChecks := []struct {
    label string
    value bool
}{
    {locale.CheckEnvironmentFile, m.checker.EnvFileExists},
    {locale.CheckDockerAPI, m.checker.DockerApiAccessible},
}
```

#### Comment Guidelines

- Comments explain **why** and **how**, not **what**
- Place comments where code might raise questions about business logic
- Avoid redundant comments that repeat obvious code behavior

## Recent Fixes & Improvements

### ✅ **Composite ScreenID Navigation System**
**Problem**: Need to preserve selected menu items and provider selections across navigation
**Solution**: Implemented composite ScreenIDs with `§` separator for parameter passing

**Features**:
```go
// Composite ScreenID examples
"main_menu§llm_providers"           // Main menu with "llm_providers" selected
"llm_providers§gemini"              // Providers list with "gemini" selected
"llm_provider_form§anthropic"       // Form for "anthropic" provider
```

**Benefits**:
- Type-safe parameter passing via `GetScreen()`, `GetArgs()`, `CreateScreenID()`
- Automatic state restoration - user returns to exact selection after ESC
- Clean navigation stack with full context preservation
- Extensible for multiple arguments per screen

### ✅ **Complete Localization Architecture**

**Problem**: Hardcoded strings scattered throughout UI components
**Solution**: Centralized all user-visible text in `locale.go` with structured constants

**Implementation**:
```go
// Multi-line text stored as single constants
const MainMenuLLMProvidersInfo = `Configure AI language model providers for PentAGI.

Supported providers:
• OpenAI (GPT-4, GPT-3.5-turbo)
• Anthropic (Claude-3, Claude-2)
...`

// Usage in components
sections = append(sections, m.styles.Paragraph.Render(locale.MainMenuLLMProvidersInfo))
```

**Coverage**: 100% of user-facing text moved to locale constants
- Menu descriptions and help text
- Form labels and error messages
- Provider-specific documentation
- Keyboard shortcuts and hints

### ✅ **Viewport-Based Form Scrolling**
**Problem**: Forms with many fields don't fit on smaller terminals
**Solution**: Implemented auto-scrolling viewport with focus tracking

**Based on research**: [BubbleTea viewport best practices](https://pkg.go.dev/github.com/charmbracelet/bubbles/viewport) and [Perplexity guidance on form scrolling](https://www.inngest.com/blog/interactive-clis-with-bubbletea)

**Key Features**:
- **Auto-scroll**: Focused field automatically stays visible
- **Smart positioning**: Calculates field heights for precise scroll positioning
- **Seamless navigation**: Tab/Shift+Tab scroll form as needed
- **No extra hotkeys**: Uses existing navigation keys

**Technical Implementation**:
```go
// Auto-scroll on field focus change
func (m *Model) ensureFocusVisible() {
    focusY := m.calculateFieldPosition(m.focusedIndex)
    if focusY < m.viewport.YOffset {
        m.viewport.YOffset = focusY  // Scroll up
    }
    if focusY >= m.viewport.YOffset + m.viewport.Height {
        m.viewport.YOffset = focusY - m.viewport.Height + 1  // Scroll down
    }
}
```

### ✅ **Enhanced Provider Configuration**

**Problem**: Missing configuration fields for several LLM providers
**Solution**: Added complete field sets for all supported providers

**Provider Field Mapping**:
- **OpenAI/Anthropic/Gemini**: Base URL + API Key
- **AWS Bedrock**: Region + Authentication (Default Auth OR Bearer Token OR Access Key + Secret Key) + Session Token (optional) + Base URL (optional)
- **DeepSeek/GLM/Kimi/Qwen**: Base URL + API Key + Provider Name (optional, for LiteLLM)
- **Ollama**: Base URL + API Key (optional, for cloud) + Model + Config Path + Pull/Load options
- **Custom**: Base URL + API Key + Model + Config Path + Provider Name + Legacy Reasoning (boolean)

**Dynamic Form Generation**: Forms adapt based on provider type with appropriate validation and help text.

## Error Handling & Performance

### Error Handling Strategy

**Graceful degradation with user-friendly messages**:
1. System check failures: Show specific resolution steps
2. Form validation: Real-time feedback with clear messaging
3. State persistence errors: Allow retry with explanation
4. Network issues: Offer offline/manual alternatives

### Performance Considerations

**Lazy loading approach**:
- System checks run asynchronously after welcome screen loads
- Markdown content loaded on-demand when screens are accessed
- Form validation debounced to avoid excessive state updates

## Common Pitfalls & Solutions

### Content Loading Issues

**Problem**: "Loading EULA" state persists, content doesn't appear
**Solutions**:
1. **Multiple Path Fallback**: Try embedded FS first, then direct file access
2. **State Reset**: Always reset model state in `Init()` for clean loading
3. **No ClearScreen**: Avoid `tea.ClearScreen` during navigation
4. **Force View Update**: Return no-op command after content loading

### Layout Consistency Issues

**Problem**: Layout breaks on terminal resize
**Solution**: Always account for actual footer height (1 line)

```go
// Consistent height calculation across all screens
headerHeight := 3 // Fixed based on content
footerHeight := 1 // Background approach always 1 line
contentHeight := m.height - headerHeight - footerHeight
```

### Common Mistakes to Avoid
- Using `tea.ClearScreen` in navigation
- Border-based footer (height inconsistency)
- String-based navigation messages
- Creating new glamour renderer instances
- Forgetting state reset in `Init()`
- Using `fmt.Printf` for debug output
- Deprecated `Line*` scroll methods

## Technology Stack

- **bubbletea**: Core TUI framework using Model-View-Update pattern
- **lipgloss**: Styling and layout engine for visual presentation
- **bubbles**: Component library for interactive elements (list, textinput, viewport)
- **huh**: Form builder for structured input collection (future screens)
- **glamour**: Markdown rendering with single shared instance
- **logger**: Custom file-based logging for TUI-safe development

## ✅ **Production Architecture Implementation**

### **Completed Form System Architecture**

#### **Form Model Pattern (llm_provider_form.go)**

```go
type LLMProviderFormModel struct {
    controller *controllers.StateController
    styles     *styles.Styles
    window     *window.Window

    // Form state
    providerID   string
    fields       []FormField
    focusedIndex int
    showValues   bool
    hasChanges   bool
    args         []string // From composite ScreenID

    // Permanent viewport for scroll state
    viewport     viewport.Model
    formContent  string
    fieldHeights []int
}
```

**Key Implementation Decisions**:
- **Args-based Construction**: `NewLLMProviderFormModel(controller, styles, window, args)`
- **Permanent Viewport**: Form viewport as struct property to preserve scroll state
- **Auto-completion**: Tab key triggers suggestion completion for boolean fields
- **GoBack Navigation**: `return NavigationMsg{GoBack: true}` prevents navigation loops

#### **Navigation Hotkeys (Production Pattern)**

```go
// Modern form navigation
case "down":    // ↓: Next field + auto-scroll
case "up":      // ↑: Previous field + auto-scroll
case "tab":     // Tab: Complete suggestion (true/false for booleans)
case "ctrl+h":  // Ctrl+H: Toggle show/hide masked values
case "ctrl+s":  // Ctrl+S: Save configuration
case "enter":   // Enter: Save and return via GoBack
```

**Important**: Tab navigation replaced with suggestion completion. Field navigation uses ↑/↓ only.

### **Adaptive Layout System**

#### **Layout Constants (Production Values)**

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

#### **Two-Column Layout Implementation**

```go
func (m *Model) renderHorizontalLayout(leftPanel, rightPanel string, width, height int) string {
    leftWidth, rightWidth := MinMenuWidth, MinInfoWidth
    extraWidth := width - leftWidth - rightWidth - PaddingWidth

    // Distribute extra space intelligently
    if extraWidth > 0 {
        leftWidth = min(leftWidth+extraWidth/2, MaxMenuWidth)  // Cap at 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)

    // Final layout viewport (temporary)
    viewport := viewport.New(width, height-PaddingHeight)
    viewport.SetContent(lipgloss.JoinHorizontal(lipgloss.Top, leftStyled, rightStyled))
    return viewport.View()
}
```

#### **Content Hiding Strategy**

```go
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)

    // Show both panels if they fit
    if lipgloss.Height(leftStyled)+lipgloss.Height(rightStyled)+2 < height {
        return lipgloss.JoinVertical(lipgloss.Left,
            leftStyled,
            verticalStyle.Height(1).Render(""),
            rightStyled,
        )
    }

    // Hide right panel if insufficient space - show only essential content
    return leftStyled
}
```

### **Composite ScreenID Navigation System**

#### **ScreenID Argument Packaging**

```go
// Navigation with selection preservation
func (m *MainMenuModel) handleMenuSelection() (tea.Model, tea.Cmd) {
    selectedItem := m.getSelectedItem()

    return m, func() tea.Msg {
        return NavigationMsg{
            Target: CreateScreenID(string(targetScreen), selectedItem.ID),
        }
    }
}

// Result: "llm_providers§openai" -> llm_providers screen with "openai" pre-selected
```

#### **Args-Based Model Construction**

```go
// No SetSelected* methods needed - selection from constructor
func NewLLMProvidersModel(
    controller *controllers.StateController, styles *styles.Styles,
    window *window.Window, args []string,
) *LLMProvidersModel {
    return &LLMProvidersModel{
        controller: controller,
        args:       args,  // Selection restored from args in Init()
    }
}

func (m *LLMProvidersModel) Init() tea.Cmd {
    // Automatic selection restoration from args[1]
    if len(m.args) > 1 && m.args[1] != "" {
        for i, provider := range m.providers {
            if provider.ID == m.args[1] {
                m.selectedIndex = i
                break
            }
        }
    }
    return nil
}
```

#### **Navigation Stack Management**

**Stack Example**: `["main_menu§llm_providers", "llm_providers§openai", "llm_provider_form§openai"]`

- **Forward Navigation**: Pushes composite ScreenID with arguments
- **Back Navigation**: `GoBack: true` pops current screen, returns to previous with preserved selection
- **No Navigation Loops**: GoBack pattern prevents infinite stack growth

### **Viewport Usage Patterns**

#### **Forms: Permanent Viewport Property**

```go
// ✅ CORRECT: Form viewport as struct property
type FormModel struct {
    viewport viewport.Model  // Preserves scroll position across updates
}

func (m *FormModel) ensureFocusVisible() {
    // Auto-scroll to focused field
    focusY := m.calculateFieldPosition(m.focusedIndex)
    if focusY < m.viewport.YOffset {
        m.viewport.YOffset = focusY
    }
    if focusY+m.fieldHeights[m.focusedIndex] >= offset+visibleRows {
        m.viewport.YOffset = focusY + m.fieldHeights[m.focusedIndex] - visibleRows + 1
    }
}
```

#### **Layout: Temporary Viewport Creation**

```go
// ✅ CORRECT: Layout viewport created for rendering only
func (m *Model) renderHorizontalLayout(left, right string, width, height int) string {
    content := lipgloss.JoinHorizontal(lipgloss.Top, leftStyled, rightStyled)

    vp := viewport.New(width, height-PaddingHeight)  // Temporary
    vp.SetContent(content)
    return vp.View()
}
```

### **Dynamic Form Field Architecture**

#### **Field Configuration Pattern**

```go
// Clean input setup without fixed width
func (m *FormModel) addInputField(fieldType string) {
    input := textinput.New()
    input.Prompt = ""  // Clean appearance
    input.PlaceholderStyle = m.styles.FormPlaceholder

    // Width set dynamically during updateFormContent()
    // NOT set here: input.Width = 50

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

#### **Dynamic Width Calculation**

```go
func (m *FormModel) getInputWidth() int {
    viewportWidth, _ := m.getViewportSize()
    inputWidth := viewportWidth - 6  // Standard padding
    if m.isVerticalLayout() {
        inputWidth = viewportWidth - 4  // Tighter in vertical mode
    }
    return inputWidth
}

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

    for i, field := range m.fields {
        field.Input.Width = inputWidth - 3  // Account for border/cursor
        field.Input.SetValue(field.Input.Value())  // Trigger width update

        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)
    }
}
```

### **Provider Configuration Architecture**

#### **Simplified Status Model**

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

// Status check via controller
configs := m.controller.GetLLMProviders()
provider := ProviderInfo{
    Configured: configs["openai"].Configured,  // Controller determines status
}
```

**Removed**: Dual `Configured`/`Enabled` status - controller handles enable/disable logic internally.

#### **Provider-Specific Field Sets**
- **OpenAI/Anthropic/Gemini**: Base URL + API Key
- **AWS Bedrock**: Region + Authentication (Default Auth OR Bearer Token OR Access Key + Secret Key) + Session Token (optional) + Base URL (optional)
  - **Default Auth**: Use AWS SDK credential chain (environment, EC2 role, ~/.aws/credentials) - highest priority
  - **Bearer Token**: Token-based authentication - priority over static credentials
  - **Static Credentials**: Access Key + Secret Key + Session Token (optional) - traditional IAM authentication
- **DeepSeek/GLM/Kimi/Qwen**: Base URL + API Key + Provider Name (optional, for LiteLLM)
- **Ollama**: Base URL + API Key (optional, for cloud) + Model + Config Path + Pull/Load options
- **Custom**: Base URL + API Key + Model + Config Path + Provider Name + Legacy/Preserve Reasoning (boolean with suggestions)

### **Screen Architecture (App.go Integration)**

#### **Content Area Responsibility**

```go
// ✅ Screen models handle ONLY content area
func (m *Model) View() string {
    leftPanel := m.renderForm()
    rightPanel := m.renderHelp()

    // Adaptive layout decision
    if m.isVerticalLayout() {
        return m.renderVerticalLayout(leftPanel, rightPanel, width, height)
    }
    return m.renderHorizontalLayout(leftPanel, rightPanel, width, height)
}
```

#### **App.go Layout Management**

```go
// App.go handles complete layout structure
func (a *App) View() string {
    header := a.renderHeader()    // Screen-specific (logo or title)
    footer := a.renderFooter()    // Dynamic actions based on screen
    content := a.currentModel.View()  // Content only from model

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

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

### **Navigation Anti-Patterns & Solutions**

#### **❌ Common Mistakes**

```go
// ❌ WRONG: Direct navigation creates loops
func (m *FormModel) saveAndReturn() (tea.Model, tea.Cmd) {
    m.saveConfiguration()
    return m, func() tea.Msg {
        return NavigationMsg{Target: LLMProvidersScreen}  // Loop!
    }
}

// ❌ WRONG: Separate SetSelected methods
func (m *Model) SetSelectedProvider(providerID string) {
    // Complexity - removed in favor of args-based construction
}

// ❌ WRONG: Fixed input widths
input.Width = 50  // Breaks responsive design
```

#### **✅ Correct Patterns**

```go
// ✅ CORRECT: GoBack navigation
func (m *FormModel) saveAndReturn() (tea.Model, tea.Cmd) {
    if err := m.saveConfiguration(); err != nil {
        return m, nil  // Stay on form if save fails
    }
    return m, func() tea.Msg {
        return NavigationMsg{GoBack: true}  // Return to previous screen
    }
}

// ✅ CORRECT: Args-based selection
func NewModel(..., args []string) *Model {
    selectedIndex := 0
    if len(args) > 1 && args[1] != "" {
        // Set selection from args during construction
        for i, item := range items {
            if item.ID == args[1] {
                selectedIndex = i
                break
            }
        }
    }
    return &Model{selectedIndex: selectedIndex, args: args}
}

// ✅ CORRECT: Dynamic input sizing
func (m *FormModel) updateFormContent() {
    inputWidth := m.getInputWidth()  // Calculate based on available space
    field.Input.Width = inputWidth - 3
}
```
