653 lines
21 KiB
Markdown
653 lines
21 KiB
Markdown
# 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:** `<type>(<scope>): <imperative description>` (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. ✓
|