# UI Plugin Architecture Specification

```
    ╔═══════════════════════════════════════════════════════════════════════════╗
    ║                                                                           ║
    ║   "The app doesn't know what a pixel is. It just HAS INTENTIONS.          ║
    ║    The UI plugin catches those intentions and makes them BEAUTIFUL."      ║
    ║                                                                           ║
    ╚═══════════════════════════════════════════════════════════════════════════╝
```

**Version:** 1.0.0
**Status:** PROPOSED
**Author:** The Architect & The Human
**Date:** 2026-01-26

---

## THE VISION

Today, Kollabor's UI is beautiful but TRAPPED. The solid blocks, the gradients, the
TagBox - they're hardwired. Want a different look? Edit core files. Want ASCII mode
for SSH sessions? Edit core files. Want to render to a web browser? Rewrite everything.

Tomorrow? Tomorrow the UI is a PLUGIN.

```
    +------------------------------------------------------------------+
    |                         KOLLABOR CORE                            |
    |                                                                  |
    |   Conversations    Tools    Permissions    State    Events       |
    |                                                                  |
    |   "I don't know what a box is. I don't know what colors are.    |
    |    I just know I have something to SAY."                         |
    |                                                                  |
    +--------------------------------+---------------------------------+
                                     |
                                     | UI Intentions (hooks)
                                     |
                                     v
    +--------------------------------+---------------------------------+
    |                         UI CONTRACT                              |
    |                                                                  |
    |   UIBox    UIMessage    UIStatus    UIInput    UIPrompt          |
    |                                                                  |
    |   "These are the shapes of communication."                       |
    |                                                                  |
    +----+----------------+----------------+----------------+----------+
         |                |                |                |
         v                v                v                v
    +----------+    +----------+    +----------+    +----------+
    | BLOCKS   |    | ASCII    |    | WEB      |    | VOICE    |
    | PLUGIN   |    | PLUGIN   |    | PLUGIN   |    | PLUGIN   |
    |          |    |          |    |          |    |          |
    | solid()  |    | +----- + |    | <div>    |    | say()    |
    | TagBox   |    | |     | |    | render   |    | speak    |
    | gradient |    | +-----+ |    | </div>   |    | listen   |
    +----------+    +----------+    +----------+    +----------+

    THE SAME BRAIN. INFINITE FACES.
```

---

## DESIGN PRINCIPLES

```
    1. INTENTIONS, NOT INSTRUCTIONS
       ─────────────────────────────
       The core app never says "draw a box with RGB(30,30,30)".
       It says "I have an assistant message to display".
       The UI plugin decides what that LOOKS like.

    2. THE BEAUTIFUL DEFAULT
       ─────────────────────
       The current solid-block theme becomes the flagship plugin.
       It ships as default. It's gorgeous. It proves the system works.
       But it's not special - any plugin could replace it.

    3. PROGRESSIVE COMPLEXITY
       ──────────────────────
       Simple plugins are simple. Want ASCII boxes? 50 lines of code.
       Complex plugins can be complex. Want a web renderer? Go wild.
       The contract supports both.

    4. GRACEFUL DEGRADATION
       ────────────────────
       No UI plugin loaded? Core provides bare-minimum text output.
       Plugin crashes? Fall back to text. Always functional.

    5. ZERO CORE UI KNOWLEDGE
       ──────────────────────
       After migration, kollabor/ has NO imports from design_system.
       No T(). No Box. No TagBox. No colors.
       Pure, beautiful ignorance.
```

---

## THE UI PRIMITIVES

These are the abstract shapes of communication. The vocabulary of intentions.

### 1. UIBox - The Universal Container

Everything is a box. Messages, status, prompts, modals - all boxes.

```
    UIBox Intent:
    ┌─────────────────────────────────────────────────────────────────┐
    │                                                                 │
    │  box_type:     'container' | 'message' | 'status' | 'modal'    │
    │  role:         'user' | 'assistant' | 'system' | 'tool' |      │
    │                'error' | 'warning' | 'info' | 'success'        │
    │  content:      list[str] - the lines to display                │
    │  metadata:     dict - extra context for smart rendering        │
    │  width:        int | None - requested width (hint, not demand) │
    │  position:     'inline' | 'block' | 'overlay'                  │
    │                                                                 │
    └─────────────────────────────────────────────────────────────────┘
```

The plugin receives this and thinks:

```
    ASCII Plugin:        "role=assistant? I'll use +---+ borders"
    Blocks Plugin:       "role=assistant? I'll use TagBox with T().ai_tag"
    Web Plugin:          "role=assistant? I'll emit <div class='assistant'>"
    Voice Plugin:        "role=assistant? I'll say 'The assistant responds:'"
```

### 2. UIMessage - Conversation Flow

A specialized box for conversation messages. Could be implemented as UIBox
under the hood, but gets its own intent for semantic clarity.

```
    UIMessage Intent:
    ┌─────────────────────────────────────────────────────────────────┐
    │                                                                 │
    │  role:         'user' | 'assistant' | 'system' | 'tool'        │
    │  content:      str | list[str] - the message content           │
    │  streaming:    bool - is this a streaming update?              │
    │  thinking:     bool - is this thinking content?                │
    │  tool_name:    str | None - if tool message, which tool        │
    │  tool_status:  'running' | 'complete' | 'error' | None         │
    │  timestamp:    datetime | None                                 │
    │                                                                 │
    └─────────────────────────────────────────────────────────────────┘
```

### 3. UIStatus - The Heartbeat

Status indicators, progress, live information. The pulse of the app.

```
    UIStatus Intent:
    ┌─────────────────────────────────────────────────────────────────┐
    │                                                                 │
    │  area:         'A' | 'B' | 'C' - which status area             │
    │  items:        list[StatusItem] - things to display            │
    │  priority:     int - higher priority items shown first         │
    │                                                                 │
    │  StatusItem:                                                    │
    │    label:      str - the status label                          │
    │    value:      str | None - optional value                     │
    │    state:      'ok' | 'warn' | 'error' | 'info' | 'active'    │
    │    icon:       str | None - optional icon/symbol               │
    │                                                                 │
    └─────────────────────────────────────────────────────────────────┘
```

### 4. UIInput - Where Humans Speak

Input areas, prompts, text entry fields.

```
    UIInput Intent:
    ┌─────────────────────────────────────────────────────────────────┐
    │                                                                 │
    │  mode:         'single' | 'multi' | 'password' | 'command'     │
    │  prompt:       str - the prompt text (e.g., "> ", ">>> ")      │
    │  placeholder:  str | None - ghost text when empty              │
    │  value:        str - current input value                       │
    │  cursor_pos:   int - cursor position in value                  │
    │  focused:      bool - is this input focused?                   │
    │  history:      list[str] | None - command history for nav      │
    │                                                                 │
    └─────────────────────────────────────────────────────────────────┘
```

### 5. UIPrompt - Decision Points

When the app needs human input on a decision. Permissions, confirmations, choices.

```
    UIPrompt Intent:
    ┌─────────────────────────────────────────────────────────────────┐
    │                                                                 │
    │  prompt_type:  'permission' | 'confirm' | 'choice' | 'input'   │
    │  title:        str - what are we asking about                  │
    │  message:      str | list[str] - the full prompt               │
    │  options:      list[PromptOption] - available choices          │
    │  urgency:      'low' | 'normal' | 'high' | 'critical'         │
    │  context:      dict - extra info (tool name, risk level, etc)  │
    │                                                                 │
    │  PromptOption:                                                  │
    │    key:        str - the key to press (e.g., 'a', 'y', 'ESC') │
    │    label:      str - what this option does                     │
    │    style:      'default' | 'primary' | 'danger' | 'muted'     │
    │                                                                 │
    └─────────────────────────────────────────────────────────────────┘
```

### 6. UIModal - Taking Over

Full-screen overlays. Configuration screens, help, special views.

```
    UIModal Intent:
    ┌─────────────────────────────────────────────────────────────────┐
    │                                                                 │
    │  modal_type:   'overlay' | 'fullscreen' | 'drawer'             │
    │  title:        str | None - modal title                        │
    │  content:      ModalContent - what goes inside                 │
    │  closeable:    bool - can user dismiss with ESC?               │
    │  width:        int | 'auto' | 'full'                           │
    │  height:       int | 'auto' | 'full'                           │
    │                                                                 │
    │  ModalContent: (one of)                                         │
    │    - list[str] for simple text                                 │
    │    - list[UIBox] for structured content                        │
    │    - WidgetTree for interactive widgets                        │
    │                                                                 │
    └─────────────────────────────────────────────────────────────────┘
```

### 7. UIProgress - Work in Motion

Progress indicators, spinners, loading states.

```
    UIProgress Intent:
    ┌─────────────────────────────────────────────────────────────────┐
    │                                                                 │
    │  progress_type: 'spinner' | 'bar' | 'dots' | 'pulse'           │
    │  label:         str - what's happening                         │
    │  value:         float | None - 0.0-1.0 for determinate         │
    │  indeterminate: bool - unknown duration?                       │
    │  style:         'inline' | 'block' | 'overlay'                 │
    │                                                                 │
    └─────────────────────────────────────────────────────────────────┘
```

---

## THE HOOK SYSTEM

### Event Types for UI

New event types added to the EventType enum:

```python
class EventType(Enum):
    # ... existing events ...

    # UI Rendering Events (1000-1099 range)
    UI_BOX_RENDER = 1000          # Generic box render request
    UI_MESSAGE_RENDER = 1001      # Conversation message render
    UI_STATUS_RENDER = 1002       # Status area update
    UI_INPUT_RENDER = 1003        # Input area render
    UI_PROMPT_RENDER = 1004       # Decision prompt render
    UI_MODAL_RENDER = 1005        # Modal/overlay render
    UI_PROGRESS_RENDER = 1006     # Progress indicator render

    # UI Lifecycle Events
    UI_INIT = 1010                # UI plugin should initialize
    UI_CLEANUP = 1011             # UI plugin should cleanup
    UI_RESIZE = 1012              # Terminal resized
    UI_FOCUS = 1013               # App gained/lost focus

    # UI Query Events (plugins respond with capabilities)
    UI_QUERY_CAPABILITIES = 1020  # What can this UI plugin do?
    UI_QUERY_SIZE = 1021          # What's the available render area?
```

### Hook Priority for UI

```python
class HookPriority(IntEnum):
    # ... existing priorities ...

    UI_CORE = 100        # Core UI plugin (the default, runs last)
    UI_THEME = 200       # Theme modifications
    UI_TRANSFORM = 300   # Content transformations before render
    UI_INTERCEPT = 400   # Full interception (alternative UI plugins)
```

Higher priority = runs first. So UI_INTERCEPT can completely handle an event
and stop propagation, preventing the core UI from rendering.

### The Render Contract

```python
@dataclass
class UIRenderResult:
    """What a UI hook returns after rendering."""

    handled: bool           # Did this plugin handle the render?
    output: str | None      # The rendered output (if applicable)

    # For streaming/interactive content:
    writer: Callable | None # Function to call for updates

    # Metadata for coordination:
    height: int | None      # How many lines were rendered
    width: int | None       # How wide was the render

    # Control flow:
    stop_propagation: bool  # Don't call lower-priority hooks
```

---

## THE UI PLUGIN CONTRACT

### Base Class

```python
class UIPlugin(BasePlugin):
    """Base class for UI rendering plugins."""

    # Plugin metadata
    plugin_type = "ui"

    @abstractmethod
    async def render_box(self, intent: UIBoxIntent) -> UIRenderResult:
        """Render a generic box."""
        pass

    @abstractmethod
    async def render_message(self, intent: UIMessageIntent) -> UIRenderResult:
        """Render a conversation message."""
        pass

    @abstractmethod
    async def render_status(self, intent: UIStatusIntent) -> UIRenderResult:
        """Render status area."""
        pass

    @abstractmethod
    async def render_input(self, intent: UIInputIntent) -> UIRenderResult:
        """Render input area."""
        pass

    @abstractmethod
    async def render_prompt(self, intent: UIPromptIntent) -> UIRenderResult:
        """Render a decision prompt."""
        pass

    @abstractmethod
    async def render_modal(self, intent: UIModalIntent) -> UIRenderResult:
        """Render a modal/overlay."""
        pass

    @abstractmethod
    async def render_progress(self, intent: UIProgressIntent) -> UIRenderResult:
        """Render a progress indicator."""
        pass

    # Lifecycle
    async def on_init(self, terminal_size: tuple[int, int]) -> None:
        """Called when UI plugin is initialized."""
        pass

    async def on_resize(self, new_size: tuple[int, int]) -> None:
        """Called when terminal is resized."""
        pass

    async def on_cleanup(self) -> None:
        """Called when app is shutting down."""
        pass

    # Capabilities query
    def get_capabilities(self) -> UICapabilities:
        """Return what this plugin can do."""
        return UICapabilities(
            supports_color=True,
            supports_unicode=True,
            supports_mouse=False,
            supports_images=False,
            supports_links=False,
            max_colors=16_777_216,  # True color
        )
```

### Capabilities Declaration

```python
@dataclass
class UICapabilities:
    """What a UI plugin can do."""

    # Color support
    supports_color: bool = True
    max_colors: int = 16          # 16, 256, or 16_777_216

    # Character support
    supports_unicode: bool = True
    supports_emoji: bool = False
    supports_nerd_fonts: bool = False

    # Interactive features
    supports_mouse: bool = False
    supports_clipboard: bool = False
    supports_links: bool = False   # Clickable hyperlinks

    # Rich content
    supports_images: bool = False  # Inline images (iTerm2, Kitty)
    supports_sixel: bool = False   # Sixel graphics

    # Layout
    min_width: int = 40
    min_height: int = 10
```

---

## THE DEFAULT PLUGIN: BLOCKS

The current gorgeous UI becomes `BlocksUIPlugin` - the flagship implementation.

### Structure

```
plugins/
└── blocks_ui/
    ├── __init__.py
    ├── plugin.py           # Main BlocksUIPlugin class
    ├── theme.py            # Theme system (extracted from design_system)
    ├── components.py       # Box, TagBox, solid(), gradient()
    ├── renderers/
    │   ├── __init__.py
    │   ├── box_renderer.py
    │   ├── message_renderer.py
    │   ├── status_renderer.py
    │   ├── input_renderer.py
    │   ├── prompt_renderer.py
    │   ├── modal_renderer.py
    │   └── progress_renderer.py
    └── effects/
        ├── __init__.py
        ├── gradient.py
        ├── shimmer.py
        └── animations.py
```

### Implementation Sketch

```python
# plugins/blocks_ui/plugin.py

from kollabor.plugins import UIPlugin
from kollabor.events import UIBoxIntent, UIMessageIntent, UIRenderResult

from .theme import Theme, set_theme, T
from .components import Box, TagBox, solid, gradient
from .renderers import (
    BoxRenderer, MessageRenderer, StatusRenderer,
    InputRenderer, PromptRenderer, ModalRenderer
)


class BlocksUIPlugin(UIPlugin):
    """
    The flagship Kollabor UI.

    Gorgeous solid-block rendering with gradients, themes,
    and that signature half-block aesthetic.
    """

    name = "blocks_ui"
    version = "1.0.0"
    description = "The beautiful default - solid blocks and gradients"

    def __init__(self):
        super().__init__()
        self.theme = Theme.lime()  # Default theme
        self.box_renderer = BoxRenderer(self.theme)
        self.message_renderer = MessageRenderer(self.theme)
        self.status_renderer = StatusRenderer(self.theme)
        self.input_renderer = InputRenderer(self.theme)
        self.prompt_renderer = PromptRenderer(self.theme)
        self.modal_renderer = ModalRenderer(self.theme)

    async def render_message(self, intent: UIMessageIntent) -> UIRenderResult:
        """Render a conversation message with TagBox styling."""

        # Map role to theme colors
        role_styles = {
            'user': (self.theme.user_tag, '>'),
            'assistant': (self.theme.ai_tag, '*'),
            'system': (self.theme.warning[0], '!'),
            'tool': (self.theme.tool_tag, '#'),
        }

        tag_color, tag_char = role_styles.get(
            intent.role,
            (self.theme.text_dim, '?')
        )

        # Render with TagBox
        output = TagBox.render(
            lines=intent.content if isinstance(intent.content, list)
                  else [intent.content],
            tag_bg=tag_color,
            tag_fg=self.theme.text_dark,
            tag_chars=[f" {tag_char} "],
            content_colors=self.theme.response_bg,
            content_fg=self.theme.text,
            width=intent.metadata.get('width', 80),
        )

        return UIRenderResult(
            handled=True,
            output='\n'.join(output),
            height=len(output),
            stop_propagation=True,
        )

    async def render_prompt(self, intent: UIPromptIntent) -> UIRenderResult:
        """Render permission/confirmation prompts."""

        # Urgency affects colors
        urgency_colors = {
            'low': self.theme.text_dim,
            'normal': self.theme.text,
            'high': self.theme.warning[0],
            'critical': self.theme.error[0],
        }

        # Build option display
        option_lines = []
        for opt in intent.options:
            key_display = f"[{opt.key}]"
            option_lines.append(f"  {key_display} {opt.label}")

        content = [
            intent.title,
            "",
            *([intent.message] if isinstance(intent.message, str)
              else intent.message),
            "",
            *option_lines,
        ]

        output = TagBox.render(
            lines=content,
            tag_bg=urgency_colors[intent.urgency],
            tag_fg=self.theme.text_dark,
            tag_chars=[" ? "],
            content_colors=self.theme.dark,
            content_fg=self.theme.text,
        )

        return UIRenderResult(
            handled=True,
            output='\n'.join(output),
            height=len(output),
            stop_propagation=True,
        )

    # ... other render methods follow the same pattern ...

    def get_capabilities(self) -> UICapabilities:
        return UICapabilities(
            supports_color=True,
            max_colors=16_777_216,  # True color baby
            supports_unicode=True,
            supports_emoji=False,   # We use symbols, not emoji
            supports_nerd_fonts=True,
            supports_mouse=False,
            supports_links=True,    # OSC 8 hyperlinks
        )
```

---

## ALTERNATIVE PLUGINS: EXAMPLES

### ASCII Plugin (Minimal)

For SSH sessions, low-bandwidth, or retro vibes:

```python
# plugins/ascii_ui/plugin.py

class ASCIIUIPlugin(UIPlugin):
    """
    Pure ASCII rendering. Works everywhere.

    +--[ Assistant ]----------------------------------+
    | Hello! I'm running in ASCII mode.              |
    | No colors, no unicode, just pure text.         |
    +------------------------------------------------+
    """

    name = "ascii_ui"

    async def render_message(self, intent: UIMessageIntent) -> UIRenderResult:
        role_labels = {
            'user': 'You',
            'assistant': 'Assistant',
            'system': 'System',
            'tool': f'Tool: {intent.tool_name or "unknown"}',
        }

        label = role_labels.get(intent.role, intent.role)
        width = intent.metadata.get('width', 70)

        # Simple ASCII box
        lines = []
        border = '+' + '-' * (width - 2) + '+'
        header = f'+--[ {label} ]' + '-' * (width - len(label) - 7) + '+'

        lines.append(header)

        content = intent.content if isinstance(intent.content, list) else [intent.content]
        for line in content:
            # Word wrap
            while len(line) > width - 4:
                lines.append(f'| {line[:width-4]} |')
                line = line[width-4:]
            lines.append(f'| {line:<{width-4}} |')

        lines.append(border)

        return UIRenderResult(
            handled=True,
            output='\n'.join(lines),
            height=len(lines),
            stop_propagation=True,
        )

    def get_capabilities(self) -> UICapabilities:
        return UICapabilities(
            supports_color=False,
            max_colors=0,
            supports_unicode=False,
            supports_emoji=False,
        )
```

### JSON Plugin (Automation)

For piping output to other tools:

```python
# plugins/json_ui/plugin.py

import json

class JSONUIPlugin(UIPlugin):
    """
    Outputs structured JSON. Perfect for automation.

    {"type": "message", "role": "assistant", "content": "Hello!"}
    """

    name = "json_ui"

    async def render_message(self, intent: UIMessageIntent) -> UIRenderResult:
        output = json.dumps({
            'type': 'message',
            'role': intent.role,
            'content': intent.content,
            'timestamp': intent.timestamp.isoformat() if intent.timestamp else None,
            'tool': intent.tool_name,
            'tool_status': intent.tool_status,
        })

        return UIRenderResult(
            handled=True,
            output=output,
            stop_propagation=True,
        )
```

### Web Plugin (Ambitious)

Renders to HTML/WebSocket for browser UI:

```python
# plugins/web_ui/plugin.py

class WebUIPlugin(UIPlugin):
    """
    Renders to a web interface via WebSocket.

    The terminal becomes a thin client - all rendering
    happens in the browser.
    """

    name = "web_ui"

    def __init__(self, websocket_port: int = 8765):
        super().__init__()
        self.port = websocket_port
        self.connections = set()

    async def on_init(self, terminal_size: tuple[int, int]) -> None:
        # Start WebSocket server
        import websockets
        self.server = await websockets.serve(
            self._handle_connection,
            "localhost",
            self.port
        )
        print(f"Web UI available at http://localhost:{self.port}")

    async def render_message(self, intent: UIMessageIntent) -> UIRenderResult:
        # Send to all connected browsers
        message = {
            'type': 'message',
            'role': intent.role,
            'content': intent.content,
            'streaming': intent.streaming,
        }

        await self._broadcast(message)

        return UIRenderResult(
            handled=True,
            output="[Rendered to web UI]",
            stop_propagation=True,
        )

    async def _broadcast(self, data: dict):
        import json
        message = json.dumps(data)
        for ws in self.connections:
            await ws.send(message)
```

---

## THE INTENT SYSTEM

### Intent Classes

```python
# kollabor/ui/intents.py

from dataclasses import dataclass, field
from typing import Any
from datetime import datetime


@dataclass
class UIIntent:
    """Base class for all UI intents."""
    metadata: dict = field(default_factory=dict)


@dataclass
class UIBoxIntent(UIIntent):
    """Request to render a generic box."""
    box_type: str = 'container'  # container, message, status, modal
    role: str = 'info'           # user, assistant, system, tool, error, warning, info, success
    content: list[str] = field(default_factory=list)
    width: int | None = None
    position: str = 'block'      # inline, block, overlay


@dataclass
class UIMessageIntent(UIIntent):
    """Request to render a conversation message."""
    role: str = 'assistant'      # user, assistant, system, tool
    content: str | list[str] = ''
    streaming: bool = False
    thinking: bool = False
    tool_name: str | None = None
    tool_status: str | None = None  # running, complete, error
    timestamp: datetime | None = None


@dataclass
class UIStatusIntent(UIIntent):
    """Request to render status information."""
    area: str = 'B'              # A, B, C
    items: list['StatusItem'] = field(default_factory=list)
    priority: int = 0


@dataclass
class StatusItem:
    """A single status item."""
    label: str
    value: str | None = None
    state: str = 'info'          # ok, warn, error, info, active
    icon: str | None = None


@dataclass
class UIInputIntent(UIIntent):
    """Request to render an input area."""
    mode: str = 'single'         # single, multi, password, command
    prompt: str = '> '
    placeholder: str | None = None
    value: str = ''
    cursor_pos: int = 0
    focused: bool = True
    history: list[str] | None = None


@dataclass
class PromptOption:
    """An option in a prompt."""
    key: str                     # The key to press
    label: str                   # What this option does
    style: str = 'default'       # default, primary, danger, muted


@dataclass
class UIPromptIntent(UIIntent):
    """Request to render a decision prompt."""
    prompt_type: str = 'confirm'  # permission, confirm, choice, input
    title: str = ''
    message: str | list[str] = ''
    options: list[PromptOption] = field(default_factory=list)
    urgency: str = 'normal'      # low, normal, high, critical
    context: dict = field(default_factory=dict)


@dataclass
class UIModalIntent(UIIntent):
    """Request to render a modal/overlay."""
    modal_type: str = 'overlay'  # overlay, fullscreen, drawer
    title: str | None = None
    content: Any = None          # list[str], list[UIBoxIntent], WidgetTree
    closeable: bool = True
    width: int | str = 'auto'    # int, 'auto', 'full'
    height: int | str = 'auto'


@dataclass
class UIProgressIntent(UIIntent):
    """Request to render a progress indicator."""
    progress_type: str = 'spinner'  # spinner, bar, dots, pulse
    label: str = ''
    value: float | None = None   # 0.0-1.0 for determinate
    indeterminate: bool = True
    style: str = 'inline'        # inline, block, overlay
```

### The UIBridge: How Core Talks to UI

```python
# kollabor/ui/bridge.py

from kollabor.events import EventBus, EventType
from .intents import *


class UIBridge:
    """
    The bridge between core application and UI plugins.

    Core code calls these methods. They emit events.
    UI plugins catch the events and render.

    The core never knows HOW things are rendered.
    It just expresses WHAT it wants to show.
    """

    def __init__(self, event_bus: EventBus):
        self.event_bus = event_bus
        self._fallback_enabled = True

    async def message(
        self,
        role: str,
        content: str | list[str],
        **kwargs
    ) -> UIRenderResult:
        """Display a conversation message."""
        intent = UIMessageIntent(
            role=role,
            content=content,
            **kwargs
        )

        result = await self.event_bus.emit(
            EventType.UI_MESSAGE_RENDER,
            {'intent': intent}
        )

        # Fallback if no UI plugin handled it
        if not result.get('handled') and self._fallback_enabled:
            return self._fallback_message(intent)

        return result

    async def box(
        self,
        content: list[str],
        role: str = 'info',
        **kwargs
    ) -> UIRenderResult:
        """Display a generic box."""
        intent = UIBoxIntent(
            content=content,
            role=role,
            **kwargs
        )

        return await self.event_bus.emit(
            EventType.UI_BOX_RENDER,
            {'intent': intent}
        )

    async def prompt(
        self,
        title: str,
        message: str | list[str],
        options: list[PromptOption],
        **kwargs
    ) -> UIRenderResult:
        """Display a decision prompt."""
        intent = UIPromptIntent(
            title=title,
            message=message,
            options=options,
            **kwargs
        )

        return await self.event_bus.emit(
            EventType.UI_PROMPT_RENDER,
            {'intent': intent}
        )

    async def status(
        self,
        items: list[StatusItem],
        area: str = 'B',
        **kwargs
    ) -> UIRenderResult:
        """Update status area."""
        intent = UIStatusIntent(
            items=items,
            area=area,
            **kwargs
        )

        return await self.event_bus.emit(
            EventType.UI_STATUS_RENDER,
            {'intent': intent}
        )

    async def progress(
        self,
        label: str,
        value: float | None = None,
        **kwargs
    ) -> UIRenderResult:
        """Show progress indicator."""
        intent = UIProgressIntent(
            label=label,
            value=value,
            indeterminate=value is None,
            **kwargs
        )

        return await self.event_bus.emit(
            EventType.UI_PROGRESS_RENDER,
            {'intent': intent}
        )

    async def modal(
        self,
        content: Any,
        title: str | None = None,
        **kwargs
    ) -> UIRenderResult:
        """Display a modal/overlay."""
        intent = UIModalIntent(
            title=title,
            content=content,
            **kwargs
        )

        return await self.event_bus.emit(
            EventType.UI_MODAL_RENDER,
            {'intent': intent}
        )

    async def input(
        self,
        prompt: str = '> ',
        **kwargs
    ) -> UIRenderResult:
        """Render input area."""
        intent = UIInputIntent(
            prompt=prompt,
            **kwargs
        )

        return await self.event_bus.emit(
            EventType.UI_INPUT_RENDER,
            {'intent': intent}
        )

    # ─────────────────────────────────────────────────────────
    # FALLBACKS: When no UI plugin is loaded
    # ─────────────────────────────────────────────────────────

    def _fallback_message(self, intent: UIMessageIntent) -> UIRenderResult:
        """Bare minimum text output."""
        role_prefix = {
            'user': '[You] ',
            'assistant': '[AI] ',
            'system': '[System] ',
            'tool': f'[Tool: {intent.tool_name}] ',
        }

        prefix = role_prefix.get(intent.role, f'[{intent.role}] ')
        content = intent.content if isinstance(intent.content, str) else '\n'.join(intent.content)

        output = f"{prefix}{content}"
        print(output)

        return UIRenderResult(
            handled=True,
            output=output,
            height=output.count('\n') + 1,
        )
```

---

## MIGRATION PATH

This is the fun part. How do we get from here to there without breaking everything?

### Phase 1: Foundation (Week 1)

```
    Create the abstractions WITHOUT changing existing code.

    [x] Define all Intent dataclasses
    [x] Define UIPlugin base class
    [x] Define UIRenderResult
    [x] Define UICapabilities
    [x] Add new EventTypes to enum
    [x] Create UIBridge class
    [x] Create kollabor/ui/intents.py
    [x] Create kollabor/ui/bridge.py
    [x] Create kollabor/ui/plugin.py

    Result: The vocabulary exists. Nothing uses it yet.
```

### Phase 2: The Flagship Plugin (Week 2)

```
    Extract current UI into BlocksUIPlugin.

    [ ] Create plugins/blocks_ui/ directory structure
    [ ] Move kollabor/ui/design_system/ → plugins/blocks_ui/
    [ ] Adapt components to UIPlugin interface
    [ ] Implement all render methods
    [ ] Register hooks with event bus
    [ ] Test: plugin produces same output as before

    Result: Current UI works as a plugin.
            But core still calls it directly.
```

### Phase 3: The Great Decoupling (Week 3)

```
    Make core use UIBridge instead of direct calls.

    File-by-file migration:

    [ ] kollabor/io/message_renderer.py
        - Find all Box/TagBox/solid calls
        - Replace with ui_bridge.message() / ui_bridge.box()

    [ ] kollabor/io/status/layout_renderer.py
        - Replace status rendering with ui_bridge.status()

    [ ] kollabor/io/status/permission_status_view.py
        - Replace permission prompts with ui_bridge.prompt()

    [ ] kollabor/io/layout.py
        - Replace progress/thinking with ui_bridge.progress()

    [ ] plugins/modern_input/renderer.py
        - Replace input rendering with ui_bridge.input()

    [ ] All modal code
        - Replace with ui_bridge.modal()

    Migration pattern for each file:

    1. Add UIBridge import
    2. Find all design_system imports
    3. Find all T(), Box, TagBox, solid() calls
    4. Replace with appropriate ui_bridge.X() call
    5. Remove design_system imports
    6. Test file still works
    7. Commit

    Result: Core has NO design_system imports.
            All UI goes through UIBridge.
            BlocksUIPlugin handles all rendering.
```

### Phase 4: Plugin Selection (Week 4)

```
    Let users choose their UI plugin.

    [ ] Add ui_plugin config option
    [ ] Add --ui CLI flag
    [ ] Implement plugin discovery for UI plugins
    [ ] Add fallback behavior when no UI plugin
    [ ] Create ASCII plugin as proof of concept
    [ ] Test switching between plugins

    Result: Users can choose their UI.
            Different plugins, different looks.
```

### Phase 5: Polish (Week 5)

```
    Edge cases, documentation, joy.

    [ ] Handle plugin crashes gracefully
    [ ] Add UI plugin hot-reloading (change theme without restart)
    [ ] Write plugin development guide
    [ ] Create plugin template
    [ ] Add more example plugins (minimal, retro, etc.)
    [ ] Performance optimization

    Result: Production ready.
            Developers can create UI plugins.
            Users can customize their experience.
```

---

## MIGRATION EXAMPLE: BEFORE/AFTER

### Before: message_renderer.py

```python
# kollabor/io/message_renderer.py (BEFORE)

from kollabor.ui.design_system import T, S, Box, TagBox, solid, solid_fg

class MessageRenderer:
    def render_assistant_message(self, content: str, width: int) -> list[str]:
        return TagBox.render(
            lines=content.split('\n'),
            tag_bg=T().ai_tag,
            tag_fg=T().text_dark,
            tag_chars=[" * "],
            content_colors=T().response_bg,
            content_fg=T().text,
            width=width,
        )

    def render_error(self, error: str, width: int) -> list[str]:
        return Box.render(
            lines=[f"ERROR: {error}"],
            colors=T().error,
            fg=T().text,
            width=width,
        )
```

### After: message_renderer.py

```python
# kollabor/io/message_renderer.py (AFTER)

from kollabor.ui.bridge import UIBridge
from kollabor.ui.intents import UIMessageIntent, UIBoxIntent

class MessageRenderer:
    def __init__(self, ui_bridge: UIBridge):
        self.ui = ui_bridge

    async def render_assistant_message(self, content: str) -> str:
        result = await self.ui.message(
            role='assistant',
            content=content,
        )
        return result.output

    async def render_error(self, error: str) -> str:
        result = await self.ui.box(
            content=[f"ERROR: {error}"],
            role='error',
        )
        return result.output
```

The MessageRenderer no longer knows:
- What colors exist
- What Box or TagBox are
- How rendering happens

It just says "show an assistant message" and trusts the UI plugin.

---

## FILE CHANGES SUMMARY

### New Files to Create

```
kollabor/ui/
├── __init__.py              # Exports UIBridge, intents
├── intents.py               # All UI intent dataclasses
├── bridge.py                # UIBridge class
├── plugin.py                # UIPlugin base class
└── capabilities.py          # UICapabilities dataclass

plugins/blocks_ui/
├── __init__.py
├── plugin.py                # BlocksUIPlugin
├── theme.py                 # (moved from design_system)
├── components.py            # (moved from design_system)
├── color_mode.py            # (moved from design_system)
├── gradient.py              # (moved from design_system)
├── border_style.py          # (moved from design_system)
└── renderers/
    ├── __init__.py
    ├── box.py
    ├── message.py
    ├── status.py
    ├── input.py
    ├── prompt.py
    ├── modal.py
    └── progress.py

plugins/ascii_ui/
├── __init__.py
└── plugin.py                # ASCIIUIPlugin (proof of concept)
```

### Files to Modify

```
kollabor/events/models.py        # Add UI_* event types
kollabor/events/bus.py           # Add UI hook priority handling
kollabor/application.py          # Initialize UIBridge, inject into components
kollabor/io/message_renderer.py  # Use UIBridge instead of design_system
kollabor/io/layout.py            # Use UIBridge for progress/thinking
kollabor/io/status/              # All files - use UIBridge for status
kollabor/llm/permissions/        # Use UIBridge for permission prompts
plugins/modern_input/        # Use UIBridge for input rendering
```

### Files to Eventually Remove

```
kollabor/ui/design_system/       # Entire directory moves to blocks_ui plugin
```

---

## SUCCESS METRICS

How do we know we've won?

```
    [ ] Zero design_system imports in kollabor/
        └── grep -r "design_system" kollabor/  →  no results

    [ ] Zero T(), Box, TagBox calls in kollabor/
        └── grep -r "from.*design_system" kollabor/  →  no results

    [ ] ASCII plugin produces usable output
        └── Run app with --ui ascii, complete a conversation

    [ ] Theme switching works at runtime
        └── /theme ocean  changes colors without restart

    [ ] Plugin crash doesn't crash app
        └── Intentionally break UI plugin, app falls back to text

    [ ] New plugin takes < 100 lines for basic functionality
        └── Create "minimal" plugin, count lines

    [ ] Documentation exists for plugin developers
        └── docs/plugins/ui-plugin-development.md exists
```

---

## THE DREAM

```
    +------------------------------------------------------------------+
    |                                                                  |
    |  KOLLABOR: A mind that can wear any face.                       |
    |                                                                  |
    |  The same powerful conversation engine, the same tools,         |
    |  the same intelligence - rendered however you want.             |
    |                                                                  |
    |  Terminal with gorgeous gradients? ✓                            |
    |  Pure ASCII over SSH? ✓                                         |
    |  Web interface in a browser? ✓                                  |
    |  Voice assistant? ✓                                             |
    |  JSON for automation? ✓                                         |
    |  Something we haven't imagined yet? ✓                           |
    |                                                                  |
    |  The UI is no longer a feature.                                 |
    |  The UI is a plugin.                                            |
    |  And that changes EVERYTHING.                                   |
    |                                                                  |
    +------------------------------------------------------------------+
```

---

## APPENDIX A: FULL EVENT TYPE ENUM

```python
class EventType(Enum):
    """All event types in the system."""

    # Existing events (unchanged)
    PRE_USER_INPUT = 100
    POST_USER_INPUT = 101
    PRE_API_REQUEST = 200
    POST_API_RESPONSE = 201
    # ... etc ...

    # NEW: UI Rendering Events
    UI_BOX_RENDER = 1000
    UI_MESSAGE_RENDER = 1001
    UI_STATUS_RENDER = 1002
    UI_INPUT_RENDER = 1003
    UI_PROMPT_RENDER = 1004
    UI_MODAL_RENDER = 1005
    UI_PROGRESS_RENDER = 1006

    # NEW: UI Lifecycle Events
    UI_INIT = 1010
    UI_CLEANUP = 1011
    UI_RESIZE = 1012
    UI_FOCUS = 1013
    UI_THEME_CHANGE = 1014

    # NEW: UI Query Events
    UI_QUERY_CAPABILITIES = 1020
    UI_QUERY_SIZE = 1021
    UI_QUERY_THEME = 1022
```

---

## APPENDIX B: QUICK REFERENCE

### For Core Developers

```python
# DON'T DO THIS ANYMORE:
from kollabor.ui.design_system import T, Box, TagBox
output = TagBox.render(lines, tag_bg=T().ai_tag, ...)

# DO THIS INSTEAD:
from kollabor.ui.bridge import get_ui_bridge
ui = get_ui_bridge()
result = await ui.message(role='assistant', content=lines)
```

### For Plugin Developers

```python
# Create a new UI plugin:
from kollabor.ui.plugin import UIPlugin

class MyUIPlugin(UIPlugin):
    name = "my_ui"

    async def render_message(self, intent):
        # Your rendering logic here
        output = f"[{intent.role}] {intent.content}"
        return UIRenderResult(handled=True, output=output)
```

### For Users

```bash
# Use default (blocks) UI
kollab

# Use ASCII UI
kollab --ui ascii

# Use minimal UI
kollab --ui minimal

# Set default in config
kollab config set ui.plugin "ascii"
```

---

*This specification was crafted with love, ambition, and an unreasonable belief
that terminal applications can be both beautiful AND flexible.*

*Let's build it.*
