# Widget Status System

**Version:** 1.1
**Status:** Implemented
**Last Updated:** 2026-01-19

## Overview

The Widget Status System is a modular, configurable status area system for the Kollabor CLI that allows users to customize which information widgets are displayed and how they are arranged in the terminal status bar. It replaces the previous fixed three-area status system with a flexible multi-row layout supporting up to 6 rows of interactive widgets.

## Key Features

- **Modular Widget Registry**: Central registry for status widgets that can be extended by plugins
- **Configurable Layout**: Users can customize widget arrangement across 6 rows
- **Interactive Navigation**: Keyboard-driven navigation system with three modes
- **Script-Based Widgets**: Zero-code customization through shell scripts
- **Visual Effects**: Shimmer, pulse, and ultra-shimmer effects for widgets
- **Color Customization**: Per-widget background color options
- **Persistence**: Layout configuration saved to user config file

## Architecture

### Component Overview

```
core/io/status/
├── widget_registry.py       # StatusWidgetRegistry, StatusWidget, WidgetWidth
├── layout_manager.py        # StatusLayoutManager, StatusLayout, RowConfig, WidgetConfig
├── layout_renderer.py       # StatusLayoutRenderer - renders status from layout
├── navigation_manager.py    # StatusNavigationManager - keyboard routing
├── navigation_state.py      # StatusNavigationState - navigation state tracking
├── core_widgets.py          # Built-in widgets (cwd, profile, model, etc.)
├── interactive_widgets.py   # Interactive widgets (temperature, max-tokens, etc.)
├── script_widgets.py        # Script-based widget system
├── script_refresh_scheduler.py  # Time-based refresh for script widgets
├── script_modal_handlers.py     # Modal interactions for script widgets
├── script_action_handlers.py    # Action execution for script widgets
├── widget_picker.py         # Widget picker modal for adding widgets
├── interaction_handler.py  # WidgetInteractionHandler
├── inline_editors.py        # InlineSliderEditor, InlineTextEditor, InlineDropdownEditor
├── help_system.py           # First-run help and help overlay
├── toggle_handler.py        # Toggle widget state management
├── constants.py             # Color names, effect names, constants
└── plugin_api.py            # StatusWidgetAPI for plugin development
```

### Core Components

#### 1. StatusWidgetRegistry (`widget_registry.py`)

Central registry for all status widgets. Manages widget registration, lookup, and rendering.

**Key Classes:**
- `StatusWidget`: Widget definition with render function, metadata, and interaction properties
- `WidgetWidth`: Width specification (auto, relative, fixed)
- `WidgetCategory`: CORE or PLUGIN
- `StatusWidgetRegistry`: Registry instance

**Usage:**
```python
# Register a widget
registry.register(
    id="my-widget",
    name="My Widget",
    description="Shows something useful",
    render_fn=render_my_widget,
    category=WidgetCategory.CORE,
    interactive=True,
    interaction_type="command",
    command="/mycommand",
)
```

#### 2. StatusLayoutManager (`layout_manager.py`)

Manages persistent layout configuration stored in `~/.kollabor-cli/config.json`.

**Key Classes:**
- `StatusLayout`: Complete layout with version, rows, and backup
- `RowConfig`: Single row configuration (id, visible, widgets)
- `WidgetConfig`: Widget configuration (id, width, color, effect, config)
- `StatusLayoutManager`: Layout manipulation and persistence

**Configuration Key:** `status_layout`

**Default Layout:**
- Row 1: cwd, profile, model, status, stats (visible)
- Row 2: agent, skills, test-toggle (visible)
- Row 3: tasks, tmux, bg-tasks (hidden)
- Row 4-6: Empty (hidden)

#### 3. StatusLayoutRenderer (`layout_renderer.py`)

Renders the status area based on configured layout using widgets from registry.

**Features:**
- Applies widget background colors
- Renders visual effects (shimmer, pulse, ultra-shimmer)
- Draws selection highlights for navigation
- Shows mode indicator (INPUT/NAVIGATE/EDIT)
- Handles slot rendering in edit mode

**Design System Integration:**
- Uses `T()` theme colors from `core.ui.design_system`
- Solid block style (▄▀) for selection highlights
- TagBox pattern for structured rendering

#### 4. StatusNavigationManager (`navigation_manager.py`)

Manages navigation state and keyboard routing for interactive widgets.

**Navigation Modes:**
- `INPUT`: Normal input mode (default)
- `STATUS_FOCUS`: Navigation mode (navigate between widgets)
- `EDIT`: Edit mode (add/remove/rearrange widgets)

**Keyboard Shortcuts:**
- `Tab`: Toggle navigation mode
- `Esc`: Exit mode (cascading: EDIT -> STATUS_FOCUS -> INPUT)
- `e`: Enter edit mode (from STATUS_FOCUS)
- `Arrow keys`: Navigate between widgets/rows
- `Enter`: Activate widget or add widget (on slot)
- `d`: Delete widget (edit mode only)
- `c`: Toggle widget color (edit mode only)
- `F1` or `?`: Show help overlay

#### 5. StatusNavigationState (`navigation_state.py`)

Thread-safe navigation state tracking with asyncio.Lock.

**Properties:**
- `mode`: Current navigation mode
- `selected_row`: Currently selected row index
- `selected_widget_index`: Selected widget/slot index
- `selected_slot_index`: Slot index (in edit mode)
- `selection_type`: WIDGET or SLOT
- `interaction_active`: Whether widget interaction is active
- `active_widget_id`: ID of actively interacting widget

#### 6. Core Widgets (`core_widgets.py`)

Built-in widgets included with Kollabor:

| Widget ID | Description | Interactive |
|-----------|-------------|-------------|
| `cwd` | Current working directory (smart path compression) | Yes (`/cd`) |
| `profile` | Active LLM profile | Yes (`/profile`) |
| `model` | Current model name | Yes (`/model`) |
| `endpoint` | API endpoint URL | No |
| `status` | Application status (Ready/Busy/Error) | No |
| `stats` | Message/token statistics | No |
| `agent` | Active agent | Yes (`/agent`) |
| `skills` | Active skills | Yes (`/skill`) |
| `tasks` | Background tasks status | No |
| `tmux` | Tmux session count | Yes (`/terminal`) |
| `bg-tasks` | Background tasks indicator | No |
| `git-branch` | Current git branch (with 5s caching) | No |
| `git-status` | Git working directory status (clean/dirty) | No |
| `token-io` | Animated token upload/download activity | No |
| `clock` | Current time display | No |
| `label` | Editable text label | Yes (inline edit) |
| `spacer` | Empty space for visual separation | No |
| `divider` | Vertical separator line | No |

**Special Widgets:**

**test-toggle**
- For verification testing
- Cycles through: TEST:OFF -> TEST:ON -> TEST:AUTO
- Demonstrates toggle widget pattern

**label**
- Editable text widget with inline editor
- Press Enter to edit text in place
- Persists custom text to layout config
- Example: Use as project name, session marker

**token-io**
- Animated token upload/download indicator
- Shows prompt tokens (↑) during upload
- Shows completion tokens (↓) during streaming
- Color-coded: green for upload, blue for download
- Variable-speed animation mimics real token flow
- Formats: "↑ 1.2K", "↑500 ↓200", "--" (idle)

### Widget Types and Interactions

#### 1. Command Widgets (Primary)

Execute a slash command when activated. Reuses existing command infrastructure.

```python
registry.register(
    id="profile",
    name="Profile",
    description="Active LLM profile",
    render_fn=render_profile,
    interactive=True,
    interaction_type="command",
    command="/profile",  # Executes this command on Enter
)
```

#### 2. Toggle Widgets

Click to toggle between states (boolean or cyclic).

```python
registry.register(
    id="test-toggle",
    name="Test Toggle",
    description="Toggle test mode",
    render_fn=render_test_toggle,
    interactive=True,
    interaction_type="toggle",
    states=["OFF", "ON", "AUTO"],
)
```

**Toggle Shortcuts:**
- `Space`: Quick toggle (in navigation mode)
- `Enter`: Activate with state cycling
- `←`/`→`: Cycle states (when activated)

#### 3. Inline Edit Widgets

Edit value directly in status area (no modal).

```python
registry.register(
    id="temperature",
    name="Temperature",
    description="Model temperature",
    render_fn=render_temperature,
    interactive=True,
    interaction_type="inline_edit",
    on_activate=edit_temperature,
    edit_config={
        "min": 0.0,
        "max": 2.0,
        "step": 0.1,
    }
)
```

**Inline Edit Shortcuts:**
- `←`/`→`: Adjust value by step
- `↑`/`↓`: Adjust by 10x step
- `Enter`: Confirm and save
- `Esc`: Cancel and revert

#### 4. Action Widgets

Execute command or function directly.

```python
registry.register(
    id="session-name",
    name="Session",
    description="Project session",
    render_fn=render_session,
    interactive=True,
    interaction_type="action",
    actions=[
        {"key": "l", "label": "Open Logs", "action": "open_logs"},
        {"key": "c", "label": "View Conversations", "action": "open_convs"},
    ]
)
```

## Script-Based Widgets

### Directory Structure

```
~/.kollabor-cli/status-widgets/       # Global widgets
├── git-branch.sh
├── docker-status.sh
└── python-env.sh

.kollabor-cli/status-widgets/         # Project-specific widgets
├── project-health.sh
└── deployment-status.sh
```

### Script Widget Format

Comment headers (recommended):

```bash
#!/bin/bash
# widget-id: git-branch
# name: Git Branch
# description: Shows current git branch
# category: git
# interactive: true
# interaction-type: modal
# on-activate: ./git-branch-modal.sh
# refresh: 5s
# hooks: post_user_input, pre_api_request
# min-width: 15
# timeout: 3s

# Render function (stdout becomes widget content)
branch=$(git branch --show-current 2>/dev/null || echo "no-git")
dirty=$(git diff --quiet 2>/dev/null || echo "*")
echo "⎇ ${branch}${dirty}"
```

### Metadata Fields

| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `widget-id` | string | Yes | - | Unique identifier (kebab-case) |
| `name` | string | Yes | - | Display name in picker |
| `description` | string | Yes | - | Brief description |
| `category` | string | No | `"custom"` | Widget category |
| `interactive` | boolean | No | `false` | Widget is clickable |
| `interaction-type` | string | No | - | modal, toggle, action, inline_edit |
| `on-activate` | string | No | - | Script to run on activation |
| `refresh` | string | No | `"manual"` | Refresh strategy |
| `hooks` | string | No | - | Comma-separated hook names |
| `min-width` | int | No | `10` | Minimum width in characters |
| `timeout` | string | No | `"5s"` | Max execution time |
| `color` | boolean | No | `true` | Allow ANSI color codes |

### Refresh Strategies

- **Manual** (default): Only updates when triggered or event fires
- **Time-based**: `5s`, `1m`, `30s` - polls at interval
- **Hook-based**: `on-event` with hooks list - runs on events
- **Smart**: Time + hooks combined
- **Once**: Runs once at startup

## Visual Effects

Widgets can have visual effects applied:

| Effect | Description |
|--------|-------------|
| `none` | No effect (default) |
| `shimmer` | Subtle wave animation |
| `pulse` | Pulsing brightness |
| `ultra` | Fast shimmer animation |

Effects are rendered by `StatusLayoutRenderer` using effect instances from `core.io.visual_effects`. Effect state is cached globally in `_widget_effects` dict to maintain animation position across renders.

## Widget Colors

Per-widget background colors can be customized:

| Color Name | Description | RGB Tuple |
|------------|-------------|-----------|
| `none` | Transparent (default) | None |
| `dark0` | Dark background | `T().dark[0]` |
| `dark1` | Slightly lighter | `T().dark[1]` |
| `primary0` | Primary accent | `T().primary[0]` |
| `secondary0` | Secondary accent | `T().secondary[0]` |

Press `c` in edit mode to cycle through colors.

## Navigation Mode Details

### Mode Transitions

```
INPUT --[Tab]--> STATUS_FOCUS --[e]--> EDIT
                    |                |
                    |<---[Esc]-------|
                    |
INPUT <---[Esc]-----|
```

### STATUS_FOCUS Mode

- Input box is **hidden**
- Status area shows selection highlight on widgets
- Arrow keys move selection between widgets
- Enter activates selected widget (executes slash command)
- `e` enters Edit Mode
- Esc returns to Input Mode

### EDIT Mode

- Input box remains hidden
- Mode indicator shows "EDIT"
- Slots (+ symbols) appear between widgets
- Empty rows become visible (all available rows shown)
- Arrow keys navigate between widgets AND slots
- Enter on slot opens Widget Picker
- Enter on widget activates it
- `d` deletes selected widget (with confirmation if last in row)
- `c` toggles widget color (cycles through none, dark0, dark1, primary0, secondary0)
- `x` toggles widget effect (cycles through none, shimmer, pulse, ultra)
- `A` adds new row (up to 6 max)
- `R` removes empty row
- Esc returns to STATUS_FOCUS

### Row Management

In Edit Mode, you can add and remove rows:

**Adding Rows:**
- Press `A` to add a new row (up to 6 maximum)
- New rows are added as hidden/empty
- Add widgets to make them visible

**Removing Rows:**
- Press `R` to remove the currently selected empty row
- Cannot remove rows that contain widgets
- Cannot remove below 1 row minimum

### Widget Picker Modal

Accessible from EDIT mode by pressing Enter on a slot.

**Features:**
- Arrow keys or `j`/`k` to navigate
- Number keys `1-9` for quick selection
- `Tab` cycles target row
- `Enter` selects widget
- `Esc` cancels

## Plugin API

Plugins can register custom widgets using `StatusWidgetAPI`:

```python
from core.io.status import StatusWidgetAPI

class MyPlugin(BasePlugin):
    def initialize(self):
        widget_api = StatusWidgetAPI(self.event_bus, self.config)

        widget_api.register_widget(
            id="my-custom-widget",
            name="My Widget",
            description="Custom plugin widget",
            render_fn=self.render_widget,
            category="plugin",
            interactive=True,
            interaction_type="command",
            command="/mycommand",
        )
```

## First-Run Help

On first run, help modal is shown explaining navigation:

```
NEW: Interactive Status Widgets

You can now interact with the status bar!

Press Tab to enter Navigation Mode
  ← → to move between widgets
  Enter to activate widget
  Esc to return to input

Press F1 or ? anytime for help

[Enter] Got it!  [D] Don't show again
```

Help overlay is accessible anytime via `F1` or `?` key.

## Configuration

### Layout Configuration

Stored in `~/.kollabor-cli/config.json` under `status_layout` key:

```json
{
  "status_layout": {
    "version": 1,
    "rows": [
      {
        "id": 1,
        "visible": true,
        "widgets": [
          {
            "id": "cwd",
            "width": {"type": "auto"},
            "color": "none",
            "effect": "none",
            "config": {}
          },
          {
            "id": "profile",
            "width": {"type": "auto"},
            "color": "dark0",
            "effect": "shimmer",
            "config": {}
          }
        ]
      }
    ]
  }
}
```

### Widget Configuration

Per-widget configuration in `config` field:

```json
{
  "id": "git-branch",
  "width": {"type": "fixed", "value": 20},
  "color": "primary0",
  "effect": "none",
  "config": {
    "show_dirty": true,
    "show_ahead_behind": true,
    "refresh_interval": "5s"
  }
}
```

## MessageDisplayCoordinator Integration

The navigation system integrates with `MessageDisplayCoordinator` to prevent render conflicts:

```python
# Check if navigation is allowed
if coordinator.is_writing_messages():
    return False  # Cannot enter navigation

# Signal navigation active
coordinator.set_navigation_active(True)

# Restore after navigation exits
coordinator.set_navigation_active(False)
```

This prevents navigation from interfering with LLM streaming or message display.

## Error Handling

### Script Execution Safety

- **Timeout Protection**: All scripts run with 5s default timeout
- **Path Validation**: Scripts must be in designated directories
- **Input Sanitization**: User input validated before passing to scripts
- **Parameterized Commands**: No shell interpolation of user input

### Error Recovery

- Widget render errors show `[widget-id:err]` placeholder
- Script timeouts return cached value or error state
- Navigation errors are logged and ignored
- Invalid configurations fall back to defaults

## Testing

Comprehensive test plan in `tests/INTERACTIVE_STATUS_WIDGETS_TEST_PLAN.md`:

- Navigation mode transitions
- Add/remove widgets
- Widget picker functionality
- Persistence across restarts
- Visual feedback accuracy
- Error handling

Run tmux tests:
```bash
./tests/tmux/verify_navigation_mode.sh
./tests/tmux/verify_widget_picker.sh
./tests/tmux/verify_persistence.sh
```

## Known Issues

See `known_issues/` directory for tracked issues and resolutions:

- Active: Issues needing fixes
- In Progress: Issues being worked on
- Resolved: Fixed issues with details
- Wont Fix: Issues not being addressed

## Future Enhancements

Potential future features:

1. **Widget Groups**: Collapsible groups, tabbed areas
2. **Widget Communication**: Events between widgets, shared state
3. **Remote Widgets**: HTTP/WebSocket widgets
4. **Widget Marketplace**: Community widget repository
5. **Advanced Interactions**: Drag-and-drop, mini-charts, progress bars

## References

- **Spec**: `docs/specs/interactive-status-widgets-spec.md`
- **Test Plan**: `tests/INTERACTIVE_STATUS_WIDGETS_TEST_PLAN.md`
- **Design System**: `core/ui/design_system/__init__.py`
- **Event System**: `core/events/bus.py`
- **Terminal Rendering**: `core/io/terminal_renderer.py`
- **Input Handling**: `core/io/input_handler.py`

## Migration from Old Status System

The old three-area status system (status_areas A, B, C) has been replaced:

**Old System:**
- Fixed 3 areas with specific widgets
- Limited customization
- No interactive navigation

**New System:**
- 6 configurable rows
- Interactive widget navigation
- Script-based custom widgets
- Visual effects and colors

Migration is automatic - old configs are ignored and new default layout is created on first run.

## Data Flow Diagrams

### Navigation Flow

```
User Input (Tab)
    │
    ▼
InputHandler detects Tab
    │
    ▼
StatusNavigationManager.enter_navigation_mode()
    │
    ├─► MessageDisplayCoordinator.set_navigation_active(True)
    ├─► Hide input box
    ├─► Render status with selection
    │
    ▼
User Input (Arrow keys)
    │
    ▼
StatusNavigationManager.move_selection()
    │
    ├─► Update state.selected_row
    ├─► Update state.selected_widget_index
    ├─► Re-render status with new selection
    │
    ▼
User Input (Enter)
    │
    ▼
StatusNavigationManager.activate_selected()
    │
    ├─► Get selected widget
    ├─► Route to WidgetInteractionHandler
    ├─► Execute widget activation
    │
    ▼
Return to Input Mode (if modal closes)
```

### Script Widget Execution Flow

```
ScriptWidgetManager.discover_widgets()
    │
    ├─► Scan ~/.kollabor-cli/status-widgets/
    ├─► Scan .kollabor-cli/status-widgets/
    ├─► Parse metadata from script comments
    ├─► Create ScriptWidget instances
    │
    ▼
register_script_widgets(registry, manager)
    │
    ├─► Create render functions for each widget
    ├─► Register with StatusWidgetRegistry
    │
    ▼
LayoutRenderer.render_status()
    │
    ├─► Get layout from StatusLayoutManager
    ├─► For each widget in layout:
    │   ├─► Call widget.render_fn(width, context)
    │   └─► render_fn calls manager.execute_script()
    │       ├─► Check cache (if enabled)
    │       ├─► Execute script with timeout
    │       ├─► Cache output
    │       └─► Return output
    │
    ▼
Display rendered widget content
```

### Edit Mode Flow

```
Navigation Mode active
    │
    ▼
User Input (e)
    │
    ▼
StatusNavigationManager.enter_edit_mode()
    │
    ├─► Set mode to EDIT
    ├─► Show "+" slots between widgets
    ├─► Show all rows (including empty)
    │
    ▼
User navigates to slot
    │
    ▼
User Input (Enter on slot)
    │
    ▼
WidgetPickerModal.show()
    │
    ├─► Display available widgets
    ├─► Filter by user input
    ├─► User selects widget
    │
    ▼
StatusLayoutManager.insert_widget_at_position()
    │
    ├─► Add widget to layout
    ├─► Auto-show row if hidden
    ├─► Mark dirty
    │
    ▼
LayoutManager.save()
    │
    ▼
Re-render status with new widget
```

## MessageDisplayCoordinator Integration

**Critical:** The navigation system integrates with `MessageDisplayCoordinator` to prevent render conflicts during LLM streaming and message display.

### Coordination Points

```python
# Entering navigation mode
async def enter_navigation_mode(self) -> None:
    # Check if LLM is streaming
    if self.coordinator.is_writing_messages():
        logger.warning("Cannot enter navigation during message display")
        return

    # Signal coordinator: navigation active
    self.coordinator.set_navigation_active(True)

    # Render navigation state
    await self.render_navigation_state()

# Exiting navigation mode
async def exit_navigation_mode(self) -> None:
    # Signal coordinator: navigation done
    self.coordinator.set_navigation_active(False)

    # Restore normal status rendering
    await self.renderer.render_status()
```

### Coordinator Extensions

```python
class MessageDisplayCoordinator:
    def __init__(self):
        # ... existing fields ...
        self.navigation_active: bool = False

    def set_navigation_active(self, active: bool) -> None:
        """Set navigation active state."""
        self.navigation_active = active

    def is_writing_messages(self) -> bool:
        """Check if writing messages or navigation active."""
        return self.writing_messages or self.navigation_active

    async def display_queued_messages(self):
        """Skip display if navigation active."""
        if self.navigation_active:
            return
        # ... existing logic ...
```

## Inline Editor System

### Editor Types

```python
# Slider editor (for numeric values)
from core.io.status import InlineSliderEditor

editor = InlineSliderEditor(
    initial_value=0.7,
    min_val=0.0,
    max_val=2.0,
    step=0.1,
    width=20,
)

result = await editor.edit()
if result.confirmed:
    new_value = result.value

# Text input editor
from core.io.status import InlineTextEditor

editor = InlineTextEditor(
    initial_value="text",
    max_length=50,
    placeholder="type here...",
    width=30,
)

# Dropdown editor
from core.io.status import InlineDropdownEditor

editor = InlineDropdownEditor(
    options=["option1", "option2", "option3"],
    selected_index=0,
    width=25,
)
```

### Inline Editor Service

The `InlineEditorService` coordinates inline editing in the status area:

**File:** `core/io/status/inline_editor_service.py`

**Key Methods:**
```python
async def edit_slider(
    self,
    row_id: int,
    widget_index: int,
    config: Dict[str, Any],
) -> EditorResult:
    """Edit inline slider widget."""

async def edit_text(
    self,
    row_id: int,
    widget_index: int,
    config: Dict[str, Any],
) -> EditorResult:
    """Edit inline text input widget."""

async def edit_dropdown(
    self,
    row_id: int,
    widget_index: int,
    config: Dict[str, Any],
) -> EditorResult:
    """Edit inline dropdown widget."""
```

## Script Refresh Scheduler

### Background Task Scheduling

The `ScriptWidgetRefreshScheduler` manages background widget refresh:

**File:** `core/io/status/script_refresh_scheduler.py`

**Refresh Modes:**
```python
class RefreshMode(Enum):
    MANUAL = "manual"       # Only explicit refresh
    TIME = "time"           # Time-based polling
    HOOK = "hook"           # Event-based refresh
    SMART = "smart"         # Time + events combined
    ONCE = "once"           # Run once at startup
```

**Usage:**
```python
from core.io.status import ScriptWidgetManager

manager = ScriptWidgetManager()
await manager.initialize_refresh_scheduler(event_bus)

# Scheduler handles:
# - Time-based refresh (5s, 1m, etc.)
# - Event-based refresh (post_user_input, pre_api_request)
# - Smart refresh (time + events)
# - Once-only refresh

await manager.shutdown_refresh_scheduler()
```

## Git Widgets

The git widgets provide repository information with intelligent caching to minimize subprocess overhead.

### Git Branch Widget (`git-branch`)

Shows the current git branch name with smart truncation:
- Full format (12+ chars): "git: feature/branch-name"
- Compact format (8+ chars): "git: feat..name"
- Minimal format (6+ chars): "git: br.."
- Returns empty string if not in a git repository

**Implementation Features:**
- Uses subprocess to call `git rev-parse --abbrev-ref HEAD`
- Caches branch name for 5 seconds (CACHE_TTL)
- Middle truncation for long branch names
- Automatic detection of git repository

### Git Status Widget (`git-status`)

Shows working directory status (clean/dirty):
- Full format (12+ chars): "clean" or "M:3 A:1 D:0"
- Compact format (8+ chars): "clean" or "M:3 +1"
- Minimal format (4+ chars): "✓" or "M3"
- Ultra-minimal (2+ chars): "" or "*"
- Returns empty string if not in a git repository

**Status Indicators:**
- `M`: Modified files count
- `A`: Added files count
- `D`: Deleted files count
- Green "✓" or "clean" for clean working directory
- Yellow/amber for dirty state

**Implementation Features:**
- Caches status for 5 seconds (CACHE_TTL)
- Uses subprocess to call `git status --porcelain`
- Parses porcelain output to count file changes
- Separate subprocess calls for branch and status

### Git Widget Caching

Both git widgets use a shared cache with time-based invalidation:

```python
# Global cache in core_widgets.py
_git_cache = {
    "branch": None,
    "status": None,
    "timestamp": 0.0,
}
CACHE_TTL = 5.0  # seconds

def _get_cached_git_data() -> dict:
    """Get cached git data, refreshing if TTL expired."""
    global _git_cache
    now = time.time()

    # Refresh cache if expired
    if now - _git_cache["timestamp"] > CACHE_TTL:
        # Run git commands and update cache
        result = subprocess.run(["git", "rev-parse", "--abbrev-ref", "HEAD"], ...)
        _git_cache["branch"] = result.stdout.strip()

        result = subprocess.run(["git", "status", "--porcelain"], ...)
        _git_cache["status"] = parse_status(result.stdout)

        _git_cache["timestamp"] = now

    return _git_cache
```

**Benefits of Caching:**
- Avoids subprocess overhead on every render
- Renders are frequent (60 FPS for animations)
- 5-second TTL keeps info reasonably fresh
- Single cache for both widgets (shared subprocess calls)

### Using Git Widgets

Add git widgets to your layout:
1. Press `Tab` to enter navigation mode
2. Press `e` to enter edit mode
3. Navigate to a slot and press `Enter`
4. Select "Git Branch" or "Git Status" from picker

**Example Layout:**
```
Row 1: cwd | profile | model | git-branch | status
Row 2: git-status | stats | agent | skills
```

## Token I/O Widget

The Token I/O widget (`token-io`) provides real-time visual feedback during LLM API communication with animated upload/download indicators.

### Widget States

The widget cycles through 4 states during a conversation:

1. **Idle** (default)
   - Shows: "--" or total token count dimmed
   - Example: "1.5K tok" or "--"

2. **Upload** (sending prompt)
   - Shows: "↑ 1.2K" with green arrow
   - Animated token counter increments
   - Color: green (success[0])
   - Animation mimics real API upload speed

3. **Waiting** (waiting for response)
   - Shows: "1.2K tok" (upload total, dimmed)
   - No arrows
   - Brief state before download starts

4. **Download** (streaming response)
   - Shows: "↑1.2K ↓500" (both arrows)
   - Upload dimmed, download highlighted
   - Blue arrow for download (primary[0])
   - Animated token counter increments as chunks arrive

### Animation Behavior

The widget uses variable-speed animation for organic feel:
- Slow start (0-20% progress): 1-3 tokens per frame
- Fast middle (20-80% progress): 8-25 tokens per frame
- Slow end (80-100%): 1-4 tokens per frame
- Random variance for natural appearance
- 30-80ms update intervals

### Event Integration

The widget integrates with LLM service events via `get_token_io_state()`:

```python
# In pre_api_request handler
from core.io.status.core_widgets import get_token_io_state

async def on_pre_api_request(context):
    token_io = get_token_io_state()
    token_io.start_upload(prompt_token_count)

# When response starts
async def on_response_start(context):
    token_io = get_token_io_state()
    token_io.start_waiting()

# On each chunk
async def on_response_chunk(context):
    token_io = get_token_io_state()
    token_io.add_chunk(len(chunk))

# On response complete
async def on_response_complete(context):
    token_io = get_token_io_state()
    token_io.finish(actual_output_tokens)
```

### Display Formats

Width-aware formatting:
- Wide (14+): "↑ 1.2K ↓500" (both arrows with formatting)
- Medium (10+): "↑1.2K ↓500" (both, compact)
- Narrow (10): "↑500 ↓200" (short format)
- Minimal (5-9): "↓500" (download only)
- Ultra-narrow (<5): "↓" (arrow only)

### Token Formatting

Token counts are formatted for readability:
- < 1000: "123"
- < 1M: "1.2K" or "1K" (short)
- >= 1M: "1.5M" or "1M" (short)

## Widget Context API

The `WidgetContext` class provides services to widget render functions:

```python
class WidgetContext:
    def __init__(
        self,
        llm_service: Any = None,
        profile_manager: Any = None,
        agent_manager: Any = None,
        config: Any = None,
        tmux_plugin: Any = None,
        background_tasks_plugin: Any = None,
        navigation_state: Any = None,
        layout_manager: Any = None,
    )
```

### Available Services

**LLM Service** (`llm_service`):
- Access to LLM API state
- `is_processing`: Whether LLM is working
- `session_stats`: Message/token counts
- Token count tracking

**Profile Manager** (`profile_manager`):
- `get_active_profile()`: Current LLM profile
- `get_model()`: Active model name
- `get_endpoint()`: API endpoint URL

**Agent Manager** (`agent_manager`):
- `get_active_agent()`: Current agent
- `active_skills`: Set of active skill names
- `list_skills()`: All available skills

**Config** (`config`):
- Application configuration
- `get(key, default)`: Get config values
- User preferences and settings

**Plugin References**:
- `tmux_plugin`: Tmux session management
- `background_tasks_plugin`: Background task registry
- `navigation_state`: Navigation mode state
- `layout_manager`: Layout configuration manager

### Using Widget Context

```python
def render_my_widget(width: int, ctx: Optional[WidgetContext]) -> str:
    """Render my widget with context access."""
    try:
        # Access services via ctx
        if ctx and ctx.llm_service:
            is_processing = ctx.llm_service.is_processing

        if ctx and ctx.profile_manager:
            profile = ctx.profile_manager.get_active_profile()
            model = profile.get_model() if profile else "unknown"

        # Width-aware formatting
        if width >= 20:
            return f"MyWidget: {model}"
        else:
            return f"MW:{model[:width-3]}..."
    except Exception as e:
        logger.error(f"My widget error: {e}")
        return "[MW:err]"
```

## Enhanced Inline Editor Service

The `InlineEditorService` provides a centralized implementation for inline editing in status widgets.

### Service Architecture

**File:** `core/io/status/inline_editor_service.py`

The service handles:
- Editor instantiation based on type
- Keyboard input loop with raw mode
- State updates and re-rendering
- Callback execution (on_save, on_change, on_select)

### Editor Configuration

All editors share a common configuration format:

```python
edit_config = {
    "type": "slider",  # or "text", "dropdown"
    "current": 0.7,    # Current value

    # Type-specific options
    "min": 0.0,       # Slider: minimum
    "max": 2.0,       # Slider: maximum
    "step": 0.1,      # Slider: increment

    "placeholder": "Enter text...",  # Text: placeholder
    "max_length": 50,                # Text: max chars

    "options": ["opt1", "opt2"],  # Dropdown: choices

    # Callbacks
    "on_save": callback_fn,       # Called on Enter
    "on_change": callback_fn,     # Called on each change
    "on_select": callback_fn,     # Dropdown selection

    # Auto-save to layout config
    "config_key": "my_value",     # Key in widget.config
}
```

### Slider Editor

```python
editor = InlineSliderEditor(
    initial_value=0.7,
    min_val=0.0,
    max_val=2.0,
    step=0.1,
    width=20,
    presets=[0.1, 0.5, 0.7, 1.0],  # Quick-select values
)

result = await editor.edit()
if result.confirmed:
    new_value = result.value
```

**Keyboard shortcuts:**
- `←`/`→`: Adjust by step
- `↑`/`↓`: Adjust by 10x step
- `0-9`: Quick-select preset values
- `Enter`: Confirm
- `Esc`: Cancel

### Text Editor

```python
editor = InlineTextEditor(
    initial_value="Hello",
    max_length=50,
    placeholder="Type here...",
    width=30,
)

result = await editor.edit()
if result.confirmed:
    new_text = result.value
```

**Keyboard shortcuts:**
- Alphanumeric: Type characters
- `Backspace`: Delete character
- `Enter`: Confirm
- `Esc`: Cancel

### Dropdown Editor

```python
editor = InlineDropdownEditor(
    options=["Option 1", "Option 2", "Option 3"],
    selected_index=0,
    width=25,
)

result = await editor.edit()
if result.confirmed:
    selected_option = result.value
```

**Keyboard shortcuts:**
- `↑`/`↓` or `j`/`k`: Navigate options
- `Enter`: Select option
- `Esc`: Cancel

### Integration Example

```python
async def activate_my_widget(widget_id: str, ctx: WidgetContext) -> dict:
    """Activate inline editor for my widget."""
    # Get position from navigation state
    row_id = ctx.navigation_state.selected_row
    widget_index = ctx.navigation_state.selected_widget_index

    async def on_save(new_value: str) -> None:
        """Save to layout config."""
        ctx.layout_manager.update_widget_config(
            row_id + 1, widget_index, "my_key", new_value
        )
        ctx.layout_manager.save()

    return {
        "type": "text",
        "current": get_current_value(ctx),
        "placeholder": "Enter value...",
        "max_length": 50,
        "on_save": on_save,
    }
```

### Editor Callbacks

**on_save**: Called when user confirms edit
- Receives final value
- Persist to config, trigger updates
- Only called if confirmed (not cancelled)

**on_change**: Called on each value change
- Receive intermediate values
- Good for live preview, validation

**on_select**: Dropdown-only
- Called when selection changes
- Good for filtering dependent options

**config_key**: Auto-save to widget config
- Automatically persists to `widget.config[key]`
- Calls `layout_manager.save()` after edit
- Simpler than writing on_save callback

### Editor State Management

The editor integrates with `StatusNavigationState`:

```python
# Set inline edit state
await navigation_state.set_inline_edit_state(
    widget_id="my-widget",
    editor=editor,
    editor_output=editor.render(),
)

# Update editor output during editing
await navigation_state.update_inline_editor_output(new_output)

# Clear state when done
await navigation_state.set_inline_edit_state()
```

This ensures proper state tracking and rendering during inline editing.

## Script Refresh Scheduler

### Background Task Scheduling

The `ScriptWidgetRefreshScheduler` manages background widget refresh:

**File:** `core/io/status/script_refresh_scheduler.py`

**Refresh Modes:**
```python
class RefreshMode(Enum):
    MANUAL = "manual"       # Only explicit refresh
    TIME = "time"           # Time-based polling
    HOOK = "hook"           # Event-based refresh
    SMART = "smart"         # Time + events combined
    ONCE = "once"           # Run once at startup
```

**Usage:**
```python
from core.io.status import ScriptWidgetManager

manager = ScriptWidgetManager()
await manager.initialize_refresh_scheduler(event_bus)

# Scheduler handles:
# - Time-based refresh (5s, 1m, etc.)
# - Event-based refresh (post_user_input, pre_api_request)
# - Smart refresh (time + events)
# - Once-only refresh

await manager.shutdown_refresh_scheduler()
```

**Scheduler Implementation:**
```python
class ScriptWidgetRefreshScheduler:
    def __init__(self, event_bus):
        self._event_bus = event_bus
        self._schedules: Dict[str, WidgetSchedule] = {}
        self._running = False
        self._refresh_task = None

    async def schedule_widget(
        self,
        widget_id: str,
        refresh_callback: Callable,
        refresh_mode: str,
        interval: Optional[str] = None,
        hooks: Optional[List[str]] = None,
    ):
        """Schedule a widget for refresh."""
        schedule = WidgetSchedule(
            widget_id=widget_id,
            callback=refresh_callback,
            mode=RefreshMode(refresh_mode),
            interval=self._parse_interval(interval),
            hooks=hooks or [],
        )
        self._schedules[widget_id] = schedule

        # Register hook listeners if needed
        if schedule.mode in (RefreshMode.HOOK, RefreshMode.SMART):
            for hook in schedule.hooks:
                self._event_bus.subscribe(hook, self._create_hook_wrapper(widget_id))

    async def start(self):
        """Start the background refresh task."""
        if self._running:
            return

        self._running = True
        self._refresh_task = asyncio.create_task(self._refresh_loop())

    async def stop(self):
        """Stop the background refresh task."""
        self._running = False
        if self._refresh_task:
            self._refresh_task.cancel()
            try:
                await self._refresh_task
            except asyncio.CancelledError:
                pass
```

## Event Integration

### Hook Events

Script widgets can refresh on application events:

```python
# Available hooks
HOOK_EVENTS = [
    "post_user_input",      # After user input
    "pre_api_request",      # Before API call
    "post_api_response",    # After API response
    "pre_message_display",  # Before message render
    "profile_changed",      # When profile changes
    "model_changed",        # When model changes
]

# Script widget with hook refresh
# hooks: post_user_input, pre_api_request
```

### Widget Activation Events

```python
# Emit when widget is activated
event_bus.emit(
    EventType.WIDGET_ACTIVATED,
    {
        "widget_id": widget_id,
        "action": action,
        "timestamp": time.time(),
    }
)

# Subscribe to widget activation
event_bus.subscribe(
    EventType.WIDGET_ACTIVATED,
    on_widget_activated
)
```

## Implementation Status

**Phase 1: Basic Navigation** - COMPLETE
- Tab/ESC navigation
- Arrow key navigation
- Mode indicator
- MessageDisplayCoordinator integration

**Phase 2: Widget Activation** - COMPLETE
- Command execution via widgets
- Modal interactions
- Toggle widgets
- Inline editors

**Phase 3: Edit Mode** - COMPLETE
- Add/remove widgets
- Widget picker
- Color customization
- Effect customization
- Row management

**Phase 4: Script Widgets** - COMPLETE
- Script discovery
- Metadata parsing
- Execution engine
- Timeout protection
- Output caching

**Phase 5: Interactive Scripts** - COMPLETE
- Modal handlers (JSON output parsing)
- Action handlers (key-based routing)
- Refresh scheduler (time + hook-based)
- Advanced examples

**Phase 6: Advanced Features** - PLANNED
- Widget groups
- Widget communication
- Remote widgets
- Widget marketplace
- Advanced visualizations
