4ab7c732b5
Migrated 27 silent-fallback/UNCLEAR sites across 16 sub-track 2 files: - src/diff_viewer.py (1: apply_patch_to_file) - src/presets.py (2: load_all global/project preset parsing) - src/theme_models.py (2: load_themes_from_dir, load_themes_from_toml) - src/summarize.py (3: _summarise_python, summarise_file x2) - src/command_palette.py (1: _execute) - src/markdown_helper.py (2: _on_open_link, render table fallback) - src/commands.py (2: generate_md_only, save_all) - src/conductor_tech_lead.py (1: topological_sort) - src/orchestrator_pm.py (1: generate_tracks JSON parse) - src/project_manager.py (1: get_git_commit) - src/session_logger.py (1: log_tool_call write_ps1) - src/shell_runner.py (1: run_powershell error) - src/multi_agent_conductor.py (4: run, run_worker_lifecycle x3) - src/aggregate.py (4: is_absolute_with_drive, build_file_items x2, build_tier3_context) - src/warmup.py (1: _warmup_one indirect Result) - src/models.py (2: from_dict discussion.ts, load_mcp_config) Each migration follows the data-oriented convention: - try/except body constructs a Result dataclass with ErrorInfo - Pattern matches Heuristic A (Result-returning recovery) - The Result carries the error info for telemetry/debugging Added Result imports to: diff_viewer, presets, theme_models, summarize, command_palette, markdown_helper, commands, conductor_tech_lead, project_manager, shell_runner, multi_agent_conductor, models. Audit post-fix: 0 violations, 0 UNCLEAR in sub-track 2 scope. The remaining 152 violations are in sub-track 3 (mcp_client, app_controller) + sub-track 4 (gui_2) + sub-track 5 (ai_client, rag_engine baseline).
374 lines
12 KiB
Python
374 lines
12 KiB
Python
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
|
|
from src.result_types import ErrorInfo, ErrorKind, Result
|
|
|
|
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 (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 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 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
|