# Modal System Guide

Comprehensive guide for creating modal UIs in Kollabor CLI. The modal system provides interactive overlays for configuration, form input, command selection, and status display.

## Table of Contents

- [Modal Architecture Overview](#modal-architecture-overview)
- [Creating Modal Definitions with UIConfig](#creating-modal-definitions-with-uiconfig)
- [Available Widgets](#available-widgets)
- [Keyboard Navigation Patterns](#keyboard-navigation-patterns)
- [Form Handling and Submission](#form-handling-and-submission)
- [Modal Lifecycle Events](#modal-lifecycle-events)
- [Example: Creating a Settings Modal](#example-creating-a-settings-modal)
- [Example: Creating a Form Modal](#example-creating-a-form-modal)

---

## Modal Architecture Overview

The modal system is built around several core components:

### Core Components

```
kollabor/ui/
├── modal_renderer.py          # Main modal rendering and interaction
├── modal_state_manager.py     # Terminal state isolation and display modes
├── modal_actions.py           # Save/cancel action handling
├── modal_overlay_renderer.py  # Overlay rendering system
├── config_merger.py           # Configuration change merging
└── widgets/
    ├── base_widget.py         # Abstract base class for all widgets
    ├── checkbox.py            # Boolean toggle widget
    ├── dropdown.py            # Option selection widget
    ├── text_input.py          # Text entry widget
    ├── slider.py              # Numeric slider widget
    └── label.py               # Read-only display widget
```

### Modal Display Modes

The `ModalDisplayMode` enum in `modal_state_manager.py` defines display options:

| Mode | Description |
|------|-------------|
| `OVERLAY` | Modal overlays existing content (default) |
| `FULLSCREEN` | Modal takes entire screen |
| `INLINE` | Modal appears inline (deprecated) |

### Terminal State Management

Modals use `ModalStateManager` to properly isolate terminal state:

- Creates `TerminalSnapshot` before displaying modal
- Complete isolation from conversation system
- Proper restoration on close
- Prevents state corruption between modals and chat

---

## Creating Modal Definitions with UIConfig

Modals are defined using the `UIConfig` dataclass from `kollabor.events.models`:

```python
@dataclass
class UIConfig:
    """Configuration for command UI interfaces."""
    type: str                              # "list", "tree", "form", "table", "menu", "modal"
    navigation: List[str] = field(default_factory=lambda: ["↑↓", "Enter", "Esc"])
    height: int = 10                       # Modal height
    width: Optional[int] = None            # Modal width (None = auto)
    scrollable: bool = True                # Enable scrolling
    title: str = ""                        # Modal title
    footer: str = ""                       # Footer instructions
    modal_config: Optional[Dict[str, Any]] = None  # Modal-specific config
```

### Modal Config Structure

The `modal_config` dictionary defines the modal content:

```python
modal_config = {
    "title": "Modal Title",
    "footer": "Navigation instructions",
    "width": 80,              # Optional: max 80 columns
    "sections": [
        {
            "title": "Section Title",
            "widgets": [
                # Widget definitions
            ]
        }
    ]
}
```

### Returning UIConfig from Commands

Commands trigger modals by returning a `CommandResult` with `ui_config`:

```python
from kollabor.events.models import CommandResult, UIConfig

async def my_command_handler(self, args: List[str], context: Dict) -> CommandResult:
    ui_config = UIConfig(
        type="modal",
        title="My Modal",
        footer="Enter to select • Esc to close",
        modal_config={
            "title": "My Modal",
            "footer": "↑↓ navigate • Enter select • Esc close",
            "width": 80,
            "sections": [...]
        }
    )
    return CommandResult(
        success=True,
        ui_config=ui_config,
        display_type="modal"
    )
```

---

## Available Widgets

All widgets extend `BaseWidget` from `kollabor/ui/widgets/base_widget.py`:

### Widget Base Interface

```python
class BaseWidget(ABC):
    def __init__(self, config: dict, config_path: str, config_service=None): ...

    @abstractmethod
    def render(self) -> List[str]: ...

    @abstractmethod
    def handle_input(self, key_press: KeyPress) -> bool: ...

    def get_value(self) -> Any: ...

    def set_value(self, value: Any): ...

    def get_pending_value(self) -> Any: ...

    def has_pending_changes(self) -> bool: ...

    def set_focus(self, focused: bool): ...

    def get_label(self) -> str: ...

    def is_valid_value(self, value: Any) -> bool: ...
```

### 1. CheckboxWidget

Boolean toggle with checkmark symbol.

**Display:** `  [✓] Label` or `  [ ] Label` (with 2-space indent)

**Config options:**

```python
{
    "type": "checkbox",
    "label": "Enable Feature",
    "config_path": "kollabor.feature.enabled",
    "help": "Optional help text"
}
```

**Key bindings:**
- `Enter` / `Space` - Toggle state
- Tab/Arrows - Navigate to next widget

**Example:**
```python
{
    "type": "checkbox",
    "label": "Enable Logging",
    "config_path": "kollabor.logging.enabled"
}
```

---

### 2. DropdownWidget

Option selection with expandable list.

**Display:** `  label: [value ▼]` (with 2-space indent, shows label and current selection)

**Config options:**

```python
{
    "type": "dropdown",
    "label": "Theme",
    "config_path": "terminal.theme",
    "options": ["dark", "light", "auto"],
    "help": "Color theme selection"
}
```

**Key bindings:**
- `Enter` - Expand/collapse dropdown
- `ArrowUp`/`ArrowDown` - Navigate options (when expanded)
- `ArrowLeft`/`ArrowRight` - Quick cycle without expansion
- `Escape` - Collapse dropdown

**Example:**
```python
{
    "type": "dropdown",
    "label": "Thinking Effect",
    "config_path": "terminal.thinking_effect",
    "options": ["shimmer", "pulse", "wave", "none"]
}
```

---

### 3. TextInputWidget

Text entry with cursor indicator.

**Display:** `  label: [text▌]` (with 2-space indent, shows label and cursor position)

**Config options:**

```python
{
    "type": "text_input",
    "label": "API Key",
    "config_path": "kollabor.api.key",
    "placeholder": "Enter your API key...",
    "validation": "text",  # "text", "integer", "number"
    "help": "Your API key for authentication"
}
```

Note: For password fields, add `"password": True` to the config (masks input in UI).

**Key bindings:**
- `ArrowLeft`/`ArrowRight` - Move cursor
- `Home` / `Ctrl+A` - Move to beginning
- `End` / `Ctrl+E` - Move to end
- `Backspace` - Delete character before cursor
- `Delete` - Delete character at cursor
- `Ctrl+U` - Clear entire line
- `Ctrl+K` - Clear from cursor to end

**Validation types:**
- `"text"` - Accept any printable characters
- `"integer"` - Only accept digits (0-9)
- `"number"` - Accept digits and decimal point

**Example:**
```python
{
    "type": "text_input",
    "label": "Max Tokens",
    "config_path": "kollabor.llm.max_tokens",
    "placeholder": "4096",
    "validation": "integer"
}
```

---

### 4. SliderWidget

Numeric value with visual progress bar.

**Display:** `  label: value [██░░░░] (min-max)` (when focused, shows range; unfocused shows just value and bar)

**Config options:**

```python
{
    "type": "slider",
    "label": "Temperature",
    "config_path": "kollabor.llm.temperature",
    "min_value": 0.0,        # or "min" for shorthand
    "max_value": 2.0,        # or "max" for shorthand
    "step": 0.1,
    "bar_width": 20,
    "decimal_places": 1,
    "help": "Controls response randomness"
}
```

Note: The SliderWidget accepts both `min_value`/`max_value` and `min`/`max` as parameter names.

**Key bindings:**
- `ArrowRight` / `ArrowUp` - Increase value
- `ArrowLeft` / `ArrowDown` - Decrease value
- `Home` - Set to minimum value
- `End` - Set to maximum value
- `Ctrl+ArrowRight` - Large step increase (10x)
- `Ctrl+ArrowLeft` - Large step decrease (10x)
- `0-9` - Jump to percentage (0=0%, 9=90%)

**Example:**
```python
{
    "type": "slider",
    "label": "Render FPS",
    "config_path": "terminal.render_fps",
    "min_value": 1,
    "max_value": 60,
    "step": 1,
    "decimal_places": 0
}
```

---

### 5. LabelWidget

Read-only display for status information.

**Display:** `  label: value` (with 2-space indent, matching other widgets)

**Constructor:** LabelWidget has a unique constructor signature:
```python
LabelWidget(label, value="", help_text="", config_path="", current_value=None, **kwargs)
```

**Config options:**

```python
{
    "type": "label",
    "label": "Status",
    "value": "Connected",
    "help": "Current connection status"
}
```

**Key bindings:** None (read-only)

**Example:**
```python
{
    "type": "label",
    "label": "Version",
    "value": "1.0.0"
}
```

---

### Widget Configuration Summary

| Widget | Required Fields | Optional Fields |
|--------|----------------|-----------------|
| checkbox | type, label, config_path | help |
| dropdown | type, label, config_path, options | help |
| text_input | type, label, config_path | placeholder, validation, password, help |
| slider | type, label, config_path, min_value/max (or min_value), max_value/max (or max) | step, bar_width, decimal_places, help |
| label | type, label, value (or current_value) | help, config_path |

Note: For form modals (non-config), use `"field"` instead of `"config_path"` to store values in form_data instead of config.

---

## Keyboard Navigation Patterns

### Global Navigation

| Key | Action |
|-----|--------|
| `Tab` | Next widget |
| `Shift+Tab` | Previous widget |
| `ArrowDown` | Next widget (or within expanded dropdown) |
| `ArrowUp` | Previous widget (or within expanded dropdown) |
| `PageDown` | Jump forward by visible height |
| `PageUp` | Jump backward by visible height |
| `Enter` | Activate widget or submit |
| `Escape` | Cancel/close modal |
| `Ctrl+S` | Save changes |

### Widget-Specific Navigation

**Checkbox:**
- `Enter` / `Space` - Toggle

**Dropdown:**
- `Enter` - Expand/collapse
- `ArrowLeft`/`ArrowRight` - Quick cycle
- `ArrowUp`/`ArrowDown` - Navigate when expanded

**TextInput:**
- All arrow keys for cursor movement
- Editing keys (Backspace, Delete, Ctrl+U, Ctrl+K)

**Slider:**
- `ArrowLeft`/`ArrowRight` - Adjust value
- `Home`/`End` - Min/max values

**Label:**
- No input handling

### Navigation Edge Cases

- **Expanded dropdowns**: Arrow keys navigate options, not widgets
- **Text input cursor**: Arrow keys move cursor, not focus
- **Wrap-around**: Last widget -> First widget and vice versa

---

## Form Handling and Submission

### Pending Value System

Widgets use a pending value system to track unsaved changes:

```python
# In BaseWidget
_pending_value = None  # Stores unsaved changes

def get_pending_value(self) -> Any:
    """Get pending value if set, otherwise current value."""
    return self._pending_value if self._pending_value is not None else self.get_value()

def has_pending_changes(self) -> bool:
    """Check if widget has unsaved changes."""
    return self._pending_value is not None
```

### Save Process

The `ModalActionHandler` class handles save operations:

```python
# Flow triggered by Ctrl+S or Enter
1. Collect changes from widgets with pending values
2. Validate changes via ConfigMerger.validate_config_changes()
3. Apply changes via ConfigMerger.apply_widget_changes()
4. Persist to config via ConfigService
5. Display success message
6. Close modal
```

**Save action result:**
```python
{
    "success": True,
    "message": "Saved 3 configuration changes",
    "action": "save",
    "changes_count": 3,
    "changes": {
        "kollabor.llm.temperature": 0.7,
        "terminal.render_fps": 30,
        "terminal.thinking_effect": "shimmer"
    }
}
```

### Cancel Process

```python
# Flow triggered by Escape
1. Check for pending changes
2. If changes exist, show confirmation prompt
3. On confirmation, discard changes
4. Close modal
```

**Cancel action result:**
```python
{
    "success": True,
    "message": "Configuration changes cancelled",
    "action": "cancel"
}
```

### ConfigMerger

The `ConfigMerger` class manages configuration changes:

```python
class ConfigMerger:
    @staticmethod
    def collect_widget_changes(widgets: List[BaseWidget]) -> Dict[str, Any]:
        """Collect all pending changes from widgets."""
        changes = {}
        for widget in widgets:
            if widget.has_pending_changes():
                changes[widget.config_path] = widget.get_pending_value()
        return changes

    @staticmethod
    def validate_config_changes(config_service, changes: Dict) -> Dict:
        """Validate configuration changes before applying."""
        # Returns {"valid": bool, "errors": List[str]}

    @staticmethod
    def apply_widget_changes(config_service, changes: Dict) -> bool:
        """Apply validated changes to configuration."""
```

---

## Modal Lifecycle Events

### Event Flow

```
1. MODAL_TRIGGER
   ├─ Command returns CommandResult with ui_config set
   └─ ModalRenderer.show_modal(ui_config) called

2. PAUSE_RENDERING
   ├─ Render loop paused during modal
   └─ Prevents chat render interference

3. MODAL_SHOW
   ├─ ModalStateManager.prepare_modal_display() called
   ├─ TerminalSnapshot created (captures cursor, screen buffer, raw mode)
   ├─ Alternate screen buffer enabled (\033[?1049h)
   └─ Modal content rendered via overlay system

4. User Interaction Loop
   ├─ InputHandler routes keys to ModalRenderer._handle_modal_input()
   ├─ Widgets handle input via handle_input() and update state
   ├─ Modal refreshed via ModalStateManager.render_modal_content()
   └─ _render_modal_box() called with preserve_widgets=True for updates

5. User Exits (Escape or Ctrl+S)
   ├─ MODAL_HIDE or MODAL_SAVE event triggered
   ├─ ModalStateManager.restore_terminal_state() called
   ├─ Alternate screen buffer disabled (\033[?1049l)
   ├─ TerminalSnapshot restored (cursor position, visibility)
   └─ Modal state reset (modal_active=False, cached content cleared)

6. RESUME_RENDERING
   ├─ Normal chat rendering resumes
   └─ MessageDisplayCoordinator displays queued messages if any
```

### Modal Display Modes

From `ModalDisplayMode` enum in `modal_state_manager.py`:

**Overlay Mode** (default):
- Preserves underlying content in alternate screen buffer
- Renders modal on top using direct terminal output
- Original screen restored on close

**Fullscreen Mode**:
- Takes entire terminal
- Used for immersive interfaces

**Inline Mode** (deprecated):
- Modal appears inline with content
- Not recommended for use

### Modal Close Patterns

The actual close methods are in `input_handler.py`, not `modal_renderer.py`:

**Normal exit** (`_exit_modal_mode`):
- Calls `modal_renderer.close_modal()`
- Restores terminal state via `ModalStateManager.restore_terminal_state()`
- Renders input prompt after close

**Minimal exit** (`_exit_modal_mode_minimal`):
- For commands that display their own content (e.g., `/branch`)
- Restores terminal state without rendering input prompt
- Prevents duplicate input boxes

### State Manager Methods

Key `ModalStateManager` methods:

- `prepare_modal_display(layout, display_mode)` - Prepare terminal for modal
- `render_modal_content(content_lines)` - Render modal content
- `refresh_modal_display()` - Re-render cached content
- `restore_terminal_state()` - Close modal and restore state
- `update_modal_layout(new_layout)` - Change modal layout dynamically

---

## Example: Creating a Settings Modal

This example shows how to create a settings modal similar to the `/config` command.

### Command Handler

```python
# In your plugin's command handler

from kollabor.events.models import CommandResult, UIConfig, CommandDefinition

async def settings_command(self, args: List[str], context: Dict) -> CommandResult:
    """Display the settings modal."""

    ui_config = UIConfig(
        type="modal",
        title="System Settings",
        navigation=["↑↓", "navigate", "Enter", "toggle", "Ctrl+S", "save", "Esc", "cancel"],
        height=30,
        width=80,
        scrollable=True,
        modal_config={
            "title": "System Settings",
            "footer": "↑↓/PgUp/PgDn navigate • Enter toggle • Ctrl+S save • Esc cancel",
            "width": 80,
            "sections": [
                {
                    "title": "Terminal Settings",
                    "widgets": [
                        {
                            "type": "slider",
                            "label": "Render FPS",
                            "config_path": "terminal.render_fps",
                            "min_value": 1,
                            "max_value": 60,
                            "step": 1,
                            "help": "Terminal refresh rate"
                        },
                        {
                            "type": "dropdown",
                            "label": "Thinking Effect",
                            "config_path": "terminal.thinking_effect",
                            "options": ["shimmer", "pulse", "wave", "none"],
                            "help": "Animation for thinking state"
                        },
                        {
                            "type": "checkbox",
                            "label": "Enable Cache",
                            "config_path": "terminal.render_cache_enabled",
                            "help": "Cache renders to reduce I/O"
                        }
                    ]
                },
                {
                    "title": "LLM Settings",
                    "widgets": [
                        {
                            "type": "slider",
                            "label": "Temperature",
                            "config_path": "kollabor.llm.temperature",
                            "min_value": 0.0,
                            "max_value": 2.0,
                            "step": 0.1,
                            "decimal_places": 1,
                            "help": "Response randomness"
                        },
                        {
                            "type": "slider",
                            "label": "Max Tokens",
                            "config_path": "kollabor.llm.max_tokens",
                            "min_value": 256,
                            "max_value": 8192,
                            "step": 256,
                            "decimal_places": 0,
                            "help": "Maximum response length"
                        }
                    ]
                }
            ]
        }
    )

    return CommandResult(
        success=True,
        ui_config=ui_config,
        display_type="modal"
    )
```

### Command Registration

```python
# In your plugin's register_commands() method

settings_command = CommandDefinition(
    name="settings",
    description="Open system settings modal",
    handler=self.settings_command,
    plugin_name=self.name,
    mode=CommandMode.MODAL,
    category=CommandCategory.SYSTEM,
    aliases=["cfg", "config"]
)

self.command_registry.register_command(settings_command)
```

---

## Example: Creating a Form Modal

This example shows how to create a form modal for data input (not bound to config).

### Command Handler

```python
from kollabor.events.models import CommandResult, UIConfig

async def create_agent_command(self, args: List[str], context: Dict) -> CommandResult:
    """Display form for creating a new agent."""

    ui_config = UIConfig(
        type="modal",
        title="Create New Agent",
        navigation=["Enter", "next field", "Esc", "cancel"],
        height=20,
        width=70,
        modal_config={
            "title": "Create New Agent",
            "footer": "Tab next field • Enter save • Esc cancel",
            "width": 70,
            "sections": [
                {
                    "title": "Agent Information",
                    "widgets": [
                        {
                            "type": "text_input",
                            "label": "Agent Name",
                            "field": "agent.name",  # Use "field" instead of "config_path"
                            "placeholder": "my-agent",
                            "help": "Unique identifier for the agent"
                        },
                        {
                            "type": "text_input",
                            "label": "Display Name",
                            "field": "agent.display_name",
                            "placeholder": "My Agent",
                            "help": "Human-readable name"
                        },
                        {
                            "type": "text_input",
                            "label": "Description",
                            "field": "agent.description",
                            "placeholder": "What this agent does...",
                            "help": "Brief description of agent purpose"
                        },
                        {
                            "type": "dropdown",
                            "label": "Base Model",
                            "field": "agent.model",
                            "options": ["gpt-4", "gpt-3.5-turbo", "claude-3-opus"],
                            "help": "Default LLM for this agent"
                        },
                        {
                            "type": "slider",
                            "label": "Temperature",
                            "field": "agent.temperature",
                            "min_value": 0.0,
                            "max_value": 1.0,
                            "step": 0.1,
                            "decimal_places": 1,
                            "help": "Default response randomness"
                        }
                    ]
                }
            ]
        }
    )

    return CommandResult(
        success=True,
        ui_config=ui_config,
        display_type="modal",
        data={"form_type": "create_agent"}  # For handling form submission
    )
```

### Command Selection Modal (No Widgets)

For command selection modals, use the "commands" format instead of widgets:

```python
async def agents_command(self, args: List[str], context: Dict) -> CommandResult:
    """Display agent selection modal."""

    # Get available agents from agent_manager
    agents = self.agent_manager.list_agents()

    agent_list = [
        {
            "name": agent["id"],
            "description": agent.get("description", ""),
            "selectable": True,
            "action": "select_agent",
            "metadata": {"agent_id": agent["id"]}
        }
        for agent in agents
    ]

    ui_config = UIConfig(
        type="modal",
        title="Select Agent",
        navigation=["↑↓", "navigate", "Enter", "select", "Esc", "cancel"],
        height=20,
        modal_config={
            "title": "Available Agents",
            "footer": "↑↓ navigate • Enter select • Esc cancel",
            "sections": [
                {
                    "title": "Agents",
                    "commands": agent_list
                }
            ]
        }
    )

    return CommandResult(
        success=True,
        ui_config=ui_config,
        display_type="modal"
    )
```

---

## Best Practices

1. **Config-bound modals**: Use `config_path` for settings that persist
2. **Form modals**: Use `field` instead of `config_path` for temporary data
3. **Widget width**: Keep labels under 26 characters for proper alignment
4. **Modal width**: Use 70-80 columns for optimal readability
5. **Validation**: Always validate user input before saving
6. **Help text**: Include helpful descriptions for complex settings
7. **Footer instructions**: Always show navigation hints in the footer
8. **Section grouping**: Group related settings into logical sections

---

## Related Files

- `kollabor/ui/modal_renderer.py` - Main modal implementation (ModalRenderer class)
- `kollabor/ui/modal_state_manager.py` - Terminal state management (ModalStateManager, ModalDisplayMode, TerminalSnapshot, ModalLayout)
- `kollabor/ui/modal_actions.py` - Save/cancel handling (ModalActionHandler)
- `kollabor/ui/modal_overlay_renderer.py` - Overlay rendering system
- `kollabor/ui/config_merger.py` - Configuration change persistence (ConfigMerger)
- `kollabor/ui/config_widgets.py` - Config widget definitions and factory (ConfigWidgetDefinitions)
- `kollabor/ui/widgets/` - Widget implementations (BaseWidget, CheckboxWidget, DropdownWidget, TextInputWidget, SliderWidget, LabelWidget)
- `kollabor/io/key_parser.py` - KeyPress dataclass for input handling
- `kollabor/commands/system_commands.py` - Example modal commands (/profile, /agent, /skill)
- `kollabor/events/models.py` - UIConfig, CommandResult, CommandDefinition, SubcommandInfo
