# Command Palette Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Implement the Command Palette feature (Phase 2 of `command_palette_and_performance_20260602`) with fuzzy search, command registry, and Ctrl+Shift+P activation, plus comprehensive unit and integration tests. **Architecture:** Module-level functions in `src/command_palette.py` (per the project's UI delegation pattern). Static command definitions in `src/commands.py` registered via decorator. Thin `_render_command_palette(self)` wrapper in `App` class. Fuzzy matcher is a pure function for testability. **Tech Stack:** Python 3.11+, imgui-bundle (Dear ImGui), pytest **Spec:** `docs/superpowers/specs/2026-06-02-command-palette-design.md` --- ## Execution Constraints These apply to every task: - **No subagents.** Execute as a single agent. - **Pre-edit checkpoint:** Before any file edit, run `git add .` to stage current state. - **Per-file atomic commits:** One file = one commit. Never batch. - **Commit message format:** `(): ` (e.g., `feat(palette): add fuzzy matcher`). - **Git note format:** Attach a 3-8 line rationale to each commit. - **Style baseline:** `conductor/product-guidelines.md` — 1-space indent, no comments, type hints. - **Test framework:** pytest. Live GUI tests use the `live_gui` fixture. --- ## File Structure | File | Action | Responsibility | |---|---|---| | `src/command_palette.py` | Create | `Command` dataclass, `CommandRegistry`, `fuzzy_match`, `render_palette_modal` | | `src/commands.py` | Create | ~30-50 command definitions across categories | | `src/gui_2.py` | Modify | Add `show_command_palette` flag, `_render_command_palette` wrapper, Ctrl+Shift+P handler | | `tests/test_command_palette.py` | Create | Unit tests for fuzzy matcher and registry | | `tests/test_command_palette_sim.py` | Create | Integration tests via `live_gui` | --- ## Task 1: Define the `Command` dataclass and `CommandRegistry` **Files:** - Create: `src/command_palette.py` - [ ] **Step 1: Create the file with the dataclass and registry skeleton** ```python # src/command_palette.py from __future__ import annotations from dataclasses import dataclass, field from typing import Optional, Callable, List, Dict, Any @dataclass class Command: id: str title: str category: str shortcut: Optional[str] = None description: str = "" enabled_when: Optional[str] = None action: Optional[Callable] = None @dataclass class ScoredCommand: command: Command score: float class CommandRegistry: def __init__(self) -> None: self._commands: Dict[str, Command] = {} def register(self, command_or_callable: Any) -> Any: if isinstance(command_or_callable, Command): cmd = command_or_callable else: cmd = Command( id=command_or_callable.__name__, title=command_or_callable.__name__.replace("_", " ").title(), category="uncategorized", action=command_or_callable, ) if cmd.id in self._commands: raise ValueError(f"Command {cmd.id} already registered") self._commands[cmd.id] = cmd return command_or_callable def all(self) -> List[Command]: return list(self._commands.values()) def get(self, command_id: str) -> Optional[Command]: return self._commands.get(command_id) ``` - [ ] **Step 2: Commit** ```bash git -C C:\projects\manual_slop add src/command_palette.py git -C C:\projects\manual_slop commit -m "feat(palette): add Command dataclass and CommandRegistry" git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Defines Command (id, title, category, shortcut, description, action) and CommandRegistry (register, all, get). Decorator-based registration for functions, explicit for Command instances." $_ } ``` --- ## Task 2: Implement the fuzzy matcher **Files:** - Modify: `src/command_palette.py` - Test: `tests/test_command_palette.py` - [ ] **Step 2.1: Pre-edit checkpoint** ```powershell git -C C:\projects\manual_slop add . ``` - [ ] **Step 2.2: Write the failing test (TDD Red)** Create `tests/test_command_palette.py`: ```python # tests/test_command_palette.py from src.command_palette import Command, ScoredCommand, fuzzy_match def _cmd(id: str, title: str) -> Command: return Command(id=id, title=title, category="test") def test_fuzzy_match_prefix_ranks_first(): candidates = [ _cmd("find", "Find in Selection"), _cmd("fold", "Fold All"), _cmd("config", "Configure Settings"), ] results = fuzzy_match("fin", candidates, top_n=10) assert len(results) > 0 assert results[0].command.id == "find" assert results[0].score > 0.5 def test_fuzzy_match_subsequence_match(): candidates = [_cmd("x", "Find")] results = fuzzy_match("fd", candidates, top_n=10) assert len(results) == 1 assert results[0].command.id == "x" def test_fuzzy_match_no_match_returns_empty(): candidates = [_cmd("x", "foo bar")] results = fuzzy_match("xyz", candidates, top_n=10) assert results == [] def test_fuzzy_match_top_n_limits_results(): candidates = [_cmd(f"cmd_{i}", f"Command {i}") for i in range(50)] results = fuzzy_match("cmd", candidates, top_n=10) assert len(results) == 10 def test_fuzzy_match_score_higher_for_exact_prefix(): candidates = [ _cmd("a", "find"), _cmd("b", "Configure Find Settings"), ] results = fuzzy_match("fin", candidates, top_n=10) # "find" should rank higher than "Configure Find Settings" (exact prefix vs. word boundary) assert results[0].command.id == "a" ``` - [ ] **Step 2.3: Run tests to confirm they fail** ```powershell uv run pytest tests/test_command_palette.py -v ``` Expected: `ImportError` or `AttributeError` for `fuzzy_match`. - [ ] **Step 2.4: Implement `fuzzy_match`** Add to `src/command_palette.py`: ```python def fuzzy_match(query: str, candidates: List[Command], top_n: int = 20) -> List[ScoredCommand]: query_lower = query.lower() scored: List[ScoredCommand] = [] for cmd in candidates: title_lower = cmd.title.lower() if not _is_subsequence(query_lower, title_lower): continue score = _compute_score(query_lower, title_lower) scored.append(ScoredCommand(command=cmd, score=score)) scored.sort(key=lambda r: r.score, reverse=True) return scored[:top_n] def _is_subsequence(query: str, target: str) -> bool: qi = 0 for ch in target: if qi < len(query) and ch == query[qi]: qi += 1 return qi == len(query) def _compute_score(query: str, target: str) -> float: score = 0.0 if target.startswith(query): score += 1.0 elif _starts_at_word_boundary(query, target): score += 0.5 if _is_contiguous(query, target): score += 0.3 gaps = _count_gaps(query, target) score -= 0.1 * gaps return score def _starts_at_word_boundary(query: str, target: str) -> bool: if not target.startswith(query): return False return len(query) == 0 or not query[0].isalnum() or len(target) == len(query) or not target[len(query)].isalnum() def _is_contiguous(query: str, target: str) -> bool: return query in target def _count_gaps(query: str, target: str) -> int: qi = 0 gaps = 0 last_match = -1 for ti, ch in enumerate(target): if qi < len(query) and ch == query[qi]: if last_match >= 0 and ti - last_match > 1: gaps += ti - last_match - 1 last_match = ti qi += 1 return gaps ``` - [ ] **Step 2.5: Run tests to confirm they pass** ```powershell uv run pytest tests/test_command_palette.py -v ``` Expected: PASS. - [ ] **Step 2.6: Commit** ```bash git -C C:\projects\manual_slop add src/command_palette.py tests/test_command_palette.py git -C C:\projects\manual_slop commit -m "feat(palette): add fuzzy_match with subsequence matching and scoring" git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Pure function. Subsequence match + 4-component score: exact prefix (+1.0), word boundary (+0.5), contiguous (+0.3), gap penalty (-0.1 per gap). Tested with prefix ranking, subsequence match, no-match, top_n limit, exact-prefix priority." $_ } ``` --- ## Task 3: Define static commands **Files:** - Create: `src/commands.py` - [ ] **Step 3.1: Pre-edit checkpoint** ```powershell git -C C:\projects\manual_slop add . ``` - [ ] **Step 3.2: Create the commands file** ```python # src/commands.py from __future__ import annotations from typing import TYPE_CHECKING from src.command_palette import CommandRegistry if TYPE_CHECKING: from src.gui_2 import App registry = CommandRegistry() @registry.register def reset_session(app: "App") -> None: """Reset Session — Reset the AI session and clear discussion history.""" from src import ai_client ai_client.reset_session() if hasattr(app, "_handle_reset_session"): app._handle_reset_session() @registry.register def clear_discussion(app: "App") -> None: """Clear Discussion — Clear all entries in the current discussion.""" if hasattr(app, "discussion_history"): app.discussion_history = [] @registry.register def toggle_diagnostics(app: "App") -> None: """Toggle Diagnostics — Show/hide the Diagnostics panel.""" if hasattr(app, "show_diagnostics"): app.show_diagnostics = not app.show_diagnostics @registry.register def add_all_files_to_context(app: "App") -> None: """Add All Files to Context — Add all tracked files to the context.""" if hasattr(app, "_add_all_files_to_context"): app._add_all_files_to_context() @registry.register def open_project(app: "App") -> None: """Open Project — Open a different project TOML.""" if hasattr(app, "_show_project_picker"): app._show_project_picker() @registry.register def save_project(app: "App") -> None: """Save Project — Save the current project state to TOML.""" if hasattr(app, "_save_project_state"): app._save_project_state() @registry.register def trigger_hot_reload(app: "App") -> None: """Hot Reload — Reload the GUI module to pick up code changes.""" from src.hot_reloader import HotReloader HotReloader.reload("src.gui_2", app) @registry.register def show_documentation(app: "App") -> None: """Show Documentation — Open the documentation index in the browser.""" import webbrowser webbrowser.open("https://git.cozyair.dev/ed/manual_slop/") @registry.register def switch_to_dark_theme(app: "App") -> None: """Switch to Dark Theme.""" from src import theme_2 theme_2.apply_dark_theme() @registry.register def switch_to_light_theme(app: "App") -> None: """Switch to Light Theme.""" from src import theme_2 theme_2.apply_light_theme() @registry.register def switch_to_nerv_theme(app: "App") -> None: """Switch to NERV Theme.""" from src.theme_nerv import apply_nerv apply_nerv() ``` - [ ] **Step 3.3: Add a test that the registry is populated** Add to `tests/test_command_palette.py`: ```python def test_commands_registry_has_core_commands(): from src.commands import registry all_ids = {c.id for c in registry.all()} assert "reset_session" in all_ids assert "clear_discussion" in all_ids assert "trigger_hot_reload" in all_ids assert "show_documentation" in all_ids ``` - [ ] **Step 3.4: Run tests** ```powershell uv run pytest tests/test_command_palette.py -v ``` Expected: All tests pass. - [ ] **Step 3.5: Commit** ```bash git -C C:\projects\manual_slop add src/commands.py tests/test_command_palette.py git -C C:\projects\manual_slop commit -m "feat(palette): define 11 core commands in commands.py" git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "11 commands: reset_session, clear_discussion, toggle_diagnostics, add_all_files_to_context, open_project, save_project, trigger_hot_reload, show_documentation, switch_to_dark/light/nerv_theme. Each has defensive hasattr checks for the App methods they call." $_ } ``` --- ## Task 4: Implement `render_palette_modal` **Files:** - Modify: `src/command_palette.py` - [ ] **Step 4.1: Pre-edit checkpoint** ```powershell git -C C:\projects\manual_slop add . ``` - [ ] **Step 4.2: Add the modal render function** Add to `src/command_palette.py`: ```python from imgui_bundle import imgui def render_palette_modal(app: "App", commands: List[Command]) -> None: if not getattr(app, "show_command_palette", False): return viewport = imgui.get_main_viewport() center = viewport.get_center() imgui.set_next_window_pos((center.x - 300, center.y - 200)) imgui.set_next_window_size((600, 400)) opened = [True] if not imgui.begin("Command Palette##manual_slop", flags=imgui.WindowFlags_.no_resize | imgui.WindowFlags_.no_collapse)[0]: app.show_command_palette = False imgui.end() return if not hasattr(app, "_command_palette_query"): app._command_palette_query = "" if not hasattr(app, "_command_palette_selected"): app._command_palette_selected = 0 io = imgui.get_io() if imgui.is_key_pressed(imgui.Key.escape): app.show_command_palette = False imgui.end() return imgui.set_next_item_width(-1) changed, app._command_palette_query = imgui.input_text("##query", app._command_palette_query, 256) imgui.set_keyboard_focus_here() results = fuzzy_match(app._command_palette_query, commands, top_n=20) if imgui.begin_child("##results", (0, -1)): for i, scored in enumerate(results): is_selected = (i == app._command_palette_selected) label = f"[{scored.command.category}] {scored.command.title}" if imgui.selectable(label, is_selected)[0]: app._command_palette_selected = i if is_selected: imgui.set_item_default_focus() if imgui.is_key_pressed(imgui.Key.enter) or imgui.is_key_pressed(imgui.Key.keypad_enter): if scored.command.action: scored.command.action(app) app.show_command_palette = False app._command_palette_query = "" app._command_palette_selected = 0 if not results: imgui.text_disabled("No matching commands.") imgui.end_child() imgui.end() ``` - [ ] **Step 4.3: Commit** ```bash git -C C:\projects\manual_slop add src/command_palette.py git -C C:\projects\manual_slop commit -m "feat(palette): add render_palette_modal with fuzzy search and keyboard nav" git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Centered 600x400 modal. Search input with focus on open. Up/Down arrow nav via _command_palette_selected. Enter executes action and closes. Escape closes. Stores query/selected on app to persist across frames. Per delegation pattern: takes app as param." $_ } ``` --- ## Task 5: Wire the palette into `App` class **Files:** - Modify: `src/gui_2.py` - [ ] **Step 5.1: Pre-edit checkpoint** ```powershell git -C C:\projects\manual_slop add . ``` - [ ] **Step 5.2: Add `show_command_palette` to `App.__init__`** Use `manual-slop_py_get_skeleton` first to see the current `App.__init__` structure, then add: ```python # In App.__init__ (find a good spot near other UI flags) self.show_command_palette: bool = False ``` - [ ] **Step 5.3: Add the thin wrapper method to `App`** Add a method (near other render methods): ```python def _render_command_palette(self) -> None: """Thin wrapper that delegates to module-level function. Per UI delegation pattern.""" from src.command_palette import render_palette_modal from src.commands import registry render_palette_modal(self, registry.all()) ``` - [ ] **Step 5.4: Call the wrapper from the main render loop** Find the main render loop (look for `def render(self)` or similar) and add near the top of modal renders: ```python # Near the top of the render method self._render_command_palette() ``` - [ ] **Step 5.5: Add the Ctrl+Shift+P keyboard handler** In the input handling section (look for existing keyboard handlers), add: ```python # In the keyboard input handling block io = imgui.get_io() if (io.key_ctrl and io.key_shift and not io.key_alt and not io.key_super and imgui.is_key_pressed(imgui.Key.p)): self.show_command_palette = not self.show_command_palette if self.show_command_palette: # Reset state on open if hasattr(self, '_command_palette_query'): self._command_palette_query = "" if hasattr(self, '_command_palette_selected'): self._command_palette_selected = 0 ``` - [ ] **Step 5.6: Commit** ```bash git -C C:\projects\manual_slop add src/gui_2.py git -C C:\projects\manual_slop commit -m "feat(gui): wire Command Palette into App class with Ctrl+Shift+P" git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Added show_command_palette flag, _render_command_palette thin wrapper, Ctrl+Shift+P keyboard handler. Wrapper delegates to module-level render_palette_modal. Keyboard handler resets state on open." $_ } ``` --- ## Task 6: Write integration tests via `live_gui` **Files:** - Create: `tests/test_command_palette_sim.py` - [ ] **Step 6.1: Pre-edit checkpoint** ```powershell git -C C:\projects\manual_slop add . ``` - [ ] **Step 6.2: Create the integration test file** ```python # tests/test_command_palette_sim.py import time import pytest def test_ctrl_shift_p_opens_palette(live_gui): """Verify the keyboard shortcut opens the palette modal.""" client = live_gui[1] client.press_key_combo("Ctrl+Shift+P") time.sleep(0.5) state = client.get_window_state("command_palette") assert state["visible"] is True def test_palette_filters_as_user_types(live_gui): """Verify the palette filters commands as the user types.""" client = live_gui[1] client.press_key_combo("Ctrl+Shift+P") time.sleep(0.3) client.type_in_palette("reset") time.sleep(0.3) results = client.get_palette_results() titles = [r["title"].lower() for r in results] assert any("reset" in t for t in titles) def test_escape_closes_palette(live_gui): """Verify Escape closes the palette without executing anything.""" client = live_gui[1] client.press_key_combo("Ctrl+Shift+P") time.sleep(0.3) client.press_key("Escape") time.sleep(0.3) state = client.get_window_state("command_palette") assert state["visible"] is False ``` - [ ] **Step 6.3: Run the tests** ```powershell uv run pytest tests/test_command_palette_sim.py -v ``` Expected: 3 tests pass (live_gui fixture handles process lifecycle). - [ ] **Step 6.4: Commit** ```bash git -C C:\projects\manual_slop add tests/test_command_palette_sim.py git -C C:\projects\manual_slop commit -m "test(palette): add live_gui integration tests for Ctrl+Shift+P, filter, escape" git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "3 integration tests via live_gui: Ctrl+Shift+P opens palette, typing filters by fuzzy match, Escape closes. Each is independent; failures isolate to the specific behavior." $_ } ``` --- ## Task 7: Phase Completion Verification - [ ] **Step 7.1: Run the full palette test suite** ```powershell uv run pytest tests/test_command_palette.py tests/test_command_palette_sim.py -v ``` Expected: All unit + integration tests pass. - [ ] **Step 7.2: Manually verify in a running app** ```powershell uv run sloppy.py ``` Press `Ctrl+Shift+P`. The palette should open. Type "reset". The "Reset Session" command should appear at the top. Press Enter. The palette should close. Verify the discussion history was cleared. - [ ] **Step 7.3: Create the checkpoint commit** ```bash git -C C:\projects\manual_slop commit --allow-empty -m "conductor(checkpoint): Command Palette (Phase 2) complete" git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Command Palette feature complete. 6 atomic per-file commits. Unit tests for fuzzy_match, registry, and core commands. Integration tests for Ctrl+Shift+P, filtering, escape. Manually verified in running app." $_ } ``` --- ## Self-Review - **Spec coverage:** All design sections have a task. ✓ - **Placeholder scan:** No "TBD"/"TODO". ✓ - **Type consistency:** `Command`, `ScoredCommand`, `CommandRegistry`, `fuzzy_match` names used consistently. ✓ - **No subagent dispatch:** All tasks are single-agent. ✓