Gitea (and any case-sensitive filesystem) was rendering the [Top]
nav links in /docs as broken because of two bugs:
1. Case-sensitivity: 22 links used '../README.md' (all-uppercase)
but the actual file is 'docs/Readme.md' (capital R, lowercase
rest). 21 guide_*.md nav bars were affected, plus 1 internal
cross-link in Readme.md itself. Works on Windows (case-
insensitive) but broken on Linux/Gitea.
Fix: 22 occurrences across 22 files changed
'../README.md' -> '../Readme.md'
2. Wrong relative-path level: 16 links used '../../conductor/...'
from 'docs/guide_*.md' to reach 'conductor/'. This goes up 2
levels to 'projects/', which doesn't exist. The correct path
from 'docs/guide_*.md' to 'conductor/' is 1 level up
('../conductor/...'). 12 unique patterns across 10 files
affected.
Fix: 16 occurrences across 10 files changed
'../../conductor/' -> '../conductor/'
3. Bonus: 1 planned-guide link in guide_context_curation.md
referenced a never-written 'guide_context_presets.md'. The
ContextPreset schema is now fully covered in the new
'guide_context_aggregation.md' (per the 2026-06-08 docs
refresh). Fix: link target updated.
No content was changed, only link paths. 24 files, 37 link
replacements, 37 deletions.
Verification:
- All .md links in docs/ now resolve to existing files
(validated by path-resolution check from each file's directory)
- The 3 new guides from the previous docs refresh commit
(guide_discussions.md, guide_state_lifecycle.md,
guide_context_aggregation.md) had the case bug inherited from
guide_architecture.md's existing nav pattern; their top-of-file
nav bars are now correct
- The 21 pre-existing guide nav bars that had the same bug
(all 21 of them, except the 3 that used the correct case:
guide_mma.md, guide_simulations.md, guide_tools.md) are now
also fixed
- Inter-guide links (e.g. [Discussions](guide_discussions.md))
were not affected; they were always correct because both the
link text and the actual filename are lowercase
This is a docs-only fix. No code modified.
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:
- Architecture — Where the Command Palette sits in the render pipeline
- Implementation — File layout and the
Commanddata model - Commands Registry — How commands are registered
- Fuzzy Search — Matching algorithm and scoring
- Built-in Commands — The 11+ commands shipped today
- Adding Custom Commands — How to extend the library
- Testing — Unit and integration coverage
- 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, forset_next_window_focus_command_palette_input_focused: bool— set on first open, forset_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
...
Fuzzy Search
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:
- 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.
- 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
- 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 intry/exceptif 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_matchprefix rankingfuzzy_matchsubsequence matchingfuzzy_matchno-match returns emptyfuzzy_matchtop_n limits resultsfuzzy_matchexact-prefix priorityCommandRegistryhas 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 startuptest_palette_toggles_via_callback—_toggle_command_paletteopens and closestest_palette_registers_core_commands— registry has expected commands with actionstest_palette_query_state_resets_on_open— state resets on close+reopentest_palette_close_helper_resets_all_state—_close_paletteclears per-open statetest_execute_runs_command_and_closes—_executeruns commands and catches exceptionstest_fuzzy_match_returns_top_n_for_navigation—top_n=20is 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
- 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.
- No User-Defined Commands via Config: Custom commands must be added in Python via
src/commands.py. There is noconfig.tomlintegration yet. - No Plugin System: External packages cannot register their own commands.
- 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.
- No Command History: Recently-used commands aren't tracked across sessions.
- Categories Not Rendered: The
categoryfield is stored but not displayed in the palette UI yet. - No
shortcutfield 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]]inconfig.tomlfor 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
shortcutfield 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.