# Widget Development Guide

**Version:** 1.0
**Status:** Active
**Last Updated:** 2026-01-19

## Overview

This guide provides comprehensive information for developers creating custom status widgets for Kollabor CLI. It covers advanced topics, design system integration, and complete examples for all widget types.

**Related Documentation:**
- [Widget Status System (Features)](../features/widget-status-system.md) - User-facing guide
- [Widget Status System (Reference)](../reference/widget-status-system.md) - Technical reference
- [Design System](../reference/design-system.md) - UI components reference

## Table of Contents

1. [Widget Architecture](#widget-architecture)
2. [Widget Context API](#widget-context-api)
3. [Design System Integration](#design-system-integration)
4. [Widget Type Examples](#widget-type-examples)
5. [Inline Editor Service](#inline-editor-service)
6. [Advanced Patterns](#advanced-patterns)
7. [Testing Widgets](#testing-widgets)
8. [Performance Considerations](#performance-considerations)

---

## Widget Architecture

### Component Hierarchy

```
StatusWidgetRegistry
    ├── StatusWidget (definition)
    │   ├── render_fn: (width, context) -> str
    │   ├── on_activate: (widget_id, context) -> result
    │   └── metadata (category, width, interactions)
    │
    └── WidgetContext (runtime services)
        ├── llm_service: LLMService
        ├── profile_manager: ProfileManager
        ├── config: ConfigManager
        ├── layout_manager: StatusLayoutManager
        └── navigation_state: StatusNavigationState
```

### Widget Lifecycle

```python
# 1. Registration (startup)
registry.register(id="my-widget", ...)

# 2. Rendering (every frame, ~60fps)
content = widget.render(width, context)
# -> "My Content" with ANSI colors

# 3. Activation (user presses Enter)
result = await widget.on_activate("my-widget", context)
# -> {"type": "slider", "current": 0.7, ...}

# 4. Cleanup (optional)
registry.unregister("my-widget")
```

### File Structure

```
core/io/status/
├── widget_registry.py          # Registry and StatusWidget dataclass
├── layout_manager.py           # Layout persistence
├── layout_renderer.py          # Status area rendering
├── navigation_manager.py       # Keyboard navigation
├── navigation_state.py         # Navigation state tracking
├── core_widgets.py             # Built-in widgets
├── interactive_widgets.py      # Interactive widget classes
├── inline_editors.py           # Slider, text, dropdown editors
├── inline_editor_service.py    # Editor orchestration
├── widget_picker.py            # Widget picker modal
├── interaction_handler.py      # Widget activation handling
├── script_widgets.py           # Script-based widget system
└── plugin_api.py               # Plugin registration API
```

---

## Widget Context API

The `WidgetContext` provides access to application services during widget rendering and activation.

### Available Services

```python
@dataclass
class WidgetContext:
    """Context object passed to widget render and activation functions.

    Attributes:
        llm_service: LLM communication service
        profile_manager: Profile configuration manager
        config: Global configuration manager
        layout_manager: Status layout manager
        navigation_state: Current navigation state
        renderer: Terminal renderer (use sparingly)
    """
    llm_service: Optional[LLMService] = None
    profile_manager: Optional[ProfileManager] = None
    config: Optional[ConfigManager] = None
    layout_manager: Optional[StatusLayoutManager] = None
    navigation_state: Optional[StatusNavigationState] = None
    renderer: Optional[Any] = None  # TerminalRenderer
```

### Accessing Services in Render Functions

```python
def render_my_widget(width: int, ctx: WidgetContext) -> str:
    """Render widget with service access."""

    # Access profile manager
    if ctx and ctx.profile_manager:
        profile = ctx.profile_manager.get_active_profile()
        profile_name = profile.get_name() if profile else "none"
    else:
        profile_name = "unknown"

    # Access LLM service
    if ctx and ctx.llm_service:
        stats = ctx.llm_service.session_stats
        msg_count = stats.get('messages', 0)
    else:
        msg_count = 0

    # Access config
    if ctx and ctx.config:
        timeout = ctx.config.get("core.llm.timeout", 30)
    else:
        timeout = 30

    return f"Profile: {profile_name} | Messages: {msg_count}"
```

### Accessing Services in Activation Handlers

```python
async def activate_my_widget(widget_id: str, ctx: WidgetContext) -> Dict[str, Any]:
    """Handle widget activation with service access."""

    # Get current temperature from profile
    if ctx and ctx.profile_manager:
        profile = ctx.profile_manager.get_active_profile()
        current_temp = profile.get_temperature() if profile else 0.7
    else:
        current_temp = 0.7

    # Return inline editor configuration
    return {
        "type": "slider",
        "title": "Temperature",
        "current": current_temp,
        "min": 0.0,
        "max": 2.0,
        "step": 0.1,
        "presets": [0.1, 0.5, 0.7, 1.0],
        "on_save": "_update_temperature",
        "config_key": "core.llm.temperature",
    }
```

---

## Design System Integration

Widgets should use the design system for consistent styling.

### Core Imports

```python
from core.ui.design_system import (
    T,           # Theme colors
    S,           # Style constants
    C,           # Color constants
    Box,         # Box drawing utilities
    TagBox,      # Tag + content rendering
    solid,       # Solid background rendering
    solid_fg,    # Solid foreground rendering
    gradient,    # Gradient rendering
    # Inline components
    inline_slider,    # Inline slider for values
    inline_dropdown,  # Inline dropdown for choices
    inline_text,      # Inline text input
)
```

### Theme Colors

```python
# Primary accent colors
T().primary       # [(r,g,b), ...] - Gradient list (usually 4-5 colors)
T().primary[0]    # (r,g,b) - First primary color
T().primary[1]    # (r,g,b) - Second primary color

# Background colors
T().dark          # [(r,g,b), ...] - Background gradient
T().dark[0]       # (r,g,b) - Darkest background
T().dark[1]       # (r,g,b) - Lighter background

# Text colors
T().text          # (r,g,b) - Main text color
T().text_dim      # (r,g,b) - Dimmed text color
T().text_dark     # (r,g,b) - Dark text for contrast

# Status colors
T().success       # [(r,g,b), ...] - Success state
T().error         # [(r,g,b), ...] - Error state
T().warning       # [(r,g,b), ...] - Warning state

# Secondary accent
T().secondary     # [(r,g,b), ...] - Secondary accent gradient
T().secondary[0]  # (r,g,b) - First secondary color

# AI tag color (for special indicators)
T().ai_tag        # (r,g,b) - AI indicator color
```

### Foreground Color Helper

```python
def _fg(text: str, color: tuple) -> str:
    """Apply foreground RGB color to text."""
    r, g, b = color
    return f"\033[38;2;{r};{g};{b}m{text}\033[39m"

# Usage
icon = _fg("cwd", T().ai_tag)
path = _fg("~/dev/project", T().text)
return f"{icon} {path}"
```

### Background Color Helper

```python
def _bg(text: str, bg: tuple) -> str:
    """Apply background RGB color to text."""
    r, g, b = bg
    return f"\033[48;2;{r};{g};{b}m{text}\033[49m"

# Usage
highlight = _bg(" Important ", T().primary[0])
return highlight
```

### Solid Block Rendering

```python
from core.ui.design_system import solid, solid_fg

# Solid background with text
content = solid("  My Content  ", T().dark[0], T().text, width=20)

# Solid foreground (for borders/edges)
top_edge = solid_fg("▄" * width, T().dark[0])
bottom_edge = solid_fg("▀" * width, T().dark[0])

# Full box pattern
def render_widget_box(width: int, content: str) -> str:
    """Render content in a solid block box."""
    content_width = width - 4  # Padding
    lines = [
        solid_fg("▄" * width, T().dark[0]),  # Top edge
        solid(f"  {content:<{content_width}}", T().dark[0], T().text, width),  # Content
        solid_fg("▀" * width, T().dark[0]),  # Bottom edge
    ]
    return "\n".join(lines)
```

### TagBox Pattern

```python
from core.ui.design_system import TagBox

# Tag + content layout
lines = ["Line 1", "Line 2", "Line 3"]
result = TagBox.render(
    lines=lines,
    tag_bg=T().primary[0],      # Tag background color
    tag_fg=T().text_dark,       # Tag text color
    tag_width=3,                # Tag character width
    content_colors=T().dark[0], # Content background
    content_fg=T().text,        # Content text color
    content_width=width - 7,    # Total width minus tag and padding
    tag_chars=[" > "],          # Tag characters to render
)

# Example output:
#   > Line 1
#   > Line 2
#   > Line 3
```

### Inline Components

```python
from core.ui.design_system import inline_slider, inline_dropdown, inline_text

# Inline slider (for numeric values)
slider = inline_slider(
    value=0.7,           # Current value
    min_val=0.0,         # Minimum
    max_val=2.0,         # Maximum
    width=width - 10,    # Available width
    show_value=True,     # Show numeric value
)
# Output: "[████████░░] 0.7"

# Inline dropdown (for selections)
dropdown = inline_dropdown(
    label="Profile:",
    options=["gpt-4", "gpt-3.5-turbo", "claude-3"],
    selected=0,
    width=width,
)
# Output: "Profile: [gpt-4]"

# Inline text (for text input)
text = inline_text(
    value="my text",
    placeholder="enter value",
    max_length=20,
    width=width,
)
# Output: "[my text           ]"
```

### Gradient Rendering

```python
from core.ui.design_system import gradient

# Apply gradient to text
text = gradient("My Text", T().primary, T().text)
# Text fades through primary gradient colors

# Preserve existing ANSI codes in text
colored_text = "\033[38;2;255;0;0mRed\033[39m Text"
result = gradient(colored_text, T().primary, T().text, preserve=True)
# "Red" stays red, rest gets gradient
```

### Style Constants

```python
from core.ui.design_system import S

# Bold text
text = f"{S.BOLD}Important{S.RESET_BOLD}"

# Dim text
text = f"{S.DIM}less important{S.RESET_DIM}"

# Underline text
text = f"{S.UNDERLINE}link{S.RESET_UNDERLINE}"

# Combine styles
text = f"{S.BOLD}{S.DIM}Subtle emphasis{S.RESET_DIM}{S.RESET_BOLD}"
```

---

## Widget Type Examples

### 1. Information Widget (Non-Interactive)

Simple display widget that shows information.

```python
from core.io.status.widget_registry import StatusWidget, WidgetCategory
from core.io.status.core_widgets import WidgetContext

def render_uptime(width: int, ctx: WidgetContext) -> str:
    """Render system uptime widget."""
    import subprocess

    try:
        result = subprocess.run(
            ["uptime", "-p"],
            capture_output=True,
            text=True,
            timeout=1.0
        )
        if result.returncode == 0:
            uptime = result.stdout.strip().replace("up ", "")
            # Shorten for compact display
            if len(uptime) > width - 5:
                uptime = uptime[:width-8] + "..."
            icon = _fg("⏱", T().secondary[0])
            return f"{icon} {uptime}"
    except (subprocess.TimeoutExpired, FileNotFoundError):
        pass

    return _fg("⏱ ?", T().text_dim)

# Registration
registry.register(
    id="uptime",
    name="Uptime",
    description="System uptime",
    render_fn=render_uptime,
    category=WidgetCategory.CORE,
    default_width="auto",
    min_width=10,
)
```

### 2. Command Widget

Widget that executes a slash command when activated.

```python
def render_cwd(width: int, ctx: WidgetContext) -> str:
    """Render current working directory."""
    from pathlib import Path

    cwd = Path.cwd()
    home = Path.home()

    # Build display path
    if cwd == home:
        display = "~"
    elif cwd.is_relative_to(home):
        display = f"~/{cwd.relative_to(home)}"
    else:
        display = str(cwd)

    # Compress to fit width
    max_len = width - 4  # Reserve space for "cwd "
    if len(display) > max_len:
        # Smart path compression
        parts = display.split("/")
        if len(parts) > 2:
            display = f".../{parts[-1]}"

    icon = _fg("cwd", T().ai_tag)
    text = _fg(display, T().text)

    return f"{icon} {text}"

# Registration with command activation
registry.register(
    id="cwd",
    name="Directory",
    description="Current working directory",
    render_fn=render_cwd,
    category=WidgetCategory.CORE,
    default_width="auto",
    min_width=10,
    interactive=True,
    interaction_type="command",
    command="/cd",  # Executes /cd command on Enter
)
```

### 3. Toggle Widget

Widget that cycles through states when activated.

```python
class TestToggleWidget:
    """Toggle widget with state persistence."""

    _states = ["OFF", "ON", "AUTO"]
    _state_colors = {
        "OFF": T().text_dim,
        "ON": T().success[0],
        "AUTO": T().primary[2],
    }

    @classmethod
    def render(cls, width: int, ctx: WidgetContext) -> str:
        """Render toggle widget with current state."""
        # Get persisted state from widget config
        state = "OFF"
        if ctx and ctx.layout_manager:
            # Try to get state from layout
            nav_state = ctx.navigation_state
            if nav_state:
                row_id = getattr(nav_state, 'selected_row', None)
                widget_idx = getattr(nav_state, 'selected_widget_index', None)
                if row_id is not None and widget_idx is not None:
                    state = ctx.layout_manager.get_widget_config(
                        row_id + 1, widget_idx, "toggle_state", "OFF"
                    )

        # Validate state
        if state not in cls._states:
            state = "OFF"

        color = cls._state_colors[state]
        return _fg(f"TEST:{state}", color)

    @classmethod
    async def activate(cls, widget_id: str, ctx: WidgetContext) -> Dict[str, Any]:
        """Handle toggle activation - cycle to next state."""
        if not ctx or not ctx.layout_manager:
            return {"new_state": "OFF"}

        # Get current position
        nav_state = ctx.navigation_state
        if not nav_state:
            return {"new_state": "OFF"}

        row_id = getattr(nav_state, 'selected_row', None)
        widget_idx = getattr(nav_state, 'selected_widget_index', None)

        if row_id is None or widget_idx is None:
            return {"new_state": "OFF"}

        # Get current state
        current_state = ctx.layout_manager.get_widget_config(
            row_id + 1, widget_idx, "toggle_state", "OFF"
        )

        # Cycle to next state
        current_idx = cls._states.index(current_state)
        next_state = cls._states[(current_idx + 1) % len(cls._states)]

        # Persist new state
        ctx.layout_manager.update_widget_config(
            row_id + 1, widget_idx, "toggle_state", next_state
        )
        ctx.layout_manager.save()

        return {"new_state": next_state}

# Registration
registry.register(
    id="test-toggle",
    name="Test Toggle",
    description="Toggle test mode (OFF/ON/AUTO)",
    render_fn=TestToggleWidget.render,
    category=WidgetCategory.CORE,
    default_width="auto",
    min_width=12,
    interactive=True,
    interaction_type="toggle",
    states=["OFF", "ON", "AUTO"],
    on_activate=TestToggleWidget.activate,
)
```

### 4. Inline Edit Widget (Slider)

Widget that opens an inline slider editor.

```python
def render_temperature(width: int, ctx: WidgetContext) -> str:
    """Render temperature widget with current value."""
    current_temp = 0.7

    # Get current temperature from profile
    if ctx and ctx.profile_manager:
        profile = ctx.profile_manager.get_active_profile()
        if profile:
            current_temp = profile.get_temperature()

    # Render inline slider
    slider_width = width - 8  # Reserve space for "T " prefix and value
    slider = inline_slider(
        value=current_temp,
        min_val=0.0,
        max_val=2.0,
        width=slider_width,
        show_value=True,
    )

    icon = _fg("T", T().secondary[0])
    return f"{icon} {slider}"

async def activate_temperature(widget_id: str, ctx: WidgetContext) -> Dict[str, Any]:
    """Return inline editor configuration."""
    current_temp = 0.7

    if ctx and ctx.profile_manager:
        profile = ctx.profile_manager.get_active_profile()
        if profile:
            current_temp = profile.get_temperature()

    return {
        "type": "slider",
        "title": "Temperature",
        "current": current_temp,
        "min": 0.0,
        "max": 2.0,
        "step": 0.1,
        "presets": [0.1, 0.5, 0.7, 1.0, 1.5],
        "on_save": "_update_temperature",
    }

# Registration
registry.register(
    id="temperature",
    name="Temperature",
    description="Model temperature (0.0-2.0)",
    render_fn=render_temperature,
    category=WidgetCategory.INTERACTIVE,
    default_width="auto",
    min_width=15,
    interactive=True,
    interaction_type="inline_edit",
    on_activate=activate_temperature,
)
```

### 5. Action Widget (Modal with Options)

Widget that shows a modal with multiple action options.

```python
def render_session(width: int, ctx: WidgetContext) -> str:
    """Render session widget."""
    import os

    # Get project name from current directory
    project_name = os.path.basename(os.getcwd())

    # Truncate if too long
    max_len = width - 9  # Reserve space for icons
    if len(project_name) > max_len:
        project_name = project_name[:max_len-3] + "..."

    icon = _fg("📁", T().primary[0])
    return f"{icon} {project_name}"

async def activate_session(widget_id: str, ctx: WidgetContext) -> Dict[str, Any]:
    """Return action menu configuration."""
    return {
        "type": "action",
        "title": "Session Actions",
        "options": [
            {
                "label": "View Logs",
                "description": "Open application logs directory",
                "action": "open_logs",
                "handler": "_handle_open_logs",
            },
            {
                "label": "View Conversations",
                "description": "Browse conversation history",
                "action": "open_conversations",
                "handler": "_handle_open_conversations",
            },
            {
                "label": "Export Session",
                "description": "Export current session data",
                "action": "export_session",
                "handler": "_handle_export_session",
                "confirm": True,  # Ask for confirmation
            },
        ],
    }

# Registration
registry.register(
    id="session",
    name="Session",
    description="Project session actions",
    render_fn=render_session,
    category=WidgetCategory.CORE,
    default_width="auto",
    min_width=12,
    interactive=True,
    interaction_type="action",
    on_activate=activate_session,
)
```

---

## Inline Editor Service

The `InlineEditorService` provides centralized inline editing for sliders, text inputs, and dropdowns.

### Service Architecture

```python
from core.io.status.inline_editor_service import InlineEditorService

# Service is initialized by the application
service = InlineEditorService(renderer, navigation_state)

# Show an inline editor
result = await service.show_editor({
    "type": "slider",
    "current": 0.7,
    "min": 0.0,
    "max": 2.0,
    "step": 0.1,
    "on_save": callback_function,
})

# Result is the edited value if confirmed, None if cancelled
if result is not None:
    print(f"New value: {result}")
```

### Editor Types

#### 1. Slider Editor

```python
config = {
    "type": "slider",
    "current": 0.7,          # Current value
    "min": 0.0,              # Minimum value
    "max": 2.0,              # Maximum value
    "step": 0.1,             # Adjustment step
    "presets": [0.1, 0.5, 0.7, 1.0],  # Quick presets
    "on_save": callback,     # Save callback
    "on_change": change_callback,  # Optional: called on each change
}
```

**Keyboard Controls:**
- `←`/`→`: Adjust by step
- `↑`/`↓`: Adjust by 10x step
- `0-9`: Jump to preset
- `Enter`: Confirm and save
- `Esc`: Cancel

#### 2. Text Editor

```python
config = {
    "type": "text",
    "current": "existing text",
    "placeholder": "enter value",
    "max_length": 50,
    "on_save": callback,
}
```

**Keyboard Controls:**
- Type: Enter text
- `Backspace`: Delete character
- `Enter`: Confirm and save
- `Esc`: Cancel

#### 3. Dropdown Editor

```python
config = {
    "type": "dropdown",
    "options": ["Option 1", "Option 2", "Option 3"],
    "selected": 0,           # Current selection index
    "on_select": callback,   # Called when selection changes
}
```

**Keyboard Controls:**
- `↑`/`↓`: Move selection
- `Enter`: Confirm selection
- `Esc`: Cancel

### Creating Custom Inline Editors

```python
from core.io.status.inline_editors import BaseInlineEditor

class MyCustomEditor(BaseInlineEditor):
    """Custom inline editor implementation."""

    def __init__(self, config, renderer, navigation_state):
        super().__init__(config, renderer, navigation_state)
        # Initialize custom state
        self.my_state = config.get("initial_state", [])

    def render(self, width: int) -> str:
        """Render editor display."""
        # Return editor content with ANSI colors
        return _fg("Custom Editor", T().text)

    def handle_key(self, key: KeyPress) -> bool:
        """Handle keyboard input.
        Returns True if editor should close (confirmed or cancelled)."""
        if key.key_type == KeyType.ENTER:
            self.confirmed = True
            return True
        elif key.key_type == KeyType.ESC:
            self.confirmed = False
            return True
        # Handle other keys...
        return False

    def get_result(self) -> Any:
        """Return final result if confirmed."""
        if self.confirmed:
            return self.my_state
        return None
```

---

## Advanced Patterns

### Pattern 1: Animated Widget

Widget with frame-by-frame animation.

```python
class AnimatedWidget:
    """Widget with cycling animation frames."""

    _frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
    _frame_index = 0
    _last_update = 0
    _frame_interval = 0.1  # Seconds between frames

    @classmethod
    def render(cls, width: int, ctx: WidgetContext) -> str:
        """Render animated widget."""
        import time

        now = time.time()

        # Update frame if interval passed
        if now - cls._last_update >= cls._frame_interval:
            cls._frame_index = (cls._frame_index + 1) % len(cls._frames)
            cls._last_update = now

        frame = cls._frames[cls._frame_index]
        return _fg(f"{frame} Processing", T().primary[0])
```

### Pattern 2: State Caching

Widget that caches expensive operations.

```python
class CachedWidget:
    """Widget with time-based cache."""

    _cache = None
    _cache_time = 0
    _cache_duration = 5  # Seconds

    @classmethod
    def _compute_value(cls) -> str:
        """Expensive computation - cache this."""
        # Simulate expensive operation
        import subprocess
        result = subprocess.run(
            ["complex", "command"],
            capture_output=True,
            text=True,
            timeout=5.0
        )
        return result.stdout.strip()

    @classmethod
    def render(cls, width: int, ctx: WidgetContext) -> str:
        """Render with caching."""
        import time

        now = time.time()

        # Check if cache is valid
        if (cls._cache is None or
            now - cls._cache_time > cls._cache_duration):
            cls._cache = cls._compute_value()
            cls._cache_time = now

        return cls._cache
```

### Pattern 3: Context-Aware Widget

Widget that adapts based on application state.

```python
def render_context_aware(width: int, ctx: WidgetContext) -> str:
    """Render widget that adapts to LLM state."""

    if not ctx or not ctx.llm_service:
        return _fg("LLM: unknown", T().text_dim)

    # Check LLM service state
    if ctx.llm_service.is_processing():
        # Show processing state
        return _fg("LLM: busy", T().warning[0])

    # Check for errors
    if ctx.llm_service.last_error:
        return _fg("LLM: error", T().error[0])

    # Show ready state
    return _fg("LLM: ready", T().success[0])
```

### Pattern 4: Composite Widget

Widget that combines multiple data sources.

```python
def render_composite(width: int, ctx: WidgetContext) -> str:
    """Render composite widget with multiple components."""

    components = []

    # Add profile info
    if ctx and ctx.profile_manager:
        profile = ctx.profile_manager.get_active_profile()
        if profile:
            name = profile.get_name()
            components.append(_fg(f"[{name}]", T().primary[0]))

    # Add model info
    if ctx and ctx.llm_service:
        model = ctx.llm_service.get_current_model()
        if model:
            components.append(_fg(model, T().text))

    # Add message count
    if ctx and ctx.llm_service:
        stats = ctx.llm_service.session_stats
        count = stats.get('messages', 0)
        components.append(_fg(f"({count} msg)", T().text_dim))

    # Join with separators
    separator = _fg(" | ", T().text_dim)
    return separator.join(components)
```

---

## Testing Widgets

### Unit Testing Render Functions

```python
import unittest
from unittest.mock import Mock
from core.io.status.core_widgets import WidgetContext

class TestMyWidget(unittest.TestCase):
    """Test widget rendering."""

    def test_render_with_context(self):
        """Test rendering with full context."""
        ctx = WidgetContext(
            profile_manager=Mock(),
            llm_service=Mock(),
        )

        # Setup mocks
        ctx.profile_manager.get_active_profile.return_value = Mock(
            get_name=Mock(return_value="test-profile")
        )

        # Render
        result = render_my_widget(20, ctx)

        # Assert
        self.assertIn("test-profile", result)

    def test_render_without_context(self):
        """Test rendering with no context."""
        result = render_my_widget(20, None)
        self.assertIsNotNone(result)

    def test_width_truncation(self):
        """Test that long content is truncated."""
        ctx = WidgetContext()
        result = render_my_widget(10, ctx)
        # Result should fit within width
        # (Note: ANSI codes don't count toward display width)
```

### Manual Testing with Tmux

Create a tmux test script:

```bash
#!/usr/bin/env bash
# tests/tmux/test_my_widget.sh

SOCKET_NAME="kollabor-test-$$"
SESSION_NAME="test-widget-$$"

# Start application
tmux -L "$SOCKET_NAME" new-session -d -s "$SESSION_NAME" "python main.py"
sleep 2

# Press Tab to enter navigation mode
tmux -L "$SOCKET_NAME" send-keys -t "$SESSION_NAME" Tab
sleep 0.5

# Capture output
OUTPUT=$(tmux -L "$SOCKET_NAME" capture-pane -t "$SESSION_NAME" -p)

# Check for widget presence
if echo "$OUTPUT" | grep -q "my-widget"; then
    echo "[PASS] Widget is displayed"
else
    echo "[FAIL] Widget not found"
    tmux -L "$SOCKET_NAME" kill-server
    exit 1
fi

# Cleanup
tmux -L "$SOCKET_NAME" kill-server
```

### Interactive Testing

```python
# Add test widget during development
registry.register(
    id="test-widget",
    name="Test Widget",
    description="Widget for testing",
    render_fn=lambda w, c: f"Width: {w}",
    category=WidgetCategory.CORE,
)

# Then:
# 1. Start application
# 2. Press Tab (navigation mode)
# 3. Press e (edit mode)
# 4. Navigate to + slot
# 5. Press Enter
# 6. Select "Test Widget"
# 7. Verify display
```

---

## Performance Considerations

### Render Frequency

Widgets are rendered on every frame (~60 FPS). Expensive operations in `render_fn` will cause performance issues.

**Bad:**
```python
def render_slow(width: int, ctx: WidgetContext) -> str:
    # This runs 60 times per second!
    result = subprocess.run(["slow-command"], capture_output=True)
    return result.stdout
```

**Good:**
```python
class CachedWidget:
    _cache = None
    _cache_time = 0

    @classmethod
    def render(cls, width: int, ctx: WidgetContext) -> str:
        import time
        if time.time() - cls._cache_time > 5:  # Only every 5 seconds
            cls._cache = subprocess.run(["slow-command"], capture_output=True).stdout
            cls._cache_time = time.time()
        return cls._cache or "computing..."
```

### State Management

Avoid heavy state in render functions. Use class-level caching or widget config for persistence.

**Good Pattern:**
```python
# Use widget config for persistent state
def render_with_state(width: int, ctx: WidgetContext) -> str:
    if ctx and ctx.layout_manager:
        state = ctx.layout_manager.get_widget_config(row, widget, "my_state", "default")
    else:
        state = "default"
    return f"State: {state}"
```

### Width Constraints

Always respect the width parameter. Content that exceeds width will break layout.

```python
def render_responsive(width: int, ctx: WidgetContext) -> str:
    long_text = "This is very long text that needs truncation"

    # Truncate to fit
    if len(long_text) > width:
        long_text = long_text[:width-3] + "..."

    return long_text
```

### Memory Management

Be careful with accumulating data in class variables.

```python
class MemorySafeWidget:
    # Use bounded cache
    _cache = {}
    _max_cache_size = 100

    @classmethod
    def render(cls, width: int, ctx: WidgetContext) -> str:
        # Check cache size
        if len(cls._cache) > cls._max_cache_size:
            # Clear old entries
            cls._cache.clear()

        # Add new entry
        key = str(width)
        cls._cache[key] = compute_value()
        return cls._cache.get(key, "")
```

---

## Plugin Integration

Plugins can easily register widgets using the `StatusWidgetAPI`.

### Basic Plugin Widget

```python
# plugins/my_plugin/__init__.py

from core.plugins.base import BasePlugin
from core.io.status import StatusWidgetAPI
from core.io.status.core_widgets import WidgetContext

class MyPlugin(BasePlugin):
    """Plugin with custom status widget."""

    async def initialize(self):
        """Initialize plugin and register widgets."""
        # Get widget API from app
        widget_api = self.app.get_widget_api()
        if widget_api:
            widget_api.register_widget(
                id="my-plugin-widget",
                name="My Plugin",
                description="Custom plugin widget",
                render_fn=self._render_widget,
                default_width="auto",
                min_width=15,
            )

    def _render_widget(self, width: int, ctx: WidgetContext) -> str:
        """Render plugin widget."""
        # Access plugin data
        plugin_data = self.get_plugin_data()
        return f"Plugin: {plugin_data}"

    def get_plugin_data(self) -> str:
        """Get plugin-specific data."""
        return "active"
```

### Interactive Plugin Widget

```python
class InteractivePlugin(BasePlugin):
    """Plugin with interactive widget."""

    async def initialize(self):
        widget_api = self.app.get_widget_api()
        if widget_api:
            widget_api.register_widget(
                id="interactive-plugin",
                name="Interactive",
                description="Interactive plugin widget",
                render_fn=self._render,
                default_width="auto",
                min_width=15,
            )

    def _render(self, width: int, ctx: WidgetContext) -> str:
        """Render widget with current value."""
        # Get value from plugin config
        value = self.config.get("my_plugin.value", 0)

        # Use inline slider from design system
        from core.ui.design_system import inline_slider
        slider = inline_slider(
            value=value,
            min_val=0,
            max_val=100,
            width=width - 10,
            show_value=True,
        )

        icon = _fg("PLUGIN", T().secondary[0])
        return f"{icon} {slider}"
```

---

## Related Documentation

- [Widget Status System (Features)](../features/widget-status-system.md) - User-facing guide
- [Widget Status System (Reference)](../reference/widget-status-system.md) - Technical reference
- [Design System](../reference/design-system.md) - UI components reference
- [Plugin Development Tutorial](../reference/plugin-development-tutorial.md) - Plugin development guide
- [Event System](../reference/event-types-reference.md) - Event bus documentation

---

## Appendix: Complete Widget Example

```python
"""
Complete example: Docker Status Widget

This widget shows running Docker containers with interactive actions.
"""

import logging
import subprocess
from typing import Any, Dict
from core.ui.design_system import T, inline_dropdown, _fg
from core.io.status.widget_registry import StatusWidget, WidgetCategory
from core.io.status.core_widgets import WidgetContext

logger = logging.getLogger(__name__)


class DockerStatusWidget:
    """Docker status widget with caching and interaction."""

    _cache = None
    _cache_time = 0
    _cache_duration = 10  # 10 seconds

    @classmethod
    def _get_docker_status(cls) -> tuple:
        """Get Docker container counts (running, total)."""
        try:
            # Check if Docker is available
            subprocess.run(
                ["docker", "--version"],
                capture_output=True,
                check=True,
                timeout=1.0
            )
        except (subprocess.CalledProcessError, FileNotFoundError, subprocess.TimeoutExpired):
            return (0, 0, False)

        try:
            # Get running containers
            running_result = subprocess.run(
                ["docker", "ps", "-q"],
                capture_output=True,
                text=True,
                timeout=2.0
            )
            running = len([line for line in running_result.stdout.strip().split('\n') if line])

            # Get all containers
            all_result = subprocess.run(
                ["docker", "ps", "-aq"],
                capture_output=True,
                text=True,
                timeout=2.0
            )
            total = len([line for line in all_result.stdout.strip().split('\n') if line])

            return (running, total, True)
        except subprocess.TimeoutExpired:
            logger.warning("Docker command timed out")
            return (0, 0, False)

    @classmethod
    def render(cls, width: int, ctx: WidgetContext) -> str:
        """Render Docker status widget."""
        import time

        now = time.time()

        # Check cache
        if (cls._cache is None or
            now - cls._cache_time > cls._cache_duration):
            cls._cache = cls._get_docker_status()
            cls._cache_time = now

        running, total, available = cls._cache

        if not available:
            return _fg("docker: unavailable", T().text_dim)

        icon = _fg("🐳", T().secondary[0])

        # Format based on width
        if width < 15:
            return f"{icon} {running}/{total}"
        else:
            return f"{icon} {running} running, {total} total"

    @classmethod
    async def activate(cls, widget_id: str, ctx: WidgetContext) -> Dict[str, Any]:
        """Handle activation - show Docker actions."""
        return {
            "type": "action",
            "title": "Docker Actions",
            "options": [
                {
                    "label": "List Running",
                    "description": "Show running containers",
                    "action": "docker_ps",
                    "handler": "_docker_ps_handler",
                },
                {
                    "label": "View Stats",
                    "description": "Container resource usage",
                    "action": "docker_stats",
                    "handler": "_docker_stats_handler",
                },
                {
                    "label": "Prune Stopped",
                    "description": "Remove stopped containers",
                    "action": "docker_prune",
                    "handler": "_docker_prune_handler",
                    "confirm": True,
                },
            ],
        }


# Registration function
def register_docker_widget(registry):
    """Register the Docker status widget."""
    registry.register(
        id="docker-status",
        name="Docker",
        description="Docker container status",
        render_fn=DockerStatusWidget.render,
        category=WidgetCategory.CORE,
        default_width="auto",
        min_width=12,
        interactive=True,
        interaction_type="action",
        on_activate=DockerStatusWidget.activate,
    )
```
