# PentAGI Installer Troubleshooting Guide

> Comprehensive troubleshooting guide including recent fixes, performance optimization, and common issues.

## 🚨 **Development-Specific Issues**

### **TUI Application Constraints**
**Problem**: Running installer breaks terminal session during development
**Solution**: Build-only development workflow

```bash
# ✅ CORRECT: Build and test separately
cd backend/
go build -o ../build/installer ./cmd/installer/main.go

# Test in separate terminal session
cd ../build/
./installer

# ❌ WRONG: Running during development
cd backend/
go run ./cmd/installer/main.go  # Breaks active terminal!
```

**Debug Monitoring**:
```bash
# Monitor debug output during development
tail -f log.json | jq '.'

# Filter by component
tail -f log.json | jq 'select(.component == "FormModel")'

# Pretty print timestamps
tail -f log.json | jq -r '"\(.timestamp) [\(.level)] \(.message)"'
```

## 🔧 **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

**Before** (❌ Problematic):
```go
// Lost selection on navigation
func (m *MenuModel) handleSelection() (tea.Model, tea.Cmd) {
    return NavigationMsg{Target: LLMProvidersScreen} // No context preserved
}
```

**After** (✅ Fixed):
```go
// Preserves selection context
func (m *MenuModel) handleSelection() (tea.Model, tea.Cmd) {
    selectedItem := m.getSelectedItem()
    return NavigationMsg{
        Target: CreateScreenID("llm_providers", selectedItem.ID),
    }
}

// Results in: "llm_providers§openai" - selection preserved
```

**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

### ✅ **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

**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 *FormModel) 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-Specific Field Sets:**

- **OpenAI/Anthropic/Gemini**: Base URL + API Key
- **AWS Bedrock**: Region + Default Auth OR Bearer Token OR (Access Key + Secret Key + Session Token) + Base URL
- **DeepSeek**: Base URL + API Key + Provider Name (for LiteLLM prefix, e.g., 'deepseek')
- **GLM**: Base URL + API Key + Provider Name (for LiteLLM prefix, e.g., 'zai')
- **Kimi**: Base URL + API Key + Provider Name (for LiteLLM prefix, e.g., 'moonshot')
- **Qwen**: Base URL + API Key + Provider Name (for LiteLLM prefix, e.g., 'dashscope')
- **Ollama**: Base URL + API Key (cloud only) + Model + Config Path + Pull/Load settings
  - Local scenario: No API key needed
  - Cloud scenario: API key required from https://ollama.com/settings/keys
- **Custom**: Base URL + API Key + Model + Config Path + Provider Name + Reasoning options

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

## 🔧 **Common Issues & Solutions**

### **macOS Reports the Installer as Malware**

**Symptoms**: macOS blocks a downloaded PentAGI installer and reports that it may contain malware.

**What is currently known**:
- Use only the official PentAGI installer downloads linked from the project README.
- Select the archive that matches your Mac CPU architecture: `darwin/amd64` for Intel Macs or `darwin/arm64` for Apple silicon.
- Maintainer notes in [issue #249](https://github.com/vxcontrol/pentagi/issues/249) report that the installer files are signed with the project certificate and that the checked `arm64` and `amd64` builds had zero VirusTotal detections.
- The maintainers have contacted Apple about the warning and are awaiting a response.

**Recommended handling**:
- Treat any unexpected malware warning as a reason to stop and verify the download source before continuing.
- Re-download the installer from the official PentAGI link if the archive came from a mirror, chat attachment, or any unofficial source.
- If you trust the official release after verification, try to open the installer once. Then open **System Settings** -> **Privacy & Security**, find the security message that says the installer was blocked, click **Open Anyway**, and confirm the follow-up prompt to allow that specific app to run.
- If your version of macOS shows different wording, follow Apple's guidance for opening a Mac app from an unidentified developer: <https://support.apple.com/guide/mac-help/open-a-mac-app-from-an-unidentified-developer-mh40616/mac>.
- Do not reuse an installer archive that was downloaded from an unknown source, even if its filename matches the official archive name.

### **Navigation Issues**

#### **Navigation Stack Corruption**
**Symptoms**: User gets stuck on screens, ESC doesn't work, back navigation fails
**Cause**: Circular navigation patterns or corrupted navigation stack

**Debug**:
```go
func (n *Navigator) debugStack() {
    stackInfo := make([]string, len(n.stack))
    for i, screenID := range n.stack {
        stackInfo[i] = string(screenID)
    }

    logger.LogWithData("Navigation Stack", map[string]interface{}{
        "stack":   stackInfo,
        "current": string(n.Current()),
        "depth":   len(n.stack),
    })
}
```

**Solution**:
```go
// ✅ CORRECT: Use GoBack to prevent 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 m, func() tea.Msg {
        return NavigationMsg{GoBack: true}  // Return to previous screen
    }
}

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

#### **Lost Selection State**
**Symptoms**: Menu selections reset, provider choices forgotten, configuration lost
**Cause**: Models not constructed with proper args

**Solution**:
```go
// ✅ CORRECT: Args-based construction
func NewModel(controller *StateController, styles *Styles,
              window *Window, args []string) *Model {
    selectedIndex := 0
    if len(args) > 0 && args[0] != "" {
        // Restore selection from navigation args
        for i, item := range items {
            if item.ID == args[0] {
                selectedIndex = i
                break
            }
        }
    }
    return &Model{selectedIndex: selectedIndex, args: args}
}
```

### **Form Issues**

#### **Form Field Width Problems**
**Symptoms**: Input fields too narrow/wide, don't adapt to terminal size
**Cause**: Fixed width assignments during field creation

**Debug**:
```go
func (m *FormModel) debugFormDimensions() {
    width, height := m.styles.GetSize()
    viewportWidth, viewportHeight := m.getViewportSize()
    inputWidth := m.getInputWidth()

    logger.LogWithData("Form Dimensions", map[string]interface{}{
        "terminal_size":  fmt.Sprintf("%dx%d", width, height),
        "viewport_size":  fmt.Sprintf("%dx%d", viewportWidth, viewportHeight),
        "input_width":    inputWidth,
        "is_vertical":    m.isVerticalLayout(),
        "field_count":    len(m.fields),
    })
}
```

**Solution**:
```go
// ✅ CORRECT: Dynamic width calculation
func (m *FormModel) updateFormContent() {
    inputWidth := m.getInputWidth()

    for i, field := range m.fields {
        // Apply width during rendering, not initialization
        field.Input.Width = inputWidth - 3
        field.Input.SetValue(field.Input.Value()) // Trigger width update
    }
}

// ❌ WRONG: Fixed width at creation
func (m *FormModel) addField() {
    input := textinput.New()
    input.Width = 50 // Breaks responsive design!
}
```

#### **Form Scrolling Issues**
**Symptoms**: Can't reach all fields, focused field goes off-screen
**Cause**: Missing auto-scroll implementation or incorrect field height calculation

**Debug**:
```go
func (m *FormModel) debugScrollState() {
    logger.LogWithData("Scroll State", map[string]interface{}{
        "focused_index":    m.focusedIndex,
        "viewport_offset":  m.viewport.YOffset,
        "viewport_height":  m.viewport.Height,
        "content_height":   lipgloss.Height(m.formContent),
        "field_heights":    m.fieldHeights,
        "total_fields":     len(m.fields),
    })
}
```

**Solution**:
```go
// ✅ CORRECT: Auto-scroll implementation
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() // Critical for auto-scroll
}
```

### **Environment Variable Issues**

#### **Configuration Not Persisting**
**Symptoms**: Settings lost between sessions, environment variables not saved
**Cause**: Not calling controller save methods or incorrect cleanup logic

**Debug**:
```go
func (m *FormModel) debugEnvVarState() {
    for _, field := range m.fields {
        envVar, _ := m.controller.GetVar(m.getEnvVarName(getEnvSuffixFromKey(field.Key)))

        logger.LogWithData("Field State", map[string]interface{}{
            "field_key":       field.Key,
            "input_value":     field.Input.Value(),
            "env_var_name":    m.getEnvVarName(getEnvSuffixFromKey(field.Key)),
            "env_var_value":   envVar.Value,
            "env_var_default": envVar.Default,
            "is_present":      envVar.IsPresent(),
            "initially_set":   m.initiallySetFields[field.Key],
        })
    }
}
```

**Solution**:
```go
// ✅ CORRECT: Proper save implementation
func (m *FormModel) saveConfiguration() error {
    // First pass: Remove cleared fields
    for _, field := range m.fields {
        value := strings.TrimSpace(field.Input.Value())

        if value == "" && m.initiallySetFields[field.Key] {
            // Field was set but now empty - remove from environment
            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 non-empty values
    for _, field := range m.fields {
        value := strings.TrimSpace(field.Input.Value())
        if 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
}
```

### **Layout Issues**

#### **Content Not Adapting to Terminal Size**
**Symptoms**: Content cut off, panels don't resize, horizontal scrolling
**Cause**: Missing responsive layout logic or incorrect dimension handling

**Debug**:
```go
func (m *Model) debugLayoutState() {
    width, height := m.styles.GetSize()
    contentWidth, contentHeight := m.window.GetContentSize()

    logger.LogWithData("Layout State", map[string]interface{}{
        "terminal_size":    fmt.Sprintf("%dx%d", width, height),
        "content_size":     fmt.Sprintf("%dx%d", contentWidth, contentHeight),
        "is_vertical":      m.isVerticalLayout(),
        "min_terminal":     MinTerminalWidth,
        "min_menu_width":   MinMenuWidth,
        "min_info_width":   MinInfoWidth,
    })
}
```

**Solution**:
```go
// ✅ CORRECT: Responsive layout implementation
func (m *Model) View() string {
    width, height := m.styles.GetSize()

    leftPanel := m.renderContent()
    rightPanel := m.renderInfo()

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

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

#### **Footer Height Inconsistency**
**Symptoms**: Footer takes more/less space than expected, layout calculations wrong
**Cause**: Using border-based footer approach instead of background approach

**Solution**:
```go
// ✅ CORRECT: Background approach (always 1 line)
func (a *App) renderFooter() string {
    actions := a.buildFooterActions()
    footerText := strings.Join(actions, " • ")

    return a.styles.Footer.Render(footerText)
}

// In styles.go
func (s *Styles) updateStyles() {
    s.Footer = lipgloss.NewStyle().
        Width(s.width).
        Background(lipgloss.Color("240")).
        Foreground(lipgloss.Color("255")).
        Padding(0, 1, 0, 1)
}

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

## 🔧 **Performance Issues**

### **Slow Rendering**
**Symptoms**: Laggy UI, delayed responses to keystrokes
**Cause**: Multiple glamour renderers, excessive content updates

**Debug**:
```go
func (m *Model) debugRenderPerformance() {
    start := time.Now()
    content := m.buildContent()
    buildDuration := time.Since(start)

    start = time.Now()
    m.viewport.SetContent(content)
    setContentDuration := time.Since(start)

    start = time.Now()
    view := m.viewport.View()
    viewDuration := time.Since(start)

    logger.LogWithData("Render Performance", map[string]interface{}{
        "content_size":       len(content),
        "rendered_size":      len(view),
        "build_ms":           buildDuration.Milliseconds(),
        "set_content_ms":     setContentDuration.Milliseconds(),
        "view_render_ms":     viewDuration.Milliseconds(),
    })
}
```

**Solution**:
```go
// ✅ CORRECT: Single shared renderer
// In styles.go
func New() *Styles {
    renderer, _ := glamour.NewTermRenderer(
        glamour.WithAutoStyle(),
        glamour.WithWordWrap(80),
    )
    return &Styles{renderer: renderer}
}

// Usage
rendered, err := m.styles.GetRenderer().Render(content)

// ❌ WRONG: Multiple renderers
func (m *Model) renderMarkdown(content string) string {
    renderer, _ := glamour.NewTermRenderer(...) // Performance killer!
    return renderer.Render(content)
}
```

### **Memory Leaks**
**Symptoms**: Increasing memory usage, application becomes sluggish over time
**Cause**: Not properly cleaning up resources, creating multiple renderer instances

**Solution**:
```go
// ✅ CORRECT: Complete state reset
func (m *Model) Init() tea.Cmd {
    // Reset ALL state completely
    m.content = ""
    m.ready = false
    m.error = nil
    m.initialized = false

    // Reset component state
    m.viewport.GotoTop()
    m.viewport.SetContent("")

    // Reset form state
    m.focusedIndex = 0
    m.hasChanges = false
    for i := range m.fields {
        m.fields[i].Input.Blur()
    }

    return m.loadContent
}
```

## 🔧 **Error Recovery Patterns**

### **Graceful State Recovery**
```go
func (m *Model) recoverFromError(err error) tea.Cmd {
    logger.Errorf("[%s] ERROR: %v", m.componentName, err)

    // Try to recover state
    m.error = err
    m.ready = true

    // Attempt graceful recovery
    return func() tea.Msg {
        logger.Log("[%s] RECOVERY: attempting state recovery", m.componentName)

        // Try to reload content
        if content, loadErr := m.loadFallbackContent(); loadErr == nil {
            logger.Log("[%s] RECOVERY: fallback content loaded", m.componentName)
            return ContentLoadedMsg{content}
        }

        logger.Log("[%s] RECOVERY: using minimal content", m.componentName)
        return ContentLoadedMsg{"# Error\n\nContent temporarily unavailable."}
    }
}
```

### **Safe Async Operations**
```go
func (m *Model) loadContent() tea.Cmd {
    return func() tea.Msg {
        defer func() {
            if r := recover(); r != nil {
                logger.Errorf("[%s] PANIC: recovered from panic: %v", m.componentName, r)
                return ErrorMsg{fmt.Errorf("panic in loadContent: %v", r)}
            }
        }()

        content, err := m.loadFromSource()
        if err != nil {
            return ErrorMsg{err}
        }

        return ContentLoadedMsg{content}
    }
}
```

## 🔧 **Testing Strategies**

### **Manual Testing Checklist**
```go
// Test dimensions
// 1. Resize terminal to various sizes
// 2. Test minimum dimensions (80x24)
// 3. Test very narrow terminals (< 80 cols)
// 4. Test very short terminals (< 24 rows)

func testDimensions() {
    testSizes := []struct{ width, height int }{
        {80, 24},   // Standard
        {40, 12},   // Small
        {120, 40},  // Large
        {20, 10},   // Tiny
    }

    for _, size := range testSizes {
        logger.LogWithData("Dimension Test", map[string]interface{}{
            "test_size":   fmt.Sprintf("%dx%d", size.width, size.height),
            "layout_mode": getLayoutMode(size.width, size.height),
        })
    }
}
```

### **Navigation Flow Testing**
```go
func testNavigationFlow() {
    testSteps := []struct {
        action   string
        expected string
    }{
        {"start", "welcome"},
        {"continue", "main_menu"},
        {"select_providers", "llm_providers"},
        {"select_openai", "llm_provider_form§openai"},
        {"go_back", "llm_providers§openai"},
        {"esc", "welcome"},
    }

    for _, step := range testSteps {
        logger.LogWithData("Navigation Test", map[string]interface{}{
            "action":   step.action,
            "expected": step.expected,
            "actual":   string(navigator.Current()),
        })
    }
}
```

This troubleshooting guide provides comprehensive solutions for:
- **Development Workflow**: TUI-safe development patterns
- **Navigation Issues**: Stack management and state preservation
- **Form Problems**: Responsive design and scrolling
- **Configuration**: Environment variable management
- **Performance**: Optimization and resource management
- **Recovery**: Graceful error handling and state restoration
