mutiny-diff 0.1.22

TUI git diff viewer with worktree management
# Spec: Command Palette (`Ctrl+P`)

**Priority**: P1 (High Impact)
**Status**: Ready for implementation
**Estimated effort**: Medium (5-7 files changed)
**Roadmap item**: New — Command Palette for action discoverability

## Problem

mdiff has grown to 50+ distinct actions (see `src/action.rs`) across navigation, annotations, search, git operations, agent management, review state, and settings. The which-key overlay (`?`) shows keybindings for the current context, but users must:

1. Know which panel they're in to see relevant bindings
2. Scroll through a static list to find the action they want
3. Memorize keybindings for infrequent actions (export feedback, toggle checklist, open settings)

As the feature set grows, discoverability becomes a bottleneck. New users don't know what's available, and experienced users forget bindings for rarely-used features.

## Competitive Reference

- **VS Code** (`Ctrl+Shift+P`): The gold standard — fuzzy-searchable list of all commands with keybinding hints
- **Fresh editor** (`Ctrl+P`): New Rust terminal editor with command palette as a core UX pattern
- **Neovim Telescope**: Fuzzy finder for commands, files, and more — keyboard-driven with preview
- **lazygit**: No command palette, but context menus serve a similar purpose

## Proposed Solution

### New State: `CommandPaletteState`

Add `src/state/command_palette_state.rs`:

```rust
use nucleo::{Matcher, Utf32Str, pattern::{Pattern, CaseMatching, Normalization}};

#[derive(Debug, Clone)]
pub struct PaletteEntry {
    pub label: String,           // Human-readable name: "Toggle Unified/Split View"
    pub keybinding: Option<String>, // Display hint: "Tab"
    pub action: crate::action::Action, // The action to dispatch
    pub category: PaletteCategory,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PaletteCategory {
    Navigation,
    DiffView,
    Annotations,
    Search,
    Git,
    Agent,
    Review,
    Settings,
}

impl PaletteCategory {
    pub fn label(&self) -> &'static str {
        match self {
            Self::Navigation => "Navigation",
            Self::DiffView => "Diff View",
            Self::Annotations => "Annotations",
            Self::Search => "Search",
            Self::Git => "Git",
            Self::Agent => "Agent",
            Self::Review => "Review",
            Self::Settings => "Settings",
        }
    }
}

#[derive(Debug, Default)]
pub struct CommandPaletteState {
    pub open: bool,
    pub query: String,
    pub cursor_pos: usize,
    pub selected_index: usize,
    pub filtered_entries: Vec<usize>, // indices into the master entry list
}
```

### Master Entry Registry

Create a function `build_palette_entries() -> Vec<PaletteEntry>` that registers ALL available actions with human-readable labels and keybinding hints. This should be called once at startup and cached. Example entries:

```rust
vec![
    PaletteEntry {
        label: "Jump to Next Hunk".into(),
        keybinding: Some("]".into()),
        action: Action::JumpNextHunk,
        category: PaletteCategory::DiffView,
    },
    PaletteEntry {
        label: "Toggle Unified/Split View".into(),
        keybinding: Some("Tab".into()),
        action: Action::ToggleViewMode,
        category: PaletteCategory::DiffView,
    },
    PaletteEntry {
        label: "Open Comment Editor".into(),
        keybinding: Some("c".into()),
        action: Action::OpenCommentEditor,
        category: PaletteCategory::Annotations,
    },
    PaletteEntry {
        label: "Export Feedback".into(),
        keybinding: Some("Ctrl+E".into()),
        action: Action::ExportFeedback,
        category: PaletteCategory::Review,
    },
    PaletteEntry {
        label: "Toggle Checklist Panel".into(),
        keybinding: Some("Ctrl+L".into()),
        action: Action::ToggleChecklist,
        category: PaletteCategory::Review,
    },
    PaletteEntry {
        label: "Open Settings".into(),
        keybinding: Some(",".into()),
        action: Action::OpenSettings,
        category: PaletteCategory::Settings,
    },
    // ... register all ~40 user-facing actions
]
```

**Important**: Only register user-facing actions. Internal actions like `Tick`, `Resize`, `CommentChar`, `SearchChar`, etc. should NOT appear in the palette.

### New Actions

Add to `src/action.rs`:

```rust
// Command palette
OpenCommandPalette,
CommandPaletteChar(char),
CommandPaletteBackspace,
CommandPaletteUp,
CommandPaletteDown,
CommandPaletteConfirm,
CancelCommandPalette,
```

### Key Mapping in `event.rs`

Add a new priority level (Priority 1.5 — after PTY focus but before other modals):

```rust
// Priority 1.5: Command palette (highest priority when open)
if ctx.command_palette_open {
    if key.modifiers.contains(KeyModifiers::CONTROL) {
        return match key.code {
            KeyCode::Char('a') => Some(Action::TextCursorHome),
            KeyCode::Char('e') => Some(Action::TextCursorEnd),
            KeyCode::Char('w') => Some(Action::TextDeleteWord),
            KeyCode::Char('n') => Some(Action::CommandPaletteDown),
            KeyCode::Char('p') => Some(Action::CommandPaletteUp),
            _ => None,
        };
    }
    return match key.code {
        KeyCode::Esc => Some(Action::CancelCommandPalette),
        KeyCode::Enter => Some(Action::CommandPaletteConfirm),
        KeyCode::Up => Some(Action::CommandPaletteUp),
        KeyCode::Down => Some(Action::CommandPaletteDown),
        KeyCode::Backspace => Some(Action::CommandPaletteBackspace),
        KeyCode::Left => Some(Action::TextCursorLeft),
        KeyCode::Right => Some(Action::TextCursorRight),
        KeyCode::Char(c) => Some(Action::CommandPaletteChar(c)),
        _ => None,
    };
}
```

Add the trigger in Priority 4 (global bindings):

```rust
KeyCode::Char('p') if key.modifiers.contains(KeyModifiers::CONTROL) => {
    return Some(Action::OpenCommandPalette)
}
```

### Fuzzy Matching

Use the `nucleo` crate (already in `Cargo.toml`) for fuzzy matching. When the user types in the palette:

1. On each `CommandPaletteChar` / `CommandPaletteBackspace`:
   - Build a `Pattern` from the query string
   - Score each `PaletteEntry.label` against the pattern
   - Sort by score descending
   - Update `filtered_entries` with indices of matching entries
   - Reset `selected_index` to 0

2. When query is empty, show all entries grouped by category

### UI Component

Create `src/components/command_palette.rs`:

- Render as a centered floating panel (60% width, 50% height), similar to the agent selector modal
- Top: search input with blinking cursor and ">" prompt
- Below: scrollable list of matching entries
- Each entry shows: `[Category] Action Name          Keybinding`
- Selected entry highlighted with accent color
- Category labels in dim/muted color
- Keybinding right-aligned in a distinct color
- Match characters highlighted in the label (bold or accent color)
- Bottom: entry count "N commands" or "N/M matching"

Layout:
```
┌─ Command Palette ─────────────────────────────┐
│ > search query█                                │
│───────────────────────────────────────────────│
│ ▸ Toggle Unified/Split View           Tab      │
│   Jump to Next Hunk                   ]        │
│   Jump to Previous Hunk               [        │
│   Open Comment Editor                 c        │
│   Export Feedback                     Ctrl+E   │
│   Toggle Checklist Panel              Ctrl+L   │
│   Open Settings                       ,        │
│   ...                                          │
│                                                │
│ 42 commands                                    │
└────────────────────────────────────────────────┘
```

### Action Dispatch

When the user presses Enter (`CommandPaletteConfirm`):

1. Look up the selected entry from `filtered_entries[selected_index]`
2. Close the palette (`state.command_palette.open = false`)
3. Clone the entry's `Action` and dispatch it through the normal `handle_action` flow

This means selecting "Open Comment Editor" in the palette is identical to pressing `c` — it goes through the same code path.

### KeyContext Update

Add `command_palette_open: bool` to `KeyContext` in `event.rs`, populated from `state.command_palette.open`.

## Files to Modify

1. **`src/state/command_palette_state.rs`** (NEW): `CommandPaletteState`, `PaletteEntry`, `PaletteCategory`, `build_palette_entries()`
2. **`src/state/mod.rs`**: Add `pub mod command_palette_state;`
3. **`src/state/app_state.rs`**: Add `command_palette: CommandPaletteState` field to `AppState`
4. **`src/action.rs`**: Add command palette actions
5. **`src/event.rs`**: Add `command_palette_open` to `KeyContext`, add priority 1.5 handler, add `Ctrl+P` trigger
6. **`src/components/command_palette.rs`** (NEW): Floating panel UI component
7. **`src/components/mod.rs`**: Register new component
8. **`src/app.rs`**: Handle command palette actions, dispatch selected action, call component render

## Testing

1. Press `Ctrl+P` — palette opens as centered floating panel
2. Type "hunk" — list filters to show hunk-related actions
3. Arrow keys / `Ctrl+N` / `Ctrl+P` — navigate filtered list
4. Press `Enter` — selected action executes, palette closes
5. Press `Esc` — palette closes without executing
6. Type "export" — shows "Export Feedback" with `Ctrl+E` hint
7. Empty query — shows all commands grouped by category
8. Run `cargo check` — no compilation errors
9. Verify palette does not interfere with other modals (agent selector, settings, etc.)

## Edge Cases

- Opening palette while another modal is open: palette should take priority (close other modal first, or block opening)
- Actions that require specific context (e.g., `OpenCommentEditor` requires diff focus): dispatch normally and let existing guards handle invalid state
- Very long action labels: truncate with ellipsis to fit panel width
- Terminal resize while palette is open: re-center on next render

## Backward Compatibility

No existing keybindings are changed. `Ctrl+P` is currently unmapped. This is purely additive.