from __future__ import annotations import webbrowser from pathlib import Path from typing import TYPE_CHECKING, Any, Callable from src import models from src import theme_2 from src.module_loader import _require_warmed from src.hot_reloader import HotReloader if TYPE_CHECKING: from src.gui_2 import App # Lazy command registry (startup_speedup_20260606 Phase 5A) # -------------------------------------------------------------------------- # The @registry.register decorator runs at module import time, but we want # to defer the actual CommandRegistry creation (and the underlying # src.command_palette import, ~244ms) until the palette is actually used. # The proxy below makes @registry.register a no-op that just queues the # function; the real CommandRegistry is built lazily on first access to # any other registry attribute (.all, .get, etc.) by gui_2.py or tests. # -------------------------------------------------------------------------- _PENDING_REGISTRATIONS: list[Callable] = [] _real_registry: Any = None class _LazyCommandRegistry: """Proxy that defers CommandRegistry instantiation. Behaves like a CommandRegistry from the caller's perspective: - @registry.register decorates functions by queuing them - .all, .get, etc. trigger real initialization on first access """ def register(self, command_or_callable: Any) -> Any: _PENDING_REGISTRATIONS.append(command_or_callable) return command_or_callable def __getattr__(self, name: str) -> Any: return getattr(_get_real_registry(), name) def _get_real_registry() -> Any: global _real_registry if _real_registry is None: command_palette = _require_warmed("src.command_palette") _real_registry = command_palette.CommandRegistry() for func in _PENDING_REGISTRATIONS: _real_registry.register(func) return _real_registry registry = _LazyCommandRegistry() # -------------------------------------------------------------------------- # 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, 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 def clear_discussion(app: "App") -> None: """Clear Discussion — Clear all entries in the current discussion.""" if hasattr(app, "discussion_history"): app.discussion_history = [] @registry.register def add_all_files_to_context(app: "App") -> None: """Add All Files to Context — Add all tracked files to the context.""" if hasattr(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 def open_project(app: "App") -> None: """Open Project — Open a different project TOML.""" if hasattr(app, "_show_project_picker"): app._show_project_picker() @registry.register def save_project(app: "App") -> None: """Save Project — Save the current project state to TOML.""" if hasattr(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.""" 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: app.save_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 @registry.register def reset_layout(app: "App") -> None: """Reset Layout — Restore the default window layout and clear the stale dock-state cache. Useful when a previously-saved manualslop_layout.ini references window names that no longer exist (e.g. after a refactor renames windows) and the dock space appears empty / non-recoverable via the Windows menu. Sets every show_windows entry to True and deletes manualslop_layout.ini so hello_imgui regenerates a fresh dock layout on the next frame (and the next process shutdown saves the new layout in place of the stale one). The user will need to restart sloppy.py for the dock layout to fully take effect; the show_windows toggles take effect immediately. """ if hasattr(app, "show_windows") and isinstance(app.show_windows, dict): for name in app.show_windows.keys(): app.show_windows[name] = True try: import os layout_paths = [ "manualslop_layout.ini", os.path.join("tests", "artifacts", "live_gui_workspace", "manualslop_layout.ini"), ] for p in layout_paths: if os.path.exists(p): os.remove(p) if hasattr(app, "ai_status"): app.ai_status = f"layout reset: removed {p}" except Exception as e: if hasattr(app, "ai_status"): app.ai_status = f"layout reset partial: {e}" # -------------------------------------------------------------------------- # 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.""" HotReloader.reload("src.gui_2", app) @registry.register 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).""" theme_2.apply("10x Dark") @registry.register def switch_to_light_theme(app: "App") -> None: """Switch to Light Theme (ImGui Light palette).""" theme_2.apply("ImGui Light") @registry.register def switch_to_nerv_theme(app: "App") -> None: """Switch to NERV Theme (Tactical Console aesthetic).""" 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).""" 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.""" 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.""" 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