from __future__ import annotations import webbrowser from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional from src import theme_2 from src.hot_reloader import HotReloader from src.result_types import ErrorInfo, ErrorKind, Result if TYPE_CHECKING: from src.gui_2 import App # -------------------------------------------------------------------------- # Command data classes + registry (moved from src/command_palette.py in # module_taxonomy_refactor_20260627 Phase 1.3; the *rendering* function # `render_palette_modal` lives in src/gui_2.py because it owns ImGui state) # -------------------------------------------------------------------------- @dataclass class Command: id: str title: str category: str shortcut: Optional[str] = None description: str = "" enabled_when: Optional[str] = None action: Optional[Callable] = None @dataclass class ScoredCommand: command: Command score: float class CommandRegistry: def __init__(self) -> None: self._commands: Dict[str, Command] = {} def register(self, command_or_callable: Any) -> Any: 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) -> Command: return self._commands.get(command_id) or Command(id="", title="", category="uncategorized", action=lambda: None) def fuzzy_match(query: str, candidates: List[Command], top_n: int = 20) -> List[ScoredCommand]: query_lower = query.lower() scored: List[ScoredCommand] = [] for cmd in candidates: title_lower = cmd.title.lower() if not _is_subsequence(query_lower, title_lower): continue score = _compute_score(query_lower, title_lower) scored.append(ScoredCommand(command=cmd, score=score)) scored.sort(key=lambda r: r.score, reverse=True) return scored[:top_n] def _is_subsequence(query: str, target: str) -> bool: qi = 0 for ch in target: if qi < len(query) and ch == query[qi]: qi += 1 return qi == len(query) def _compute_score(query: str, target: str) -> float: score = 0.0 if target.startswith(query): score += 1.0 elif _starts_at_word_boundary(query, target): score += 0.5 if _is_contiguous(query, target): score += 0.3 gaps = _count_gaps(query, target) score -= 0.1 * gaps return score def _starts_at_word_boundary(query: str, target: str) -> bool: if not target.startswith(query): return False return len(query) == 0 or not query[0].isalnum() or len(target) == len(query) or not target[len(query)].isalnum() def _is_contiguous(query: str, target: str) -> bool: return query in target def _count_gaps(query: str, target: str) -> int: qi = 0 gaps = 0 last_match = -1 for ti, ch in enumerate(target): if qi < len(query) and ch == query[qi]: if last_match >= 0 and ti - last_match > 1: gaps += ti - last_match - 1 last_match = ti qi += 1 return gaps def _close_palette(app: Any) -> None: app.show_command_palette = False app._command_palette_query = "" app._command_palette_selected = 0 app._command_palette_focused = False app._command_palette_input_focused = False def _execute(app: Any, command: Command) -> None: if not command.action: return try: command.action(app) except (AttributeError, TypeError, ValueError, OSError) as e: _cmd_err = Result(data=None, errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=f"Action {command.id} raised: {e}", source="command_palette._execute", original=e)]) print(f"[CommandPalette] Action {command.id} raised: {e}") _close_palette(app) # -------------------------------------------------------------------------- # Eager registry (was _LazyCommandRegistry; the lazy pattern is no longer # needed since src/commands.py is a thin data module, not the heavy # command_palette.py that previously pulled in imgui at module load time) # -------------------------------------------------------------------------- _PENDING_REGISTRATIONS: list[Callable] = [] _real_registry: CommandRegistry | None = None def _get_real_registry() -> CommandRegistry: global _real_registry if _real_registry is None: _real_registry = CommandRegistry() for func in _PENDING_REGISTRATIONS: _real_registry.register(func) return _real_registry class _EagerCommandRegistry: """Eager registry proxy. @registry.register queues until first .all/.get, then materializes the real CommandRegistry and replays the queue. """ 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) registry = _EagerCommandRegistry() # -------------------------------------------------------------------------- # 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 (OSError, ValueError, TypeError) as e: _md_err = Result(data=None, errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=f"generate_md_only: {e}", source="commands.generate_md_only", original=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 (OSError, ValueError) as e: _save_err = Result(data=None, errors=[ErrorInfo(kind=ErrorKind.INTERNAL, message=f"save_config: {e}", source="commands.save_all", original=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 the cwd's manualslop_layout.ini so hello_imgui regenerates a fresh dock layout on the next process restart. 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: list[str] = ["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 OSError 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