# Command Palette Implementation & Tests

**Date:** 2026-06-02
**Status:** Draft (pending review)
**Parent Track:** `command_palette_and_performance_20260602` (continuing Phase 2 + adding Phase 3)
**Spec:** `conductor/tracks/command_palette_and_performance_20260602/spec.md` (existing)

---

## Context & Motivation

A `command_palette_and_performance_20260602` track was started in early June 2026. **Phase 1 (Async Context Preview)** is complete. **Phase 2 (Command Palette)** is unstarted — no `src/command_palette.py`, no `src/commands.py`, no test files. The user reports the palette doesn't pop up on `Ctrl+Shift+P`.

The existing spec says `Ctrl+P`; this design uses `Ctrl+Shift+P` (per the user's expectation and VSCode convention documented in `docs/guide_command_palette.md`).

This design finishes Phase 2 of the existing track and adds Phase 3 (tests).

---

## Scope

### In Scope

- `src/command_palette.py` — Module-level: `Command` dataclass, `CommandRegistry`, `fuzzy_match()`, `render_palette_modal(app)`, `render_everything_modal(app)`
- `src/commands.py` — Static command definitions (~30-50 commands across categories)
- `src/gui_2.py` — `self.show_command_palette: bool` in `App.__init__`, `_render_command_palette(self)` thin wrapper, `Ctrl+Shift+P` keyboard handler
- `tests/test_command_palette.py` — Unit tests for fuzzy matcher, command registry, mode detection
- `tests/test_command_palette_sim.py` — Integration tests via `live_gui`

### Out of Scope

- Async context preview (Phase 1; already complete)
- The "Everything" mode async search worker (mentioned in the existing spec; defer to a follow-up track)
- Visual theming of the palette (use existing ImGui style)

---

## Design

### Data Model: `Command`

```python
@dataclass
class Command:
    id: str                    # Unique identifier
    title: str                 # Display name
    category: str              # Category for grouping
    shortcut: Optional[str]   # Optional default shortcut (e.g., "Ctrl+S")
    description: str = ""      # Optional help text
    enabled_when: Optional[str] = None  # Optional condition expression
    action: Callable = None    # Function to execute when selected
```

### Command Registry

```python
# src/commands.py
from src.command_palette import Command, CommandRegistry

registry = CommandRegistry()

@registry.register
def save_file(app: App) -> None:
    """Save File — File category, Ctrl+S"""
    # ... call app's save logic
```

**Registration patterns:**
- Decorator: `@registry.register` for top-level functions
- Explicit: `registry.register(Command(id=..., title=..., action=...))` for closures or classes

### Fuzzy Matcher

Implemented in `src/command_palette.py` as a pure function:

```python
def fuzzy_match(query: str, candidates: List[Command], top_n: int = 20) -> List[ScoredCommand]:
    """
    Returns the top_n candidates matching query, ranked by score.
    
    Algorithm:
    1. Subsequence check: query chars must appear in title, in order
    2. Score calculation:
       - Exact prefix match: +1.0
       - Word boundary match: +0.5
       - Contiguous match: +0.3
       - Character distance penalty: -0.1 per gap
    3. Sort by score descending
    4. Return top_n
    """
```

### Modal Rendering

The palette is a centered ImGui modal. Module-level function (per delegation pattern):

```python
# src/command_palette.py
def render_palette_modal(app: App) -> None:
    """Render the Command Palette modal. Called from gui_2.py when app.show_command_palette is True."""
    if not app.show_command_palette:
        return
    imgui.set_next_window_position(...)  # Centered
    imgui.set_next_window_size(...)
    if imgui.begin("Command Palette##palette", closable=True):
        # Search input
        # Fuzzy-matched results list
        # Keyboard navigation
    imgui.end()
```

### Keyboard Handler

In `gui_2.py`'s main event loop:

```python
io = imgui.get_io()
if io.key_ctrl and io.key_shift and imgui.is_key_pressed(imgui.Key.p):
    app.show_command_palette = not app.show_command_palette
```

---

## File Structure

- `src/command_palette.py` — NEW: Command, CommandRegistry, fuzzy_match, render_palette_modal
- `src/commands.py` — NEW: Static command definitions and registry
- `src/gui_2.py` — MODIFY: add `self.show_command_palette`, add `_render_command_palette` wrapper, add Ctrl+Shift+P handler, register the palette module
- `tests/test_command_palette.py` — NEW: Unit tests
- `tests/test_command_palette_sim.py` — NEW: Integration tests via live_gui

---

## Tests

### Unit Tests (`tests/test_command_palette.py`)

```python
def test_fuzzy_match_prefix_ranks_first():
    from src.command_palette import fuzzy_match
    candidates = [
        Command(id="find", title="Find in Selection"),
        Command(id="fold", title="Fold All"),
        Command(id="config", title="Configure Settings"),
    ]
    results = fuzzy_match("fin", candidates)
    assert results[0].command.id == "find"
    assert results[0].score > 0.5

def test_fuzzy_match_rejects_no_match():
    from src.command_palette import fuzzy_match
    candidates = [Command(id="x", title="foo bar")]
    results = fuzzy_match("xyz", candidates)
    assert len(results) == 0

def test_command_registry_register_and_list():
    from src.command_palette import CommandRegistry
    from src.commands import registry
    assert "save_file" in registry.all()
    # All commands have id, title, category
    for cmd in registry.all():
        assert cmd.id and cmd.title and cmd.category

def test_command_registry_duplicate_raises():
    from src.command_palette import CommandRegistry, Command
    reg = CommandRegistry()
    reg.register(Command(id="x", title="X", category="test"))
    with pytest.raises(ValueError):
        reg.register(Command(id="x", title="X", category="test"))
```

### Integration Tests (`tests/test_command_palette_sim.py`)

```python
def test_ctrl_shift_p_opens_palette(live_gui):
    client = live_gui[1]
    # Press Ctrl+Shift+P
    client.press_key_combo("Ctrl+Shift+P")
    # Verify the palette is visible
    state = client.get_window_state("command_palette")
    assert state["visible"] == True

def test_palette_filters_as_user_types(live_gui):
    client = live_gui[1]
    client.press_key_combo("Ctrl+Shift+P")
    client.type_in_palette("save")
    results = client.get_palette_results()
    assert any("Save" in r.title for r in results)
    # Other commands not shown
    assert not any("Compress" in r.title for r in results)

def test_palette_executes_command_on_enter(live_gui):
    client = live_gui[1]
    client.press_key_combo("Ctrl+Shift+P")
    client.type_in_palette("Reset")
    client.press_key("Down")
    client.press_key("Enter")
    # Verify the reset command was executed (check via Hook API)
    state = client.get_session_state()
    assert state.get("discussion_history", []) == []
```

---

## Acceptance Criteria

- `Ctrl+Shift+P` opens the palette (verified via `live_gui` test)
- Typing in the palette filters results via fuzzy match
- Selecting a command (Enter key) executes it and closes the palette
- Escape closes the palette without executing
- All unit tests pass
- All integration tests pass
- The palette respects the existing theme (dark/light/nerv)
- No new lint errors

---

## Risks

1. **Keyboard handler conflicts:** The Ctrl+Shift+P combo might be intercepted by other subsystems. Mitigation: check for other handlers in the codebase first; if conflicts, document them.
2. **Pyodide build dependencies:** The image_bundle web backend (for Track 4) has a different architecture than this track. The two are independent but should be aware of each other.
3. **Test flakiness:** `live_gui` tests can be flaky if the GUI doesn't initialize in time. Mitigation: the standard 15-second readiness polling is sufficient.
