From a280706ce452671e458574e019bf54e6bb3d126d Mon Sep 17 00:00:00 2001 From: Ed_ Date: Tue, 2 Jun 2026 22:54:52 -0400 Subject: [PATCH] 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. --- src/commands.py | 246 ++++++++++++++++++++++++++++++++-- tests/test_command_palette.py | 85 ++++++++++++ 2 files changed, 317 insertions(+), 14 deletions(-) diff --git a/src/commands.py b/src/commands.py index 4ac4fd7d..a2edfb2d 100644 --- a/src/commands.py +++ b/src/commands.py @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Callable from src.command_palette import CommandRegistry if TYPE_CHECKING: @@ -9,13 +9,41 @@ if TYPE_CHECKING: 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 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 ai_client.reset_session() if hasattr(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 @@ -25,13 +53,6 @@ def clear_discussion(app: "App") -> None: 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 def add_all_files_to_context(app: "App") -> None: """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() +@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 def open_project(app: "App") -> None: """Open Project — Open a different project TOML.""" @@ -53,6 +93,137 @@ def save_project(app: "App") -> None: 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 def trigger_hot_reload(app: "App") -> None: """Hot Reload — Reload the GUI module to pick up code changes.""" @@ -61,12 +232,23 @@ def trigger_hot_reload(app: "App") -> None: @registry.register -def show_documentation(app: "App") -> None: - """Show Documentation — Open the documentation index in the browser.""" - import webbrowser - webbrowser.open("https://git.cozyair.dev/ed/manual_slop/") +def undo(app: "App") -> None: + """Undo — Revert the most recent UI mutation.""" + if hasattr(app, "_handle_undo"): + 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 def switch_to_dark_theme(app: "App") -> None: """Switch to Dark Theme (10x Dark palette).""" @@ -83,6 +265,42 @@ def switch_to_light_theme(app: "App") -> None: @registry.register def switch_to_nerv_theme(app: "App") -> None: - """Switch to NERV Theme.""" + """Switch to NERV Theme (Tactical Console aesthetic).""" from src import theme_2 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 diff --git a/tests/test_command_palette.py b/tests/test_command_palette.py index 443a87a1..bd83ceae 100644 --- a/tests/test_command_palette.py +++ b/tests/test_command_palette.py @@ -52,3 +52,88 @@ def test_commands_registry_has_core_commands(): assert "clear_discussion" in all_ids assert "trigger_hot_reload" 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()