Private
Public Access
0
0
Files
manual_slop/src/commands.py
T
ed 7bcb5a8c07 refactor(config): Route all config I/O through AppController
Eliminates 22 call sites that bypassed the AppController state owner
and read/wrote config.toml directly. AppController is now the single
source of truth for self.config; gui_2.py, commands.py, etc. go
through controller.save_config() / controller.load_config().

Production changes:
- src/models.py: rename load_config -> _load_config_from_disk,
  save_config -> _save_config_to_disk (private I/O primitives)
- src/app_controller.py: add public load_config()/save_config() methods
  that own the state. Update 3 internal call sites and 3 ConductorEngine
  call sites to pass max_workers from self.config
- src/multi_agent_conductor.py: ConductorEngine.__init__ now takes
  max_workers as a parameter (caller responsibility, not I/O primitive)
- src/external_editor.py: get_default_launcher() takes config as a
  parameter; gui_2.py:1311,4776 pass app.config
- src/gui_2.py: 17 sites of models.save_config(X.config) replaced with
  X.save_config() (delegates via __getattr__ to controller)
- src/commands.py: save_all() uses app.save_config()

Test changes (route through controller, not I/O primitive):
- tests/conftest.py: mock_app and app_instance fixtures now patch
  AppController.load_config/save_config instead of models I/O primitives
- 18 other test files: patches renamed from models._save_config_to_disk
  to AppController.save_config (and same for load_config)
- tests/test_app_controller_mcp.py: use SLOP_CONFIG env var instead of
  patching removed CONFIG_PATH module constant
- tests/test_parallel_execution.py: pass max_workers=2 explicitly to
  ConductorEngine (caller no longer reads config)
- tests/test_gui_paths.py: add save_config=MagicMock() to MockApp;
  assert on controller method, not I/O primitive
- tests/test_models_no_top_level_tomli_w.py: still calls private
  _save_config_to_disk directly (the only allowed exception; tests
  the lazy-load behavior of the primitive itself)

New files:
- scripts/audit_no_models_config_io.py: enforces the rule (--strict,
  --json modes; AST-based docstring detection to avoid false positives)
- conductor/code_styleguides/config_state_owner.md: documents the rule

Verification:
- 67 targeted tests pass
- scripts/audit_no_models_config_io.py --strict returns 0

This is the architectural cleanup that surfaced during the
audit_architectural_cheats_20260607 review. Closes the smoke-gun
CONFIG_PATH module constant (already done in 0c7ebf22) AND the
free-function models.load_config/save_config smell.

[conductor(checkpoint): config-iO-refactor-20260607]
2026-06-07 19:54:17 -04:00

371 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
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