Private
Public Access
0
0
Files
manual_slop/docs/superpowers/plans/2026-05-16-hot-reloader-implementation-plan.md
T

21 KiB

Hot Reloader Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Implement selective, state-preserving hot-reload for src/gui_2.py with delegation pattern refactor, manual trigger via Ctrl+Alt+R and GUI button, and visual error tint feedback on failure.

Architecture: Separating code into delegators (thin App wrappers in gui_2.py) and delegation targets (actual render logic as module-level functions). State preservation via copy.deepcopy capture/restore around importlib.reload. Error tint overlay on reload failure using ImGui draw list.

Tech Stack: Python importlib.reload, copy.deepcopy, Dear PyGui / ImGui draw lists, traceback


File Structure

File Responsibility
src/hot_reloader.py New — HotModule dataclass and HotReloader class with registry, capture/restore, reload API
src/gui_2.py Refactor render methods from App._render_xxx to module-level render_xxx(app)
src/app_controller.py Delegation wrappers — App._render_xxx becomes import src.gui_2 as g2; g2.render_xxx(self)
tests/test_hot_reloader.py Unit tests for HotModule, HotReloader registry and reload
tests/test_hot_reload_integration.py Integration tests via live_gui fixture

Task 1: Create src/hot_reloader.py — HotModule Dataclass and HotReloader Core

Files:

  • Create: src/hot_reloader.py

  • Test: tests/test_hot_reloader.py

  • Step 1: Write failing test for HotModule dataclass

# tests/test_hot_reloader.py
import pytest
from dataclasses import dataclass
from src.hot_reloader import HotModule, HotReloader

def test_hot_module_dataclass_fields():
    hm = HotModule(
        name="test_module",
        file_path="/path/to/test_module.py",
        state_keys=["attr1", "attr2"],
        delegation_targets=["method_a", "method_b"],
    )
    assert hm.name == "test_module"
    assert hm.file_path == "/path/to/test_module.py"
    assert hm.state_keys == ["attr1", "attr2"]
    assert hm.delegation_targets == ["method_a", "method_b"]

def test_hot_reloader_register_and_get():
    HotReloader.HOT_MODULES.clear()
    hm = HotModule(name="test_mod", file_path="/fake/path.py", state_keys=[], delegation_targets=[])
    HotReloader.register(hm)
    assert "test_mod" in HotReloader.HOT_MODULES
    assert HotReloader.HOT_MODULES["test_mod"] is hm

def test_hot_reloader_register_duplicate_raises():
    HotReloader.HOT_MODULES.clear()
    hm1 = HotModule(name="dup", file_path="/a.py", state_keys=[], delegation_targets=[])
    HotReloader.register(hm1)
    with pytest.raises(ValueError, match="already registered"):
        HotReloader.register(hm1)

def test_hot_reloader_is_error_state():
    HotReloader.HOT_MODULES.clear()
    HotReloader.last_error = None
    HotReloader.is_error_state = False
    assert HotReloader.is_error_state is False
  • Step 2: Run test to verify it fails

Run: uv run pytest tests/test_hot_reloader.py -v Expected: FAIL — src/hot_reloader.py does not exist

  • Step 3: Write minimal HotModule dataclass and HotReloader skeleton
# src/hot_reloader.py
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
import copy
import traceback

@dataclass
class HotModule:
    name: str
    file_path: str
    state_keys: list[str] = field(default_factory=list)
    delegation_targets: list[str] = field(default_factory=list)

class HotReloader:
    HOT_MODULES: dict[str, HotModule] = {}
    last_error: str | None = None
    is_error_state: bool = False

    @classmethod
    def register(cls, module: HotModule) -> None:
        if module.name in cls.HOT_MODULES:
            raise ValueError(f"Module {module.name} already registered")
        cls.HOT_MODULES[module.name] = module

    @classmethod
    def capture_state(cls, app: Any, state_keys: list[str]) -> dict[str, Any]:
        return {key: copy.deepcopy(getattr(app, key, None)) for key in state_keys if hasattr(app, key)}

    @classmethod
    def restore_state(cls, app: Any, state: dict[str, Any]) -> None:
        for key, value in state.items():
            setattr(app, key, value)
  • Step 4: Run test to verify it passes

Run: uv run pytest tests/test_hot_reloader.py -v Expected: PASS

  • Step 5: Commit
git add src/hot_reloader.py tests/test_hot_reloader.py
git commit -m "feat(hot-reload): Add HotModule dataclass and HotReloader registry"

Task 2: Implement HotReloader.reload() and HotReloader.reload_all()

Files:

  • Modify: src/hot_reloader.py

  • Test: tests/test_hot_reloader.py

  • Step 1: Write failing test for reload method

# tests/test_hot_reloader.py (add these tests)
import importlib
from unittest.mock import MagicMock, patch

def test_reload_unknown_module_returns_false():
    HotReloader.HOT_MODULES.clear()
    HotReloader.register(HotModule(name="nonexistent_mod", file_path="/nonexistent.py", state_keys=[], delegation_targets=[]))
    app = MagicMock()
    result = HotReloader.reload("nonexistent_mod", app)
    assert result is False
    assert HotReloader.is_error_state is True
    assert HotReloader.last_error is not None

def test_reload_success_clears_error_state():
    HotReloader.HOT_MODULES.clear()
    # Create a minimal test module that can be reloaded
    import sys
    import types
    test_mod = types.ModuleType("src._test_reload_mod")
    sys.modules["src._test_reload_mod"] = test_mod
    HotReloader.register(HotModule(name="src._test_reload_mod", file_path=str(Path(__file__).parent.parent / "src" / "_test_reload_mod.py"), state_keys=[], delegation_targets=[]))
    app = MagicMock()
    result = HotReloader.reload("src._test_reload_mod", app)
    assert result is True
    assert HotReloader.is_error_state is False
    assert HotReloader.last_error is None
    del sys.modules["src._test_reload_mod"]

def test_reload_captures_and_restores_state_on_failure():
    HotReloader.HOT_MODULES.clear()
    HotReloader.register(HotModule(name="bad_mod", file_path="/bad.py", state_keys=["_test_attr"], delegation_targets=[]))
    app = MagicMock()
    app._test_attr = "preserved_value"
    result = HotReloader.reload("bad_mod", app)
    assert result is False
    assert HotReloader.is_error_state is True
    # State should be restored even on failure

def test_reload_all():
    HotReloader.HOT_MODULES.clear()
    # Register two modules and verify reload_all calls both
  • Step 2: Run test to verify it fails

Run: uv run pytest tests/test_hot_reloader.py -v Expected: FAIL — reload method not implemented

  • Step 3: Write reload() and reload_all() implementations
# src/hot_reloader.py (add after existing methods)
@classmethod
def reload(cls, module_name: str, app: Any) -> bool:
    if module_name not in cls.HOT_MODULES:
        cls.last_error = f"Module {module_name} not registered"
        cls.is_error_state = True
        return False

    hm = cls.HOT_MODULES[module_name]
    state = cls.capture_state(app, hm.state_keys)

    try:
        import importlib
        import sys
        if module_name in sys.modules:
            old_module = sys.modules[module_name]
            importlib.reload(old_module)
        else:
            importlib.import_module(module_name)
        cls.last_error = None
        cls.is_error_state = False
        return True
    except Exception:
        cls.restore_state(app, state)
        cls.last_error = traceback.format_exc()
        cls.is_error_state = True
        return False

@classmethod
def reload_all(cls, app: Any) -> bool:
    success = True
    for name in cls.HOT_MODULES:
        if not cls.reload(name, app):
            success = False
    return success
  • Step 4: Run test to verify it passes

Run: uv run pytest tests/test_hot_reloader.py -v Expected: PASS

  • Step 5: Commit
git add src/hot_reloader.py tests/test_hot_reloader.py
git commit -m "feat(hot-reload): Implement HotReloader.reload and reload_all"

Task 3: Register src.gui_2 with state_keys and delegation_targets

Files:

  • Modify: src/app_controller.py — add HotModule registration

  • Test: tests/test_hot_reloader.py

  • Step 1: Write test for src.gui_2 registration

def test_gui_2_module_registered():
    from src.hot_reloader import HotReloader
    assert "src.gui_2" in HotReloader.HOT_MODULES
    hm = HotReloader.HOT_MODULES["src.gui_2"]
    assert len(hm.state_keys) > 0
    assert len(hm.delegation_targets) > 0
    assert "_render_main_interface" in hm.delegation_targets
  • Step 2: Run test to verify it fails

Run: uv run pytest tests/test_hot_reloader.py::test_gui_2_module_registered -v Expected: FAIL — registration not yet added

  • Step 3: Add src.gui_2 HotModule registration in app_controller.py

Find the end of AppController.__init__ or add a _register_hot_modules() function called at end of __init__:

# In src/app_controller.py, near end of AppController.__init__ or as standalone:
from src.hot_reloader import HotReloader, HotModule
from pathlib import Path

def _register_hot_modules() -> None:
    HotReloader.register(HotModule(
        name="src.gui_2",
        file_path=str(Path(__file__).parent / "gui_2.py"),
        state_keys=[
            "_active_discussion", "_disc_entries", "_disc_roles",
            "show_windows", "ui_discussion_split_h", "active_tickets",
            "show_preset_manager_window", "show_tool_preset_manager_window",
            "show_persona_editor_window", "_pending_patch",
            "_tool_log", "_comms_log",
        ],
        delegation_targets=[
            "_render_main_interface", "_render_discussion_hub",
            "_render_discussion_panel", "_render_discussion_selector",
            "_render_discussion_entries", "_render_discussion_entry",
            "_render_files_and_media", "_render_files_panel",
            "_render_screenshots_panel", "_render_context_composition_panel",
            "_render_ai_settings_hub", "_render_provider_panel",
            "_render_persona_selector_panel", "_render_agent_tools_panel",
            "_render_rag_panel", "_render_system_prompts_panel",
            "_render_preset_manager_content", "_render_persona_editor_window",
            "_render_tool_preset_manager_content",
            "_render_operations_hub", "_render_tool_calls_panel",
            "_render_comms_history_panel", "_render_mma_dashboard",
            "_render_mma_modals", "_render_ticket_queue",
            "_render_task_dag_panel", "_render_approve_script_modal",
            "_render_patch_modal",
        ],
    ))

Call _register_hot_modules() at the end of AppController.__init__ after all state is initialized.

  • Step 4: Run test to verify it passes

Run: uv run pytest tests/test_hot_reloader.py::test_gui_2_module_registered -v Expected: PASS

  • Step 5: Commit
git add src/app_controller.py
git commit -m "feat(hot-reload): Register src.gui_2 with state_keys and delegation_targets"

Task 4: Phase 1 — Refactor gui_2 render methods to module-level functions

Files:

  • Modify: src/gui_2.py — extract App._render_xxx to render_xxx(app)
  • Modify: src/app_controller.py — delegation wrappers
  • Test: tests/test_hot_reloader.py

Refactor Pattern (for each render method):

# BEFORE (in App class):
def _render_main_interface(self) -> None:
    if self.perf_profiling_enabled:
        self.perf_monitor.start_component("_render_main_interface")
    with imscope.window("Main Interface", self.show_windows.get("main_interface", True)) as (exp, opened):
        if not exp: return
        # ... all render logic

# AFTER — App method becomes thin delegation wrapper:
def _render_main_interface(self) -> None:
    import src.gui_2 as g2
    g2.render_main_interface(self)

# NEW module-level function:
def render_main_interface(app: App) -> None:
    if app.perf_profiling_enabled:
        app.perf_monitor.start_component("_render_main_interface")
    with imscope.window("Main Interface", app.show_windows.get("main_interface", True)) as (exp, opened):
        if not exp: return
        # ... same render logic, replacing self with app

Methods to refactor (in order):

  1. _render_main_interface (lines 769-857)
  2. _render_discussion_hub (lines 3235-3245)
  3. _render_discussion_panel (lines 3395-3415)
  4. _render_discussion_selector (lines 3430-3473)
  5. _render_discussion_entries (lines 3247-3259)
  6. _render_discussion_entry (lines 3261-3304)
  7. _render_files_and_media (lines 2529-2636)
  8. _render_files_panel (lines 2638-2716)
  9. _render_screenshots_panel (lines 2718-2750)
  10. _render_context_composition_panel (lines 2752-2765)
  11. _render_ai_settings_hub (lines 1754-1759)
  12. _render_provider_panel (lines 2377-2441)
  13. _render_persona_selector_panel (lines 2443-2523)
  14. _render_agent_tools_panel (lines 1850-1913)
  15. _render_rag_panel (lines 1761-1796)
  16. _render_system_prompts_panel (lines 1798-1848)
  17. _render_preset_manager_content (lines 1915-2002)
  18. _render_persona_editor_window (lines 2205-2375)
  19. _render_tool_preset_manager_content (lines 2014-2193)
  20. _render_operations_hub (lines 3750-3788)
  21. _render_tool_calls_panel (lines 3790-3853)
  22. _render_comms_history_panel (lines 3855-3971)
  23. _render_mma_dashboard (lines 4664-4695)
  24. _render_mma_modals (lines 4711-4785)
  25. _render_ticket_queue (lines 4365-4478)
  26. _render_task_dag_panel (lines 4480-4621)
  27. _render_approve_script_modal (lines 4269-4310)
  28. _render_patch_modal (lines 4135-4177)
  • Step 1: Write test for module-level function pattern
def test_gui2_has_module_level_render_functions():
    import src.gui_2 as g2
    # After refactor, these should exist as module-level functions
    assert hasattr(g2, 'render_main_interface')
    assert hasattr(g2, 'render_discussion_hub')
    assert hasattr(g2, 'render_discussion_panel')
    # ... etc
    # Each should accept app: App as first parameter
    import inspect
    sig = inspect.signature(g2.render_main_interface)
    assert 'app' in sig.parameters
  • Step 2: Run test to verify it fails

Run: uv run pytest tests/test_hot_reloader.py::test_gui2_has_module_level_render_functions -v Expected: FAIL — module-level functions don't exist yet

  • Step 3: Refactor _render_main_interface first as pilot

Extract the method body, replace self with app, create module-level render_main_interface(app: App) -> None, and convert App._render_main_interface to delegation wrapper.

  • Step 4: Run test after each method refactor

Run: uv run pytest tests/test_hot_reloader.py::test_gui2_has_module_level_render_functions -v Continue until all module-level functions exist.

  • Step 5: Commit after completing all methods
git add src/gui_2.py src/app_controller.py
git commit -m "refactor(hot-reload): Extract gui_2 render methods to module-level functions"

Task 5: Add Ctrl+Alt+R trigger and GUI Hot Reload button

Files:

  • Modify: src/gui_2.py:App.run — capture keyboard shortcut

  • Modify: src/gui_2.py:_render_mma_dashboard — add Hot Reload button

  • Modify: src/gui_2.py — add _trigger_hot_reload and _hot_reload_error state

  • Test: tests/test_hot_reload_integration.py

  • Step 1: Write test for hot reload trigger

def test_ctrl_alt_r_triggers_hot_reload(live_gui):
    client = ApiHookClient(port=8999)
    # Verify initial state
    initial_state = client.get_value("_hot_reload_error")
    # Note: Direct keyboard simulation via hooks not yet implemented
    # Test will verify button-based trigger
  • Step 2: Write test for Hot Reload button in MMA dashboard
def test_hot_reload_button_in_mma_dashboard(live_gui):
    client = ApiHookClient(port=8999)
    # Verify _render_mma_dashboard has a Hot Reload button
    # via exposed component state or via behavior
  • Step 3: Add _hot_reload_error state to App.init

In src/gui_2.py:App.__init__, add:

self._hot_reload_error = False
  • Step 4: Add _trigger_hot_reload() method to App
def _trigger_hot_reload(self) -> None:
    from src.hot_reloader import HotReloader
    success = HotReloader.reload_all(self)
    self._hot_reload_error = not success

def _render_hot_reload_button(self) -> None:
    imgui.push_style_color(imgui.Col_.button, imgui.get_color_u32_rgba(0.2, 0.2, 0.2, 1.0))
    if imgui.button("Hot Reload"):
        self._trigger_hot_reload()
    imgui.pop_style_color()
    if self._hot_reload_error:
        imgui.same_line()
        imgui.text_colored(imgui.get_color_u32_rgba(1.0, 0.3, 0.2, 1.0), "Reload failed")
  • Step 5: Add keyboard capture in App.run main loop

In src/gui_2.py:App.run, inside the main while running: loop:

# Check for Ctrl+Alt+R
io = imgui.get_io()
if io.key_ctrl and io.key_alt and imgui.is_key_down(imgui.KEY_R):
    self._trigger_hot_reload()
  • Step 6: Integrate button into _render_mma_global_controls or _render_mma_dashboard

Add self._render_hot_reload_button() call in _render_mma_dashboard or a nearby section.

  • Step 7: Run tests

Run: uv run pytest tests/test_hot_reload_integration.py -v --timeout=60 Expected: PASS

  • Step 8: Commit
git add src/gui_2.py src/app_controller.py
git commit -m "feat(hot-reload): Add Ctrl+Alt+R trigger and GUI Hot Reload button"

Task 6: Implement visual error tint on reload failure

Files:

  • Modify: src/gui_2.py — add _render_error_tint() and call from render_main_interface

  • Test: tests/test_hot_reload_integration.py

  • Step 1: Write test for error tint overlay

def test_error_tint_renders_on_failure(live_gui):
    # Inject a failure state and verify tint appears
    pass
  • Step 2: Add _render_error_tint(app: App) function
def _render_error_tint(app: App) -> None:
    if not getattr(app, '_hot_reload_error', False):
        return
    # NERV theme error tint
    tint_color = imgui.get_color_u32_rgba(1.0, 0.28, 0.25, 0.15)
    draw_list = imgui.get_background_draw_list()
    viewport = imgui.get_main_viewport()
    draw_list.add_rect_filled(
        viewport.pos,
        viewport.pos + viewport.size,
        tint_color,
    )
  • Step 3: Call _render_error_tint(app) at end of render_main_interface

Add at the very start of render_main_interface since it's an overlay:

def render_main_interface(app: App) -> None:
    _render_error_tint(app)
    if app.perf_profiling_enabled:
        # ...
  • Step 4: Run test

Run: uv run pytest tests/test_hot_reload_integration.py -v --timeout=60 Expected: PASS

  • Step 5: Commit
git add src/gui_2.py
git commit -m "feat(hot-reload): Add visual error tint on reload failure"

Task 7: End-to-End Integration Test

Files:

  • Create: tests/test_hot_reload_integration.py

  • Test: tests/test_hot_reload_integration.py

  • Step 1: Write e2e tests using live_gui fixture

import pytest
from api_hook_client import ApiHookClient
from simulation.sim_base import BaseSimulation, run_sim

class HotReloadSimulation(BaseSimulation):
    def run(self) -> None:
        # 1. Verify hot reload button is accessible
        # 2. Trigger hot reload via API hook
        # 3. Verify GUI still renders after reload
        # 4. Verify error tint clears on success
        pass

def test_hot_reload_e2e(live_gui):
    client = ApiHookClient(port=8999)
    # Trigger hot reload
    response = client.post("/api/gui", {"action": "hot_reload"})
    # Verify GUI still responds
    state = client.get("/api/gui_state")
    assert state is not None

def test_hot_reload_preserves_state(live_gui):
    client = ApiHookClient(port=8999)
    # Set some state
    # Trigger reload
    # Verify state preserved
  • Step 2: Run e2e tests

Run: uv run pytest tests/test_hot_reload_integration.py -v --timeout=120 Expected: PASS

  • Step 3: Commit
git add tests/test_hot_reload_integration.py
git commit -m "test(hot-reload): Add e2e integration test"

Success Criteria Verification

  1. Pressing Ctrl+Alt+R reloads src.gui_2 without losing App state — tested via manual trigger + state verification
  2. GUI "Hot Reload" button triggers same reload — tested via button click in live_gui
  3. Failed reload shows error tint, preserves last-good state — tested via mock failure injection
  4. New hot modules can be added via HotReloader.register() without modifying core — tested in unit tests
  5. No performance impact when not reloading (< 1ms overhead per frame) — verified via perf profiling

Out of Scope (Per Spec)

  • Automatic file watching (manual trigger only)
  • Hot-reloading of C extensions or native code
  • Cross-platform reload support (Windows focus)