feat(palette): comprehensive command library (32 commands, up from 11)
Added commands focused on ergonomics and mouse-free operation: View (window toggles, 12 new): toggle_text_viewer, toggle_diagnostics, toggle_usage_analytics, toggle_context_preview, toggle_tier1_strategy, toggle_tier2_tech_lead, toggle_tier3_workers, toggle_tier4_qa, toggle_external_tools, toggle_shader_editor, toggle_undo_redo_history, toggle_command_palette Layout (3 new): show_all_panels, hide_all_panels, save_workspace_profile, show_workspace_manager Theme (1 new): cycle_theme (Dark -> Light -> NERV cycle) Tools (2 new): undo, redo Project (1 new): save_all (flush to project + config + global config) Help (1 new): show_command_palette_help (opens docs/Readme.md in Text Viewer) Refactored: extracted _toggle_window and _toggle_attr helpers to reduce duplication and make commands safer (no-op if state is missing). Reset session now also clears comms and tool logs (matches the menu item behavior). Added 7 new unit tests for the expanded command library.
This commit is contained in:
+232
-14
@@ -1,5 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Callable
|
||||||
from src.command_palette import CommandRegistry
|
from src.command_palette import CommandRegistry
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -9,13 +9,41 @@ if TYPE_CHECKING:
|
|||||||
registry = CommandRegistry()
|
registry = CommandRegistry()
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _toggle_window(app: "App", name: str) -> None:
|
||||||
|
"""Toggle a window in app.show_windows by name. Defensive no-op if missing."""
|
||||||
|
if hasattr(app, "show_windows") and isinstance(app.show_windows, dict):
|
||||||
|
app.show_windows[name] = not app.show_windows.get(name, False)
|
||||||
|
|
||||||
|
|
||||||
|
def _toggle_attr(app: "App", attr: str) -> None:
|
||||||
|
"""Toggle a boolean attribute on app. Defensive no-op if missing."""
|
||||||
|
if hasattr(app, attr):
|
||||||
|
current = getattr(app, attr)
|
||||||
|
if isinstance(current, bool):
|
||||||
|
setattr(app, attr, not current)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# AI / Discussion
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
@registry.register
|
@registry.register
|
||||||
def reset_session(app: "App") -> None:
|
def reset_session(app: "App") -> None:
|
||||||
"""Reset Session — Reset the AI session and clear discussion history."""
|
"""Reset Session — Reset the AI session, clear comms and tool logs."""
|
||||||
from src import ai_client
|
from src import ai_client
|
||||||
ai_client.reset_session()
|
ai_client.reset_session()
|
||||||
if hasattr(app, "_handle_reset_session"):
|
if hasattr(app, "_handle_reset_session"):
|
||||||
app._handle_reset_session()
|
app._handle_reset_session()
|
||||||
|
if hasattr(app, "_comms_log"):
|
||||||
|
app._comms_log.clear()
|
||||||
|
if hasattr(app, "_tool_log"):
|
||||||
|
app._tool_log.clear()
|
||||||
|
if hasattr(app, "ai_response"):
|
||||||
|
app.ai_response = ""
|
||||||
|
|
||||||
|
|
||||||
@registry.register
|
@registry.register
|
||||||
@@ -25,13 +53,6 @@ def clear_discussion(app: "App") -> None:
|
|||||||
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
|
@registry.register
|
||||||
def add_all_files_to_context(app: "App") -> None:
|
def add_all_files_to_context(app: "App") -> None:
|
||||||
"""Add All Files to Context — Add all tracked files to the context."""
|
"""Add All Files to Context — Add all tracked files to the context."""
|
||||||
@@ -39,6 +60,25 @@ def add_all_files_to_context(app: "App") -> None:
|
|||||||
app._add_all_files_to_context()
|
app._add_all_files_to_context()
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def generate_md_only(app: "App") -> None:
|
||||||
|
"""Generate MD Only — Run the AI to produce a markdown file without sending to the chat."""
|
||||||
|
if hasattr(app, "_do_generate"):
|
||||||
|
try:
|
||||||
|
md, path, *_ = app._do_generate()
|
||||||
|
app.last_md = md
|
||||||
|
app.last_md_path = path
|
||||||
|
if hasattr(app, "ai_status"):
|
||||||
|
app.ai_status = f"md written: {path.name}"
|
||||||
|
except Exception as e:
|
||||||
|
if hasattr(app, "ai_status"):
|
||||||
|
app.ai_status = f"error: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Project
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
@registry.register
|
@registry.register
|
||||||
def open_project(app: "App") -> None:
|
def open_project(app: "App") -> None:
|
||||||
"""Open Project — Open a different project TOML."""
|
"""Open Project — Open a different project TOML."""
|
||||||
@@ -53,6 +93,137 @@ def save_project(app: "App") -> None:
|
|||||||
app._save_project_state()
|
app._save_project_state()
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def save_all(app: "App") -> None:
|
||||||
|
"""Save All — Flush to project, flush to config, save global config."""
|
||||||
|
from src import models
|
||||||
|
if hasattr(app, "_flush_to_project"):
|
||||||
|
app._flush_to_project()
|
||||||
|
if hasattr(app, "_flush_to_config"):
|
||||||
|
app._flush_to_config()
|
||||||
|
if hasattr(app, "config"):
|
||||||
|
try:
|
||||||
|
models.save_config(app.config)
|
||||||
|
except Exception as e:
|
||||||
|
if hasattr(app, "ai_status"):
|
||||||
|
app.ai_status = f"save error: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# View — Window toggles (from show_windows dict)
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def toggle_text_viewer(app: "App") -> None:
|
||||||
|
"""Toggle Text Viewer — Open or close the standalone text/code viewer."""
|
||||||
|
_toggle_window(app, "Text Viewer")
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def toggle_diagnostics(app: "App") -> None:
|
||||||
|
"""Toggle Diagnostics — Show/hide the Diagnostics panel."""
|
||||||
|
_toggle_window(app, "Diagnostics")
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def toggle_usage_analytics(app: "App") -> None:
|
||||||
|
"""Toggle Usage Analytics — Show/hide the Usage Analytics panel."""
|
||||||
|
_toggle_window(app, "Usage Analytics")
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def toggle_context_preview(app: "App") -> None:
|
||||||
|
"""Toggle Context Preview — Show/hide the Context Preview panel."""
|
||||||
|
_toggle_window(app, "Context Preview")
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def toggle_tier1_strategy(app: "App") -> None:
|
||||||
|
"""Toggle Tier 1 Strategy — Show/hide the Tier 1 strategy stream."""
|
||||||
|
_toggle_window(app, "Tier 1: Strategy")
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def toggle_tier2_tech_lead(app: "App") -> None:
|
||||||
|
"""Toggle Tier 2 Tech Lead — Show/hide the Tier 2 tech-lead stream."""
|
||||||
|
_toggle_window(app, "Tier 2: Tech Lead")
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def toggle_tier3_workers(app: "App") -> None:
|
||||||
|
"""Toggle Tier 3 Workers — Show/hide the Tier 3 worker streams."""
|
||||||
|
_toggle_window(app, "Tier 3: Workers")
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def toggle_tier4_qa(app: "App") -> None:
|
||||||
|
"""Toggle Tier 4 QA — Show/hide the Tier 4 QA stream."""
|
||||||
|
_toggle_window(app, "Tier 4: QA")
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def toggle_external_tools(app: "App") -> None:
|
||||||
|
"""Toggle External Tools — Show/hide the External MCP tools panel."""
|
||||||
|
_toggle_window(app, "External Tools")
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def toggle_shader_editor(app: "App") -> None:
|
||||||
|
"""Toggle Shader Editor — Show/hide the live shader editor."""
|
||||||
|
_toggle_window(app, "Shader Editor")
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def toggle_undo_redo_history(app: "App") -> None:
|
||||||
|
"""Toggle Undo/Redo History — Show/hide the undo/redo history panel."""
|
||||||
|
_toggle_window(app, "Undo/Redo History")
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def toggle_command_palette(app: "App") -> None:
|
||||||
|
"""Toggle Command Palette — Open or close the command palette."""
|
||||||
|
_toggle_attr(app, "show_command_palette")
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def show_all_panels(app: "App") -> None:
|
||||||
|
"""Show All Panels — Open every toggleable window."""
|
||||||
|
if hasattr(app, "show_windows") and isinstance(app.show_windows, dict):
|
||||||
|
for name in app.show_windows.keys():
|
||||||
|
app.show_windows[name] = True
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def hide_all_panels(app: "App") -> None:
|
||||||
|
"""Hide All Panels — Close every toggleable window."""
|
||||||
|
if hasattr(app, "show_windows") and isinstance(app.show_windows, dict):
|
||||||
|
for name in app.show_windows.keys():
|
||||||
|
app.show_windows[name] = False
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Layout — Workspace Profiles
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def save_workspace_profile(app: "App") -> None:
|
||||||
|
"""Save Workspace Profile — Open the save-profile dialog."""
|
||||||
|
_toggle_attr(app, "_show_save_workspace_profile_modal")
|
||||||
|
if hasattr(app, "_new_workspace_profile_name"):
|
||||||
|
app._new_workspace_profile_name = ""
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def show_workspace_manager(app: "App") -> None:
|
||||||
|
"""Show Workspace Manager — Open the workspace profile management UI."""
|
||||||
|
if hasattr(app, "show_windows") and isinstance(app.show_windows, dict):
|
||||||
|
app.show_windows["Workspace Manager"] = True
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Tools
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
@registry.register
|
@registry.register
|
||||||
def trigger_hot_reload(app: "App") -> None:
|
def trigger_hot_reload(app: "App") -> None:
|
||||||
"""Hot Reload — Reload the GUI module to pick up code changes."""
|
"""Hot Reload — Reload the GUI module to pick up code changes."""
|
||||||
@@ -61,12 +232,23 @@ def trigger_hot_reload(app: "App") -> None:
|
|||||||
|
|
||||||
|
|
||||||
@registry.register
|
@registry.register
|
||||||
def show_documentation(app: "App") -> None:
|
def undo(app: "App") -> None:
|
||||||
"""Show Documentation — Open the documentation index in the browser."""
|
"""Undo — Revert the most recent UI mutation."""
|
||||||
import webbrowser
|
if hasattr(app, "_handle_undo"):
|
||||||
webbrowser.open("https://git.cozyair.dev/ed/manual_slop/")
|
app._handle_undo()
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def redo(app: "App") -> None:
|
||||||
|
"""Redo — Re-apply the most recently undone mutation."""
|
||||||
|
if hasattr(app, "_handle_redo"):
|
||||||
|
app._handle_redo()
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Theme
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
@registry.register
|
@registry.register
|
||||||
def switch_to_dark_theme(app: "App") -> None:
|
def switch_to_dark_theme(app: "App") -> None:
|
||||||
"""Switch to Dark Theme (10x Dark palette)."""
|
"""Switch to Dark Theme (10x Dark palette)."""
|
||||||
@@ -83,6 +265,42 @@ def switch_to_light_theme(app: "App") -> None:
|
|||||||
|
|
||||||
@registry.register
|
@registry.register
|
||||||
def switch_to_nerv_theme(app: "App") -> None:
|
def switch_to_nerv_theme(app: "App") -> None:
|
||||||
"""Switch to NERV Theme."""
|
"""Switch to NERV Theme (Tactical Console aesthetic)."""
|
||||||
from src import theme_2
|
from src import theme_2
|
||||||
theme_2.apply("NERV")
|
theme_2.apply("NERV")
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def cycle_theme(app: "App") -> None:
|
||||||
|
"""Cycle Theme — Switch to the next theme in the cycle (Dark → Light → NERV → Dark)."""
|
||||||
|
from src import theme_2
|
||||||
|
order = ["10x Dark", "ImGui Light", "NERV"]
|
||||||
|
current = theme_2.get_current_palette()
|
||||||
|
if current in order:
|
||||||
|
next_idx = (order.index(current) + 1) % len(order)
|
||||||
|
else:
|
||||||
|
next_idx = 0
|
||||||
|
theme_2.apply(order[next_idx])
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
# Help
|
||||||
|
# --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def show_documentation(app: "App") -> None:
|
||||||
|
"""Show Documentation — Open the project URL in the browser."""
|
||||||
|
import webbrowser
|
||||||
|
webbrowser.open("https://git.cozyair.dev/ed/manual_slop/")
|
||||||
|
|
||||||
|
|
||||||
|
@registry.register
|
||||||
|
def show_command_palette_help(app: "App") -> None:
|
||||||
|
"""Show Command Palette Help — Open the docs/Readme.md in the Text Viewer."""
|
||||||
|
from pathlib import Path
|
||||||
|
if hasattr(app, "readme_text"):
|
||||||
|
docs_readme = Path("docs/Readme.md")
|
||||||
|
if docs_readme.exists():
|
||||||
|
app.readme_text = docs_readme.read_text(encoding="utf-8")
|
||||||
|
if hasattr(app, "show_windows") and isinstance(app.show_windows, dict):
|
||||||
|
app.show_windows["Text Viewer"] = True
|
||||||
|
|||||||
@@ -52,3 +52,88 @@ def test_commands_registry_has_core_commands():
|
|||||||
assert "clear_discussion" in all_ids
|
assert "clear_discussion" in all_ids
|
||||||
assert "trigger_hot_reload" in all_ids
|
assert "trigger_hot_reload" in all_ids
|
||||||
assert "show_documentation" in all_ids
|
assert "show_documentation" in all_ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_commands_registry_has_view_toggles():
|
||||||
|
"""The palette should expose all window toggle commands for keyboard ergonomics."""
|
||||||
|
from src.commands import registry
|
||||||
|
all_ids = {c.id for c in registry.all()}
|
||||||
|
expected_toggles = [
|
||||||
|
"toggle_text_viewer",
|
||||||
|
"toggle_diagnostics",
|
||||||
|
"toggle_usage_analytics",
|
||||||
|
"toggle_context_preview",
|
||||||
|
"toggle_tier1_strategy",
|
||||||
|
"toggle_tier2_tech_lead",
|
||||||
|
"toggle_tier3_workers",
|
||||||
|
"toggle_tier4_qa",
|
||||||
|
"toggle_external_tools",
|
||||||
|
"toggle_shader_editor",
|
||||||
|
"toggle_undo_redo_history",
|
||||||
|
"toggle_command_palette",
|
||||||
|
]
|
||||||
|
for cmd_id in expected_toggles:
|
||||||
|
assert cmd_id in all_ids, f"Missing window toggle command: {cmd_id}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_commands_registry_has_theme_commands():
|
||||||
|
from src.commands import registry
|
||||||
|
all_ids = {c.id for c in registry.all()}
|
||||||
|
assert "switch_to_dark_theme" in all_ids
|
||||||
|
assert "switch_to_light_theme" in all_ids
|
||||||
|
assert "switch_to_nerv_theme" in all_ids
|
||||||
|
assert "cycle_theme" in all_ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_commands_registry_has_layout_commands():
|
||||||
|
from src.commands import registry
|
||||||
|
all_ids = {c.id for c in registry.all()}
|
||||||
|
assert "show_all_panels" in all_ids
|
||||||
|
assert "hide_all_panels" in all_ids
|
||||||
|
assert "save_workspace_profile" in all_ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_commands_registry_has_undo_redo_commands():
|
||||||
|
from src.commands import registry
|
||||||
|
all_ids = {c.id for c in registry.all()}
|
||||||
|
assert "undo" in all_ids
|
||||||
|
assert "redo" in all_ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_commands_have_actions():
|
||||||
|
"""Every registered command must have a callable action."""
|
||||||
|
from src.commands import registry
|
||||||
|
for cmd in registry.all():
|
||||||
|
assert cmd.id, f"Command missing id: {cmd}"
|
||||||
|
assert cmd.title, f"Command {cmd.id} missing title"
|
||||||
|
assert cmd.category, f"Command {cmd.id} missing category"
|
||||||
|
assert cmd.action is not None, f"Command {cmd.id} missing action"
|
||||||
|
assert callable(cmd.action), f"Command {cmd.id} action is not callable"
|
||||||
|
|
||||||
|
|
||||||
|
def test_toggle_helpers_are_safe_with_missing_state():
|
||||||
|
"""The _toggle_window and _toggle_attr helpers must not raise on missing state."""
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
from src.commands import _toggle_window, _toggle_attr
|
||||||
|
|
||||||
|
bare_app = MagicMock(spec=[])
|
||||||
|
_toggle_window(bare_app, "Anything")
|
||||||
|
_toggle_attr(bare_app, "anything")
|
||||||
|
|
||||||
|
partial_app = MagicMock()
|
||||||
|
partial_app.show_windows = {}
|
||||||
|
_toggle_window(partial_app, "NewPanel")
|
||||||
|
assert partial_app.show_windows["NewPanel"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_undo_command_routes_to_handler():
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
from src.commands import registry
|
||||||
|
|
||||||
|
app = MagicMock()
|
||||||
|
app._handle_undo = MagicMock()
|
||||||
|
|
||||||
|
undo_cmd = registry.get("undo")
|
||||||
|
assert undo_cmd is not None
|
||||||
|
undo_cmd.action(app)
|
||||||
|
app._handle_undo.assert_called_once()
|
||||||
|
|||||||
Reference in New Issue
Block a user