Private
Public Access
0
0
Files
manual_slop/docs/guide_command_palette.md
T

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

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

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.

┌─────────────────────────────────────────────────┐
│  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                                │
└─────────────────┬───────────────────────────────┘
                  │ reads commands from
                  ▼
┌─────────────────────────────────────────────────┐
│  Commands Registry                               │
│  - Built-in commands (file ops, MMA, view, etc.) │
│  - User-defined commands (from config)           │
│  - Plugin commands (future)                      │
└─────────────────┬───────────────────────────────┘
                  │ fuzzy-matched against
                  ▼
┌─────────────────────────────────────────────────┐
│  Fuzzy Matcher (synchronous)                     │
│  - Returns ranked list of (command, score)       │
│  - Updates as user types                         │
│  - Supports fuzzy subsequence matching            │
└─────────────────┬───────────────────────────────┘
                  │ for "Everything" mode, queries
                  ▼
┌─────────────────────────────────────────────────┐
│  Async Context Preview Worker                     │
│  - Reads file content, symbols, history          │
│  - Runs in background thread to avoid frame       │
│    stutter                                       │
│  - Results stream into the palette incrementally │
└─────────────────────────────────────────────────┘

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.


Commands Registry

Built-in Commands

Manual Slop ships with ~50 built-in commands, organized by category:

Category Examples
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

Command Schema

@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

Commands are registered in src/commands.py (or similar) at module import time. The registry exposes:

def get_all_commands() -> List[Command]:
    """Returns all registered commands."""

def get_command_by_id(command_id: str) -> Optional[Command]:
    """Returns a command by its ID, or None if not found."""

User-Defined Commands

Users can add custom commands via config.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"

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 matching algorithm is a fuzzy subsequence matcher with scoring.

Algorithm

Given the query string (what the user typed) and a candidate command's title, the matcher:

  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.

Example Matches

Query: "fld" (user wants "Find in Selection")

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

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


Async Context Preview

When the palette is in "Everything" mode, the underlying search may need to perform I/O:

  • File content search (text content within tracked files)
  • Symbol search (definitions, references via py_find_usages etc.)
  • History search (past AI responses, discussion entries)

Why Async

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:

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

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.

Cancellation

If the user types another character before the previous search completes, the previous search is cancelled:

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)

The cancelled worker checks _stop_event periodically and exits early. Partial results are discarded.


"Everything" Mode

The "Everything" mode is a special palette mode that searches across multiple domains:

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

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


Testing

Unit Tests

  • 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

Test Pattern

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

Async Testing

Async search is harder to test deterministically. The pattern is:

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)

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.


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.

See guide_architecture.md for the async pattern and guide_simulations.md for the testing infrastructure.