# Multi-Line Input Specification

## Overview

Enable multi-line text input in the Kollabor CLI input box. Users press Shift+Enter to insert newlines. The input box displays up to 3 visible lines with vertical scrolling when content exceeds the visible area.

## User Experience

### Input Behavior
- **Shift+Enter**: Insert newline at cursor position
- **Enter**: Submit entire multi-line content as single message
- **Backspace**: Delete character before cursor (including newlines)
- **Delete**: Delete character after cursor (including newlines)

### Navigation
- **Arrow Left/Right**: Move cursor horizontally, wrapping at line boundaries
- **Arrow Up**: Move cursor to previous line (same column, or end if line is shorter)
- **Arrow Down**: Move cursor to next line (same column, or end if line is shorter)
- **Home**: Move to start of current line
- **End**: Move to end of current line
- **Ctrl+A**: Move to start of entire input
- **Ctrl+E**: Move to end of entire input

### Display
- Maximum 3 visible lines at a time
- First line shows prompt: `⠠⠵`
- Continuation lines show: `...`
- Scroll indicators when content exceeds visible area:
  - `^` at top when scrolled down
  - `v` at bottom when more content below

## Data Model

### BufferManager Additions

```python
class BufferManager:
    # Existing
    _buffer: str              # Raw content with \n characters
    _cursor_pos: int          # Absolute position in buffer

    # New properties
    MAX_VISIBLE_LINES = 3
    _visible_start_line: int  # First visible line index (0-indexed)

    @property
    def lines(self) -> List[str]:
        """Split buffer into lines."""
        return self._buffer.split("\n")

    @property
    def total_lines(self) -> int:
        """Total number of lines."""
        return len(self.lines)

    @property
    def cursor_line(self) -> int:
        """Which line the cursor is on (0-indexed)."""
        # Calculate from _cursor_pos

    @property
    def cursor_col(self) -> int:
        """Column position within current line."""
        # Calculate from _cursor_pos

    @property
    def visible_end_line(self) -> int:
        """Last visible line index (exclusive)."""
        return min(self._visible_start_line + self.MAX_VISIBLE_LINES, self.total_lines)

    @property
    def visible_lines(self) -> List[str]:
        """Lines currently in view."""
        return self.lines[self._visible_start_line:self.visible_end_line]

    @property
    def is_multiline(self) -> bool:
        """True if buffer contains newlines."""
        return "\n" in self._buffer
```

### New Methods

```python
def get_cursor_line_col(self) -> Tuple[int, int]:
    """Calculate cursor line and column from absolute position.

    Returns:
        Tuple of (line_index, column_index)
    """
    lines = self.lines
    chars_counted = 0
    for i, line in enumerate(lines):
        line_end = chars_counted + len(line)
        if self._cursor_pos <= line_end:
            return (i, self._cursor_pos - chars_counted)
        chars_counted = line_end + 1  # +1 for newline
    # Cursor at very end
    return (len(lines) - 1, len(lines[-1]))

def move_cursor_vertical(self, direction: str) -> bool:
    """Move cursor up or down one line.

    Args:
        direction: "up" or "down"

    Returns:
        True if cursor moved, False if at boundary
    """
    line, col = self.get_cursor_line_col()
    lines = self.lines

    if direction == "up" and line > 0:
        target_line = line - 1
        target_col = min(col, len(lines[target_line]))
        self._set_cursor_to_line_col(target_line, target_col)
        return True
    elif direction == "down" and line < len(lines) - 1:
        target_line = line + 1
        target_col = min(col, len(lines[target_line]))
        self._set_cursor_to_line_col(target_line, target_col)
        return True
    return False

def _set_cursor_to_line_col(self, line: int, col: int) -> None:
    """Set cursor position from line and column.

    Args:
        line: Target line index
        col: Target column index
    """
    lines = self.lines
    pos = sum(len(lines[i]) + 1 for i in range(line))  # +1 for each newline
    pos += col
    self._cursor_pos = pos
    self._ensure_cursor_visible()

def _ensure_cursor_visible(self) -> None:
    """Adjust visible_start_line to keep cursor in view."""
    line, _ = self.get_cursor_line_col()

    # Scroll up if cursor above visible area
    if line < self._visible_start_line:
        self._visible_start_line = line

    # Scroll down if cursor below visible area
    if line >= self._visible_start_line + self.MAX_VISIBLE_LINES:
        self._visible_start_line = line - self.MAX_VISIBLE_LINES + 1

def get_display_info_multiline(self) -> Dict[str, Any]:
    """Get display information for multi-line rendering.

    Returns:
        Dict with keys:
            - visible_lines: List[str] - lines to display
            - cursor_visible_line: int - cursor line within visible area
            - cursor_col: int - cursor column
            - has_more_above: bool - scroll indicator
            - has_more_below: bool - scroll indicator
    """
    line, col = self.get_cursor_line_col()
    return {
        "visible_lines": self.visible_lines,
        "cursor_visible_line": line - self._visible_start_line,
        "cursor_col": col,
        "has_more_above": self._visible_start_line > 0,
        "has_more_below": self.visible_end_line < self.total_lines,
    }
```

## Key Press Handler Changes

### Arrow Up/Down Logic

```python
# In _handle_key_press()

elif key_press.name == "ArrowUp":
    if self.buffer_manager.is_multiline:
        # Multi-line mode: move cursor up
        moved = self.buffer_manager.move_cursor_vertical("up")
        if moved:
            await self.display_controller.update_display(force_render=True)
    else:
        # Single-line mode: history navigation
        self.buffer_manager.navigate_history("up")
        await self.display_controller.update_display(force_render=True)

elif key_press.name == "ArrowDown":
    if self.buffer_manager.is_multiline:
        # Multi-line mode: move cursor down
        moved = self.buffer_manager.move_cursor_vertical("down")
        if moved:
            await self.display_controller.update_display(force_render=True)
    else:
        # Single-line mode: history navigation
        self.buffer_manager.navigate_history("down")
        await self.display_controller.update_display(force_render=True)
```

### Arrow Left/Right Wrapping

```python
elif key_press.name == "ArrowLeft":
    if self.buffer_manager.cursor_position == 0:
        pass  # At start, can't move
    else:
        # Check if at start of line (not first line)
        line, col = self.buffer_manager.get_cursor_line_col()
        if col == 0 and line > 0:
            # Wrap to end of previous line
            prev_line_len = len(self.buffer_manager.lines[line - 1])
            self.buffer_manager._set_cursor_to_line_col(line - 1, prev_line_len)
        else:
            self.buffer_manager.move_cursor("left")
        await self.display_controller.update_display(force_render=True)

elif key_press.name == "ArrowRight":
    line, col = self.buffer_manager.get_cursor_line_col()
    lines = self.buffer_manager.lines
    if col >= len(lines[line]) and line < len(lines) - 1:
        # Wrap to start of next line
        self.buffer_manager._set_cursor_to_line_col(line + 1, 0)
    else:
        self.buffer_manager.move_cursor("right")
    await self.display_controller.update_display(force_render=True)
```

## Terminal Renderer Changes

### _render_input_modern()

```python
def _render_input_modern(self, lines: List[str], position: str = "first") -> None:
    # Get terminal width
    terminal_width, _ = self.terminal_state.get_size()
    width = min(max(40, terminal_width), 76)

    # Get multi-line display info from buffer
    display_info = self.buffer_manager.get_display_info_multiline() \
        if hasattr(self, 'buffer_manager') else None

    if display_info and len(display_info["visible_lines"]) > 1:
        # Multi-line rendering
        self._render_multiline_input(lines, width, display_info, position)
    else:
        # Single-line rendering (existing code)
        self._render_singleline_input(lines, width, position)

def _render_multiline_input(
    self,
    lines: List[str],
    width: int,
    display_info: Dict[str, Any],
    position: str
) -> None:
    """Render multi-line input box."""
    prompt = "⠠⠵"
    continuation = "..."

    visible_lines = display_info["visible_lines"]
    cursor_line = display_info["cursor_visible_line"]
    cursor_col = display_info["cursor_col"]
    has_above = display_info["has_more_above"]
    has_below = display_info["has_more_below"]

    show_top = position in ("only", "first")
    show_bottom = position in ("only", "last")

    # Top border with scroll indicator
    if show_top:
        border = "▄" * width
        if has_above:
            border = "▄" * (width - 2) + " ^"
        lines.append(solid_fg(border, T().dark[0]))

    # Render each visible line
    for i, line_text in enumerate(visible_lines):
        line_prompt = prompt if i == 0 and not has_above else continuation

        # Add cursor if on this line
        if i == cursor_line and not self.thinking_active:
            col = min(cursor_col, len(line_text))
            text = line_text[:col] + C['cursor'] + line_text[col:]
        else:
            text = line_text

        content = f" {line_prompt} {text}".ljust(width)[:width]
        lines.append(gradient(content, T().input_bg, T().text, width))

    # Bottom border with scroll indicator
    if show_bottom:
        border = "▀" * width
        if has_below:
            border = "▀" * (width - 2) + " v"
        lines.append(solid_fg(border, T().dark[0]))
```

## Files to Modify

| File | Changes |
|------|---------|
| `core/io/buffer_manager.py` | Add multi-line properties and methods |
| `core/io/input/key_press_handler.py` | Update arrow key handling for multi-line |
| `core/io/terminal_renderer.py` | Add multi-line rendering with scroll indicators |
| `core/io/input/display_controller.py` | Pass buffer_manager to renderer if needed |

## Testing Checklist

- [ ] Shift+Enter inserts newline
- [ ] Enter submits all lines as single message
- [ ] Arrow up/down navigate lines in multi-line mode
- [ ] Arrow up/down do history in single-line mode
- [ ] Arrow left at line start wraps to previous line
- [ ] Arrow right at line end wraps to next line
- [ ] Scroll indicators appear when > 3 lines
- [ ] Cursor stays visible when scrolling
- [ ] Backspace deletes newlines correctly
- [ ] Home/End work within current line
- [ ] Ctrl+A/E work for entire input

## Future Enhancements

- Configurable MAX_VISIBLE_LINES
- Page Up/Page Down for faster scrolling
- Ctrl+Up/Down for paragraph navigation
- Line numbers in continuation prompt
- Syntax highlighting for code blocks
