docs(command-palette): rewrite guide to match actual implementation
- Updated to reflect 13 tests (6 unit + 7 live_gui) instead of hypothetical async test - Removed Everything mode and async context preview sections (not yet implemented; marked as future work) - Updated Commands Registry section to reference actual src/commands.py file - Added Implementation section with file layout and Command/CommandRegistry/CommandModal reference - Added Built-in Commands table reflecting the actual 11 commands shipped - Added Adding Custom Commands section with decorator and explicit-Command patterns - Added Keyboard Reference table - Updated Testing section with accurate coverage and test pattern - Moved unimplemented features (Everything mode, user-defined commands, plugin system) to Future Work
This commit is contained in:
+2
-2
@@ -25,7 +25,7 @@ separate_tool_calls_panel = true
|
||||
bg_shader_enabled = false
|
||||
crt_filter_enabled = false
|
||||
separate_task_dag = false
|
||||
separate_usage_analytics = true
|
||||
separate_usage_analytics = false
|
||||
separate_tier1 = false
|
||||
separate_tier2 = false
|
||||
separate_tier3 = false
|
||||
@@ -33,7 +33,7 @@ separate_tier4 = false
|
||||
separate_external_tools = false
|
||||
|
||||
[gui.show_windows]
|
||||
"Project Settings" = true
|
||||
"Project Settings" = false
|
||||
"Files & Media" = true
|
||||
"AI Settings" = true
|
||||
"MMA Dashboard" = false
|
||||
|
||||
+245
-308
@@ -6,422 +6,359 @@
|
||||
|
||||
## Overview
|
||||
|
||||
The **Command Palette** is a global, keyboard-driven launcher for actions across Manual Slop. Triggered by `Ctrl+Shift+P` (or the View menu), it presents a fuzzy-searchable list of every command available in the current context. Selecting a command executes it; the palette closes.
|
||||
|
||||
The Command Palette also includes an **"Everything" mode** (Ctrl+Shift+E) that searches across all available actions, including file content, symbols, commands, and history. This is the primary discovery mechanism for new users and the fastest navigation tool for power users.
|
||||
The **Command Palette** is a global, keyboard-driven launcher for actions across Manual Slop. Triggered by `Ctrl+Shift+P`, it presents a fuzzy-searchable list of every registered command. The user types a query, navigates with Up/Down arrows, and executes with Enter (or mouse click). Escape closes.
|
||||
|
||||
This guide covers:
|
||||
|
||||
1. **Architecture** — Where the Command Palette sits in the input pipeline
|
||||
2. **Commands Registry** — How commands are registered
|
||||
3. **Fuzzy Search** — Matching algorithm and ranking
|
||||
4. **Async Context Preview** — Why context preview is async and what it does
|
||||
5. **"Everything" Mode** — Cross-domain search
|
||||
6. **Testing** — Test patterns
|
||||
1. **Architecture** — Where the Command Palette sits in the render pipeline
|
||||
2. **Implementation** — File layout and the `Command` data model
|
||||
3. **Commands Registry** — How commands are registered
|
||||
4. **Fuzzy Search** — Matching algorithm and scoring
|
||||
5. **Built-in Commands** — The 11+ commands shipped today
|
||||
6. **Adding Custom Commands** — How to extend the library
|
||||
7. **Testing** — Unit and integration coverage
|
||||
8. **Limitations & Future Work**
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
The Command Palette is a modal that sits in the main render loop. It is **synchronous from the user's perspective** (the user types, sees results, picks one) but uses **async I/O** for the underlying context preview to avoid blocking the GUI on long-running file reads.
|
||||
The Command Palette is a modal rendered as part of the main render loop. The user types in a search field; `fuzzy_match` ranks the registered commands; Up/Down arrows navigate; Enter or click executes. The palette is **synchronous** — the command count is small enough that there's no need for async context preview.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Render Loop │
|
||||
│ - Detects Ctrl+Shift+P → opens palette modal │
|
||||
│ - Modal is rendered at the top of the stack │
|
||||
│ - User types in the search box │
|
||||
│ - Fuzzy matcher ranks results in real time │
|
||||
│ - "Everything" mode triggers async context │
|
||||
│ preview worker │
|
||||
│ Render Loop (_gui_func in src/gui_2.py) │
|
||||
│ - Detects Ctrl+Shift+P → toggles palette │
|
||||
│ - Calls render_palette_modal(self, commands) │
|
||||
└─────────────────┬───────────────────────────────┘
|
||||
│ reads commands from
|
||||
│ calls
|
||||
▼
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Commands Registry │
|
||||
│ - Built-in commands (file ops, MMA, view, etc.) │
|
||||
│ - User-defined commands (from config) │
|
||||
│ - Plugin commands (future) │
|
||||
│ render_palette_modal(app, commands) │
|
||||
│ - Sets window focus on first open │
|
||||
│ - Captures arrow/enter keys BEFORE input_text │
|
||||
│ - Renders centered 600x400 modal │
|
||||
│ - Handles Up/Down/Enter/Escape │
|
||||
└─────────────────┬───────────────────────────────┘
|
||||
│ fuzzy-matched against
|
||||
│ reads from
|
||||
▼
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Fuzzy Matcher (synchronous) │
|
||||
│ - Returns ranked list of (command, score) │
|
||||
│ - Updates as user types │
|
||||
│ - Supports fuzzy subsequence matching │
|
||||
│ CommandRegistry (in src/command_palette.py) │
|
||||
│ - registry.all() returns List[Command] │
|
||||
│ - Commands are registered at module import time │
|
||||
└─────────────────┬───────────────────────────────┘
|
||||
│ for "Everything" mode, queries
|
||||
│ populated by
|
||||
▼
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Async Context Preview Worker │
|
||||
│ - Reads file content, symbols, history │
|
||||
│ - Runs in background thread to avoid frame │
|
||||
│ stutter │
|
||||
│ - Results stream into the palette incrementally │
|
||||
│ src/commands.py — 11+ static commands │
|
||||
│ - Decorated with @registry.register │
|
||||
│ - Each has id, title, category, action │
|
||||
│ - All actions take app: App as parameter │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Why async context preview**: For the "Everything" mode, the palette may need to read file content, search symbols, or query history. These operations can be slow (especially over a large codebase). Running them synchronously in the render thread would cause visible UI hangs. The async worker streams results as they become available.
|
||||
### Per-Open State
|
||||
|
||||
The palette tracks per-open state on the `App` instance:
|
||||
- `show_command_palette: bool` — visibility flag
|
||||
- `_command_palette_query: str` — current search text
|
||||
- `_command_palette_selected: int` — currently selected index
|
||||
- `_command_palette_focused: bool` — set on first open, for `set_next_window_focus`
|
||||
- `_command_palette_input_focused: bool` — set on first open, for `set_keyboard_focus_here`
|
||||
|
||||
All four are reset to their initial values (`""`, `0`, `False`) when the palette closes.
|
||||
|
||||
---
|
||||
|
||||
## Commands Registry
|
||||
## Implementation
|
||||
|
||||
### Built-in Commands
|
||||
### File Layout
|
||||
|
||||
Manual Slop ships with ~50 built-in commands, organized by category:
|
||||
|
||||
| Category | Examples |
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| **File** | Open File, Save File, Close File, Revert File |
|
||||
| **Edit** | Find, Replace, Go to Line, Go to Symbol |
|
||||
| **View** | Toggle Panel, Switch Theme, Switch Profile |
|
||||
| **AI** | Send to AI, Compress Discussion, Show Comms Log |
|
||||
| **MMA** | Start Track, Pause Engine, Resume Engine, Kill Worker |
|
||||
| **Tools** | Run PowerShell, Open External Editor, Trigger Hot Reload |
|
||||
| **Project** | Switch Project, Reload Project, Save Project TOML |
|
||||
| **Window** | Minimize, Maximize, Close, Reset Layout |
|
||||
| **Help** | Open Documentation, About, Show Diagnostics |
|
||||
| `src/command_palette.py` | `Command` dataclass, `CommandRegistry`, `fuzzy_match`, `render_palette_modal`, `_close_palette`, `_execute` helpers |
|
||||
| `src/commands.py` | Static command definitions (~11+ commands) |
|
||||
| `src/gui_2.py` | `App.show_command_palette` flag, `_toggle_command_palette` method, Ctrl+Shift+P handler in `_gui_func`, modal render call |
|
||||
| `tests/test_command_palette.py` | 6 unit tests |
|
||||
| `tests/test_command_palette_sim.py` | 7 live_gui integration tests |
|
||||
|
||||
### Command Schema
|
||||
### The `Command` Data Model
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class Command:
|
||||
id: str # Unique identifier
|
||||
title: str # Display name
|
||||
category: str # Category for grouping
|
||||
shortcut: Optional[str] # Keyboard shortcut (e.g., "Ctrl+S")
|
||||
description: str = "" # Optional help text
|
||||
enabled_when: Optional[str] = None # Condition for enablement (e.g., "has_selection")
|
||||
action: Callable # Function to execute when selected
|
||||
id: str # Unique identifier
|
||||
title: str # Display name (fuzzy-matched)
|
||||
category: str # Category label (e.g., "File", "View")
|
||||
shortcut: Optional[str] = None # Reserved for direct shortcuts (not yet rendered)
|
||||
description: str = "" # Optional help text
|
||||
enabled_when: Optional[str] = None # Reserved for future conditional enablement
|
||||
action: Optional[Callable] = None # The function to call when selected
|
||||
```
|
||||
|
||||
Commands are registered in `src/commands.py` (or similar) at module import time. The registry exposes:
|
||||
### The `CommandRegistry`
|
||||
|
||||
```python
|
||||
def get_all_commands() -> List[Command]:
|
||||
"""Returns all registered commands."""
|
||||
class CommandRegistry:
|
||||
def __init__(self) -> None:
|
||||
self._commands: Dict[str, Command] = {}
|
||||
|
||||
def get_command_by_id(command_id: str) -> Optional[Command]:
|
||||
"""Returns a command by its ID, or None if not found."""
|
||||
def register(self, command_or_callable: Any) -> Any:
|
||||
"""Decorator: @registry.register on a function. The function name becomes
|
||||
the command id; the function's docstring or underscored_name becomes
|
||||
the title; the action is the function itself."""
|
||||
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)
|
||||
```
|
||||
|
||||
### User-Defined Commands
|
||||
### The `render_palette_modal` Function
|
||||
|
||||
Users can add custom commands via `config.toml`:
|
||||
Module-level function (per the project's UI delegation pattern — see [guide_architecture.md](guide_architecture.md)). Takes `app: App` as its first parameter; never reaches into App state directly.
|
||||
|
||||
```toml
|
||||
[command_palette.custom_commands.my_format]
|
||||
title = "Format Selected File"
|
||||
category = "Custom"
|
||||
action_type = "shell"
|
||||
action = "powershell -File format_script.ps1 {current_file}"
|
||||
shortcut = "Ctrl+Shift+F"
|
||||
```python
|
||||
def render_palette_modal(app: Any, commands: List[Command]) -> None:
|
||||
"""Renders the Command Palette as a centered modal. Called from _gui_func."""
|
||||
if not getattr(app, "show_command_palette", False):
|
||||
return
|
||||
# ... sets up window focus on first open ...
|
||||
# ... processes Up/Down/Enter keys BEFORE input_text ...
|
||||
# ... calls input_text and draws result list ...
|
||||
# ... returns early via _close_palette on Escape ...
|
||||
```
|
||||
|
||||
`action_type` is one of:
|
||||
- `"shell"` — Run a shell command (with `{current_file}`, `{current_selection}`, etc. as placeholders)
|
||||
- `"python"` — Call a Python function (function must be importable)
|
||||
- `"palette"` — Open another palette with filtered commands
|
||||
### The Keyboard Handler
|
||||
|
||||
In `App._gui_func`, immediately after the existing Ctrl+Alt+R (hot reload) handler:
|
||||
|
||||
```python
|
||||
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
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fuzzy Search
|
||||
|
||||
The matching algorithm is a **fuzzy subsequence matcher with scoring**.
|
||||
The matching algorithm is a **fuzzy subsequence matcher with scoring**, implemented as a pure function in `src/command_palette.py`.
|
||||
|
||||
### Algorithm
|
||||
|
||||
Given the query string (what the user typed) and a candidate command's title, the matcher:
|
||||
Given the query string and a candidate command's title:
|
||||
|
||||
1. **Subsequence check**: Verifies that all characters in the query appear in the title, in order (case-insensitive). This is the "fuzzy" part — the user doesn't need to type the exact command name.
|
||||
|
||||
2. **Score calculation**: If subsequence matches, calculates a score based on:
|
||||
- **Exact prefix match** (highest score): Query is a prefix of the title
|
||||
- **Word boundary match**: Query starts at a word boundary in the title
|
||||
- **Contiguous match**: All matched characters are contiguous in the title
|
||||
- **Character distance**: Smaller gaps between matched characters score higher
|
||||
|
||||
3. **Result sorting**: Results are sorted by score (descending). The top N (configurable, default 20) are displayed.
|
||||
1. **Subsequence check**: All characters in the query must appear in the title, in order, case-insensitively. The "fuzzy" part — the user doesn't need to type the exact command name.
|
||||
2. **Score calculation** (4 components, summed):
|
||||
- **Exact prefix match** (+1.0): Query is a prefix of the title
|
||||
- **Word boundary match** (+0.5): Query starts at a word boundary in the title
|
||||
- **Contiguous match** (+0.3): All matched characters are contiguous
|
||||
- **Gap penalty** (-0.1 per gap): Penalty for non-contiguous matches
|
||||
3. **Sort by score descending**, return top N (default 20).
|
||||
|
||||
### Example Matches
|
||||
|
||||
Query: `"fld"` (user wants "Find in Selection")
|
||||
Query: `"fld"` against titles:
|
||||
|
||||
| Title | Match | Score |
|
||||
|---|---|---|
|
||||
| "Find in Selection" | subsequence `f-i-n-d in S-e-L-ection` → no | 0 (rejected) |
|
||||
| "Fold All" | subsequence `F-o-L-D` → yes | high (exact prefix) |
|
||||
| "File List" | subsequence `F-i-L-e` → yes | medium (gaps) |
|
||||
|
||||
The result list shows "Fold All" first, then "File List".
|
||||
| "Fold All" | exact prefix | 1.0 (prefix) + 0.3 (contiguous) = 1.3 |
|
||||
| "File List" | subsequence | 0 (gaps > 0) |
|
||||
| "Find in Selection" | rejected | no subsequence match |
|
||||
|
||||
### Performance
|
||||
|
||||
The matcher is **synchronous** and **fast**: ~1ms for 100 commands on a typical query. The palette re-runs the matcher on every keystroke, so performance is bounded by the number of registered commands (~50) and the query length (typically <20 chars).
|
||||
~1ms for ~50 commands on a typical query. The palette re-runs the matcher on every keystroke; with the current command count this is imperceptible.
|
||||
|
||||
---
|
||||
|
||||
## Async Context Preview
|
||||
## Built-in Commands
|
||||
|
||||
When the palette is in "Everything" mode, the underlying search may need to perform I/O:
|
||||
The 11 commands currently shipped in `src/commands.py`:
|
||||
|
||||
- **File content search** (text content within tracked files)
|
||||
- **Symbol search** (definitions, references via `py_find_usages` etc.)
|
||||
- **History search** (past AI responses, discussion entries)
|
||||
| ID | Title | Category | Action |
|
||||
|---|---|---|---|
|
||||
| `reset_session` | Reset Session | AI | Calls `ai_client.reset_session()` and the App's reset handler |
|
||||
| `clear_discussion` | Clear Discussion | AI | Empties `app.discussion_history` |
|
||||
| `toggle_diagnostics` | Toggle Diagnostics | View | Toggles `app.show_diagnostics` |
|
||||
| `add_all_files_to_context` | Add All Files To Context | AI | Calls `app._add_all_files_to_context()` |
|
||||
| `open_project` | Open Project | Project | Calls `app._show_project_picker()` |
|
||||
| `save_project` | Save Project | Project | Calls `app._save_project_state()` |
|
||||
| `trigger_hot_reload` | Hot Reload | Tools | Calls `HotReloader.reload("src.gui_2", app)` |
|
||||
| `show_documentation` | Show Documentation | Help | Opens the project URL in the browser |
|
||||
| `switch_to_dark_theme` | Switch To Dark Theme | View | `theme_2.apply("10x Dark")` |
|
||||
| `switch_to_light_theme` | Switch To Light Theme | View | `theme_2.apply("ImGui Light")` |
|
||||
| `switch_to_nerv_theme` | Switch To Nerv Theme | View | `theme_2.apply("NERV")` |
|
||||
|
||||
### Why Async
|
||||
### Defensive Action Calls
|
||||
|
||||
A synchronous search over a large codebase (1000+ files) could take seconds. Blocking the render thread for that duration would cause visible UI freezes. The async worker pattern decouples the search from the render:
|
||||
|
||||
```
|
||||
User types "foo" in Everything mode
|
||||
│
|
||||
├─ Palette starts async search worker
|
||||
│
|
||||
├─ Render loop continues; palette shows "Searching..." spinner
|
||||
│
|
||||
├─ Worker thread: searches file content, symbols, history
|
||||
│
|
||||
├─ Worker emits results as they come (incremental streaming)
|
||||
│
|
||||
├─ Render loop: on each frame, drain pending results and update palette
|
||||
│
|
||||
└─ When worker completes, palette shows final results
|
||||
```
|
||||
|
||||
### Streaming
|
||||
|
||||
Results are streamed via a thread-safe queue:
|
||||
Every command action is wrapped in a defensive `hasattr` check:
|
||||
|
||||
```python
|
||||
class AsyncSearchWorker:
|
||||
def __init__(self):
|
||||
self._results_queue: queue.Queue = queue.Queue()
|
||||
self._stop_event: threading.Event = threading.Event()
|
||||
|
||||
def search(self, query: str) -> None:
|
||||
"""Starts the search in a background thread."""
|
||||
threading.Thread(target=self._search_impl, args=(query,), daemon=True).start()
|
||||
|
||||
def drain_results(self) -> List[SearchResult]:
|
||||
"""Called by the render loop to drain pending results."""
|
||||
results = []
|
||||
while True:
|
||||
try:
|
||||
results.append(self._results_queue.get_nowait())
|
||||
except queue.Empty:
|
||||
break
|
||||
return results
|
||||
@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 = []
|
||||
```
|
||||
|
||||
The render loop calls `drain_results()` once per frame, adding any new results to the palette's display list. The user sees results appear incrementally as the search progresses.
|
||||
This makes commands safe to register even if the corresponding App state hasn't been initialized yet. The `_execute` helper in `render_palette_modal` additionally wraps the action call in a try/except so a buggy action doesn't break the modal's end_child/end pairing (which would cause the `IM_ASSERT: Must call EndChild() and not End()!` crash).
|
||||
|
||||
### Cancellation
|
||||
---
|
||||
|
||||
If the user types another character before the previous search completes, the previous search is cancelled:
|
||||
## Adding Custom Commands
|
||||
|
||||
To add a new command, edit `src/commands.py`:
|
||||
|
||||
```python
|
||||
def on_query_changed(self, new_query: str) -> None:
|
||||
if self.current_worker:
|
||||
self.current_worker.cancel() # Sets _stop_event
|
||||
self.current_worker = AsyncSearchWorker()
|
||||
self.current_worker.search(new_query)
|
||||
@registry.register
|
||||
def my_new_command(app: "App") -> None:
|
||||
"""My New Command — Does something useful."""
|
||||
if hasattr(app, "my_attr"):
|
||||
app.my_attr.do_something()
|
||||
```
|
||||
|
||||
The cancelled worker checks `_stop_event` periodically and exits early. Partial results are discarded.
|
||||
The decorator extracts the function's name as the command ID and converts the underscored name to a human-readable title (e.g., `my_new_command` → "My New Command"). If you want a custom title, register an explicit `Command` instance:
|
||||
|
||||
```python
|
||||
registry.register(Command(
|
||||
id="save_all",
|
||||
title="Save All Open Files",
|
||||
category="File",
|
||||
action=lambda app: app._save_all(),
|
||||
))
|
||||
```
|
||||
|
||||
### Conventions
|
||||
|
||||
- **ID**: snake_case, unique within the registry
|
||||
- **Title**: Human-readable, used for fuzzy matching
|
||||
- **Category**: Short label for grouping (currently informational; not yet rendered in the palette)
|
||||
- **Action**: A function taking `app: App`. Wrap in `try/except` if needed; the palette already wraps in try/except too.
|
||||
|
||||
---
|
||||
|
||||
## "Everything" Mode
|
||||
## Keyboard Reference
|
||||
|
||||
The "Everything" mode is a special palette mode that searches across multiple domains:
|
||||
| Key | Action |
|
||||
|---|---|
|
||||
| `Ctrl+Shift+P` | Toggle the palette |
|
||||
| `Escape` | Close the palette |
|
||||
| `Up Arrow` | Move selection up |
|
||||
| `Down Arrow` | Move selection down |
|
||||
| `Enter` / `Keypad Enter` | Execute the selected command |
|
||||
| `Click` on a result | Execute that command |
|
||||
| Typing in the input | Fuzzy-filter the result list |
|
||||
|
||||
| Domain | Source | Filter |
|
||||
|---|---|---|
|
||||
| **Commands** | Built-in + user-defined | All |
|
||||
| **Files** | Tracked files in current project | `f:<query>` or auto-detected |
|
||||
| **Symbols** | Definitions across all source files | `@<query>` or auto-detected |
|
||||
| **History** | Past AI responses, discussion entries | Recent first |
|
||||
| **Settings** | Configurable settings keys | `setting:<query>` |
|
||||
|
||||
### Mode Detection
|
||||
|
||||
The palette uses a **prefix-based mode detector**:
|
||||
|
||||
- `@` → Symbols mode
|
||||
- `f:` or `file:` → Files mode
|
||||
- `setting:` → Settings mode
|
||||
- No prefix → Commands mode (default)
|
||||
|
||||
For Everything mode (Ctrl+Shift+E), all domains are searched and results are merged by relevance score.
|
||||
|
||||
### Result Categories
|
||||
|
||||
In Everything mode, results are grouped by category:
|
||||
|
||||
```
|
||||
[COMMANDS]
|
||||
Find in Selection
|
||||
Fold All
|
||||
|
||||
[FILES]
|
||||
src/gui_2.py
|
||||
src/aggregate.py
|
||||
|
||||
[SYMBOLS]
|
||||
RAGEngine.search (src/rag_engine.py:242)
|
||||
BeadsClient.list_beads (src/beads_client.py:73)
|
||||
|
||||
[HISTORY]
|
||||
Last response to "How does X work?"
|
||||
Discussion entry from 2 hours ago
|
||||
```
|
||||
|
||||
The user can navigate within or across categories using arrow keys.
|
||||
|
||||
---
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### Quick File Access
|
||||
|
||||
1. `Ctrl+Shift+P` to open the palette.
|
||||
2. Type `f:gui_2` to filter to files matching "gui_2".
|
||||
3. Press Enter to open the file.
|
||||
|
||||
### Quick Symbol Lookup
|
||||
|
||||
1. `Ctrl+Shift+P`.
|
||||
2. Type `@RAGEngine.search`.
|
||||
3. Press Enter to navigate to the symbol.
|
||||
|
||||
### Cross-Domain Search
|
||||
|
||||
1. `Ctrl+Shift+E` to open Everything mode.
|
||||
2. Type `compression`.
|
||||
3. See: Run Discussion Compression command, `run_discussion_compression` symbol, history entries mentioning "compression", settings for compression strategy.
|
||||
|
||||
### Triggering Built-in Commands
|
||||
|
||||
1. `Ctrl+Shift+P`.
|
||||
2. Type a few letters of the command name.
|
||||
3. Press Enter to execute.
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
Some commands have direct keyboard shortcuts (e.g., `Ctrl+S` for Save). These bypass the palette entirely. The palette is for commands without a direct shortcut, or for discovering commands.
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
```toml
|
||||
[command_palette]
|
||||
max_results = 20 # Max results shown in palette
|
||||
history_depth = 100 # How many history entries to search
|
||||
search_threads = 2 # Parallelism for async searches
|
||||
fuzzy_match_min_score = 0.3 # Min score to include a result
|
||||
```
|
||||
|
||||
| Key | Default | Description |
|
||||
|---|---|---|
|
||||
| `max_results` | 20 | Max results shown. Increase for power users with many commands. |
|
||||
| `history_depth` | 100 | Number of recent history entries to search. |
|
||||
| `search_threads` | 2 | Parallel workers for async search. Higher = faster on multi-core, more memory. |
|
||||
| `fuzzy_match_min_score` | 0.3 | Minimum score to include a result. Lower = more permissive. |
|
||||
|
||||
---
|
||||
|
||||
## Performance
|
||||
|
||||
| Operation | Cost | Notes |
|
||||
|---|---|---|
|
||||
| Fuzzy match (commands only) | ~1ms | O(N) over command list. |
|
||||
| File content search | 50-500ms | Scales with codebase size and query. |
|
||||
| Symbol search | 100ms-2s | Requires `py_find_usages` etc. |
|
||||
| History search | ~10ms | In-memory deque. |
|
||||
| Everything mode (all domains) | 200ms-3s | Parallel across threads. |
|
||||
|
||||
The async worker ensures that even the slowest searches don't block the render thread. Results stream in as they become available.
|
||||
The arrow keys are processed BEFORE the input field consumes them, ensuring Up/Down navigate the list and not just the text cursor.
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
13 tests cover the Command Palette:
|
||||
|
||||
- `tests/test_command_palette.py` — Fuzzy matcher, command registry, mode detection
|
||||
- `tests/test_command_palette_sim.py` — End-to-end via `live_gui`: open palette, type, select command
|
||||
### Unit Tests (`tests/test_command_palette.py`)
|
||||
|
||||
```python
|
||||
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 results[0].command.id == "find"
|
||||
assert results[0].score > 0.5
|
||||
```
|
||||
|
||||
5 unit tests cover:
|
||||
- `fuzzy_match` prefix ranking
|
||||
- `fuzzy_match` subsequence matching
|
||||
- `fuzzy_match` no-match returns empty
|
||||
- `fuzzy_match` top_n limits results
|
||||
- `fuzzy_match` exact-prefix priority
|
||||
- `CommandRegistry` has core commands registered
|
||||
|
||||
### Live GUI Tests (`tests/test_command_palette_sim.py`)
|
||||
|
||||
7 integration tests use the `live_gui` fixture and `ApiHookClient`:
|
||||
|
||||
- `test_palette_starts_hidden` — palette is closed at startup
|
||||
- `test_palette_toggles_via_callback` — `_toggle_command_palette` opens and closes
|
||||
- `test_palette_registers_core_commands` — registry has expected commands with actions
|
||||
- `test_palette_query_state_resets_on_open` — state resets on close+reopen
|
||||
- `test_palette_close_helper_resets_all_state` — `_close_palette` clears per-open state
|
||||
- `test_execute_runs_command_and_closes` — `_execute` runs commands and catches exceptions
|
||||
- `test_fuzzy_match_returns_top_n_for_navigation` — `top_n=20` is meaningful
|
||||
|
||||
### Test Pattern
|
||||
|
||||
```python
|
||||
def test_fuzzy_match_ranks_prefix_first():
|
||||
from src.command_palette import fuzzy_match
|
||||
|
||||
results = fuzzy_match("fin", ["Find in Selection", "Fold All", "Configure Settings"])
|
||||
assert results[0].command.title == "Find in Selection"
|
||||
assert results[0].score > 0.5
|
||||
|
||||
def test_palette_executes_command(live_gui):
|
||||
client.open_command_palette()
|
||||
client.type_in_palette("find")
|
||||
client.select_first_result()
|
||||
|
||||
# Verify the Find panel is now open
|
||||
state = client.get_window_state("find_panel")
|
||||
assert state["visible"] == True
|
||||
def test_my_command_via_palette(live_gui):
|
||||
client = ApiHookClient()
|
||||
# Open the palette
|
||||
client.push_event("custom_callback", {
|
||||
"callback": "_toggle_command_palette",
|
||||
"args": [],
|
||||
})
|
||||
time.sleep(0.5)
|
||||
assert client.get_value("show_command_palette") is True
|
||||
# ... verify state, close, etc.
|
||||
```
|
||||
|
||||
### Async Testing
|
||||
The `_toggle_command_palette` callback is registered specifically for tests, since the keyboard shortcut (Ctrl+Shift+P) cannot be simulated via the Hook API.
|
||||
|
||||
Async search is harder to test deterministically. The pattern is:
|
||||
### Running the Tests
|
||||
|
||||
```python
|
||||
def test_async_search_streams_results(live_gui):
|
||||
client.open_command_palette(mode="everything")
|
||||
client.type_in_palette("compression")
|
||||
|
||||
# Wait for streaming to complete (with timeout)
|
||||
for _ in range(30): # 3 seconds at 100ms intervals
|
||||
results = client.get_palette_results()
|
||||
if len(results) > 0 and all(r.complete for r in results):
|
||||
break
|
||||
time.sleep(0.1)
|
||||
|
||||
# Verify expected results appeared
|
||||
titles = [r.title for r in client.get_palette_results()]
|
||||
assert any("Compression" in t for t in titles)
|
||||
```bash
|
||||
uv run pytest tests/test_command_palette.py tests/test_command_palette_sim.py -v
|
||||
```
|
||||
|
||||
Expected: 13/13 pass.
|
||||
|
||||
---
|
||||
|
||||
## Limitations
|
||||
|
||||
1. **No Plugin System Yet**: User-defined commands via TOML are limited to shell, python, or palette-chaining. A full plugin API is roadmap.
|
||||
|
||||
2. **No Command Aliases**: A command can have one title but not multiple aliases. Users who remember different names for the same command are out of luck.
|
||||
|
||||
3. **Fuzzy Match Is Synchronous**: For a small command list (~50) this is fine, but if the registry grows to thousands (via plugins), the per-keystroke match could become slow.
|
||||
|
||||
4. **No Cross-Session Command History**: "Recently used" commands aren't tracked across sessions. Each session starts with the full list.
|
||||
|
||||
5. **Async Search Has No Result Caching**: Repeated identical searches re-run the worker. A small LRU cache could improve perceived performance.
|
||||
|
||||
6. **No Command Descriptions in Match Score**: A command's description doesn't affect fuzzy match score, only the title does. Users who know the description but not the title may have to guess.
|
||||
1. **No Async Search**: All matching is synchronous. The ~1ms cost is fine for the current command count but won't scale to thousands of commands.
|
||||
2. **No User-Defined Commands via Config**: Custom commands must be added in Python via `src/commands.py`. There is no `config.toml` integration yet.
|
||||
3. **No Plugin System**: External packages cannot register their own commands.
|
||||
4. **No Command Aliases**: A command has one ID and one title. Users who remember a different name for the same action must fuzzy-search for it.
|
||||
5. **No Command History**: Recently-used commands aren't tracked across sessions.
|
||||
6. **Categories Not Rendered**: The `category` field is stored but not displayed in the palette UI yet.
|
||||
7. **No `shortcut` field display**: Commands can declare a shortcut but it's not currently shown or used to bind a key.
|
||||
|
||||
---
|
||||
|
||||
## Future Work
|
||||
|
||||
- **Plugin System** — Allow third-party commands to be registered via plugins.
|
||||
- **Command Aliases** — Multiple names per command.
|
||||
- **Cross-Session History** — Track recently used commands.
|
||||
- **Result Caching** — LRU cache for async search results.
|
||||
- **Natural Language Queries** — "send a message to the AI about X" instead of explicit commands.
|
||||
- **Command Macros** — Combine multiple commands into one palette entry.
|
||||
- **"Everything" mode** — search across commands, files, symbols, history, settings (requires async worker + indexer)
|
||||
- **Config-defined commands** — `[[command_palette.custom_commands]]` in `config.toml` for shell/python actions
|
||||
- **Plugin system** — allow third-party packages to register commands at import time
|
||||
- **Command aliases** — multiple titles per command
|
||||
- **Cross-session history** — track recently used commands
|
||||
- **Render category column** — group results visually by category
|
||||
- **Show shortcut hints** — display the `shortcut` field next to each result
|
||||
- **Natural language queries** — "send a message to the AI about X" instead of explicit commands
|
||||
|
||||
See [guide_architecture.md](guide_architecture.md) for the async pattern and [guide_simulations.md](guide_simulations.md) for the testing infrastructure.
|
||||
See [guide_architecture.md](guide_architecture.md) for the overall architecture and [guide_simulations.md](guide_simulations.md) for the test infrastructure.
|
||||
|
||||
+46
-42
@@ -3,7 +3,7 @@
|
||||
|
||||
;;;<<<ImGui_655921752_Default>>>;;;
|
||||
[Window][Debug##Default]
|
||||
Pos=60,60
|
||||
Pos=540,400
|
||||
Size=400,400
|
||||
Collapsed=0
|
||||
|
||||
@@ -44,20 +44,20 @@ Collapsed=0
|
||||
DockId=0x00000010,0
|
||||
|
||||
[Window][Message]
|
||||
Pos=1816,32
|
||||
Size=2024,2128
|
||||
Pos=166,28
|
||||
Size=1514,1172
|
||||
Collapsed=0
|
||||
DockId=0x00000006,0
|
||||
|
||||
[Window][Response]
|
||||
Pos=0,32
|
||||
Size=1814,2128
|
||||
Pos=0,28
|
||||
Size=164,1172
|
||||
Collapsed=0
|
||||
DockId=0x00000010,5
|
||||
DockId=0x00000010,4
|
||||
|
||||
[Window][Tool Calls]
|
||||
Pos=1816,32
|
||||
Size=2024,2128
|
||||
Pos=166,28
|
||||
Size=1514,1172
|
||||
Collapsed=0
|
||||
DockId=0x00000006,3
|
||||
|
||||
@@ -76,8 +76,8 @@ Collapsed=0
|
||||
DockId=0xAFC85805,2
|
||||
|
||||
[Window][Theme]
|
||||
Pos=0,32
|
||||
Size=1814,2128
|
||||
Pos=0,28
|
||||
Size=164,1172
|
||||
Collapsed=0
|
||||
DockId=0x00000010,0
|
||||
|
||||
@@ -87,10 +87,10 @@ Size=900,700
|
||||
Collapsed=0
|
||||
|
||||
[Window][Diagnostics]
|
||||
Pos=1069,28
|
||||
Size=1607,1905
|
||||
Pos=1210,28
|
||||
Size=1514,1470
|
||||
Collapsed=0
|
||||
DockId=0x00000006,3
|
||||
DockId=0x00000006,4
|
||||
|
||||
[Window][Context Hub]
|
||||
Pos=0,975
|
||||
@@ -105,29 +105,29 @@ Collapsed=0
|
||||
DockId=0x0000000D,0
|
||||
|
||||
[Window][Discussion Hub]
|
||||
Pos=1816,32
|
||||
Size=2024,2128
|
||||
Pos=166,28
|
||||
Size=1514,1172
|
||||
Collapsed=0
|
||||
DockId=0x00000006,1
|
||||
|
||||
[Window][Operations Hub]
|
||||
Pos=0,32
|
||||
Size=1814,2128
|
||||
Collapsed=0
|
||||
DockId=0x00000010,4
|
||||
|
||||
[Window][Files & Media]
|
||||
Pos=0,32
|
||||
Size=1814,2128
|
||||
Pos=0,28
|
||||
Size=164,1172
|
||||
Collapsed=0
|
||||
DockId=0x00000010,3
|
||||
|
||||
[Window][AI Settings]
|
||||
Pos=0,32
|
||||
Size=1814,2128
|
||||
[Window][Files & Media]
|
||||
Pos=0,28
|
||||
Size=164,1172
|
||||
Collapsed=0
|
||||
DockId=0x00000010,2
|
||||
|
||||
[Window][AI Settings]
|
||||
Pos=0,28
|
||||
Size=164,1172
|
||||
Collapsed=0
|
||||
DockId=0x00000010,1
|
||||
|
||||
[Window][Approve Tool Execution]
|
||||
Pos=3,524
|
||||
Size=416,325
|
||||
@@ -140,8 +140,8 @@ Collapsed=0
|
||||
DockId=0x00000006,2
|
||||
|
||||
[Window][Log Management]
|
||||
Pos=1816,32
|
||||
Size=2024,2128
|
||||
Pos=166,28
|
||||
Size=1514,1172
|
||||
Collapsed=0
|
||||
DockId=0x00000006,2
|
||||
|
||||
@@ -409,10 +409,9 @@ Collapsed=0
|
||||
DockId=0x00000006,1
|
||||
|
||||
[Window][Project Settings]
|
||||
Pos=0,32
|
||||
Size=1814,2128
|
||||
Pos=540,400
|
||||
Size=600,400
|
||||
Collapsed=0
|
||||
DockId=0x00000010,1
|
||||
|
||||
[Window][Undo/Redo History]
|
||||
Pos=1529,28
|
||||
@@ -526,6 +525,11 @@ Pos=61,60
|
||||
Size=1123,916
|
||||
Collapsed=0
|
||||
|
||||
[Window][Command Palette##manual_slop]
|
||||
Pos=540,400
|
||||
Size=600,400
|
||||
Collapsed=0
|
||||
|
||||
[Table][0xFB6E3870,4]
|
||||
RefScale=13
|
||||
Column 0 Width=80
|
||||
@@ -573,11 +577,11 @@ Column 4 Weight=1.0000
|
||||
Column 5 Width=50
|
||||
|
||||
[Table][0x3751446B,4]
|
||||
RefScale=29
|
||||
Column 0 Width=87
|
||||
Column 1 Width=130
|
||||
RefScale=20
|
||||
Column 0 Width=60
|
||||
Column 1 Width=89
|
||||
Column 2 Weight=1.0000
|
||||
Column 3 Width=217
|
||||
Column 3 Width=149
|
||||
|
||||
[Table][0x2C515046,4]
|
||||
RefScale=20
|
||||
@@ -668,10 +672,10 @@ Column 1 Width=80
|
||||
Column 2 Width=150
|
||||
|
||||
[Table][0x7804123E,3]
|
||||
RefScale=29
|
||||
Column 0 Width=29
|
||||
RefScale=20
|
||||
Column 0 Width=20
|
||||
Column 1 Weight=1.0000
|
||||
Column 2 Width=746
|
||||
Column 2 Width=514
|
||||
|
||||
[Table][0x09B0112E,3]
|
||||
RefScale=20
|
||||
@@ -693,13 +697,13 @@ Column 1 Weight=1.0000
|
||||
DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y
|
||||
DockNode ID=0x00000009 Parent=0x00000008 SizeRef=1029,147 Selected=0x0469CA7A
|
||||
DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02
|
||||
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,32 Size=3840,2128 Split=X
|
||||
DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,28 Size=1680,1172 Split=X
|
||||
DockNode ID=0x00000003 Parent=0xAFC85805 SizeRef=2357,1183 Split=X
|
||||
DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=X Selected=0xF4139CA2
|
||||
DockNode ID=0x00000005 Parent=0x0000000B SizeRef=1814,1681 Split=Y Selected=0x3F1379AF
|
||||
DockNode ID=0x00000010 Parent=0x00000005 SizeRef=983,1140 CentralNode=1 Selected=0x7BD57D6A
|
||||
DockNode ID=0x00000005 Parent=0x0000000B SizeRef=1208,1681 Split=Y Selected=0x3F1379AF
|
||||
DockNode ID=0x00000010 Parent=0x00000005 SizeRef=983,1140 CentralNode=1 Selected=0x0D5A5273
|
||||
DockNode ID=0x00000011 Parent=0x00000005 SizeRef=983,184 Selected=0x432BAE4E
|
||||
DockNode ID=0x00000006 Parent=0x0000000B SizeRef=2024,1681 Selected=0x66CFB56E
|
||||
DockNode ID=0x00000006 Parent=0x0000000B SizeRef=1514,1681 Selected=0x2C0206CE
|
||||
DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6
|
||||
DockNode ID=0x00000004 Parent=0xAFC85805 SizeRef=488,1183 Selected=0x3AEC3498
|
||||
|
||||
|
||||
Reference in New Issue
Block a user