Private
Public Access
0
0
Files
manual_slop/docs/guide_command_palette.md
T
ed 86d093e101 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
2026-06-02 22:46:48 -04:00

15 KiB

Command Palette

Top | Architecture | Simulations | Workspace Profiles


Overview

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 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 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 (_gui_func in src/gui_2.py)        │
│  - Detects Ctrl+Shift+P → toggles palette        │
│  - Calls render_palette_modal(self, commands)   │
└─────────────────┬───────────────────────────────┘
                  │ calls
                  ▼
┌─────────────────────────────────────────────────┐
│  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                 │
└─────────────────┬───────────────────────────────┘
                  │ reads from
                  ▼
┌─────────────────────────────────────────────────┐
│  CommandRegistry (in src/command_palette.py)    │
│  - registry.all() returns List[Command]         │
│  - Commands are registered at module import time │
└─────────────────┬───────────────────────────────┘
                  │ populated by
                  ▼
┌─────────────────────────────────────────────────┐
│  src/commands.py — 11+ static commands           │
│  - Decorated with @registry.register            │
│  - Each has id, title, category, action          │
│  - All actions take app: App as parameter        │
└─────────────────────────────────────────────────┘

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.


Implementation

File Layout

File Role
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

The Command Data Model

@dataclass
class Command:
    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

The CommandRegistry

class CommandRegistry:
    def __init__(self) -> None:
        self._commands: Dict[str, Command] = {}

    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)

The render_palette_modal Function

Module-level function (per the project's UI delegation pattern — see guide_architecture.md). Takes app: App as its first parameter; never reaches into App state directly.

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 ...

The Keyboard Handler

In App._gui_func, immediately after the existing Ctrl+Alt+R (hot reload) handler:

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
        ...

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 and a candidate command's title:

  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" against titles:

Title Match Score
"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

~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.


Built-in Commands

The 11 commands currently shipped in src/commands.py:

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")

Defensive Action Calls

Every command action is wrapped in a defensive hasattr check:

@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 = []

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).


Adding Custom Commands

To add a new command, edit src/commands.py:

@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 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:

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.

Keyboard Reference

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

The arrow keys are processed BEFORE the input field consumes them, ensuring Up/Down navigate the list and not just the text cursor.


Testing

13 tests cover the Command Palette:

Unit Tests (tests/test_command_palette.py)

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_navigationtop_n=20 is meaningful

Test Pattern

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.

The _toggle_command_palette callback is registered specifically for tests, since the keyboard shortcut (Ctrl+Shift+P) cannot be simulated via the Hook API.

Running the Tests

uv run pytest tests/test_command_palette.py tests/test_command_palette_sim.py -v

Expected: 13/13 pass.


Limitations

  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

  • "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 for the overall architecture and guide_simulations.md for the test infrastructure.