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()andreload_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— addHotModuleregistration -
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— extractApp._render_xxxtorender_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):
_render_main_interface(lines 769-857)_render_discussion_hub(lines 3235-3245)_render_discussion_panel(lines 3395-3415)_render_discussion_selector(lines 3430-3473)_render_discussion_entries(lines 3247-3259)_render_discussion_entry(lines 3261-3304)_render_files_and_media(lines 2529-2636)_render_files_panel(lines 2638-2716)_render_screenshots_panel(lines 2718-2750)_render_context_composition_panel(lines 2752-2765)_render_ai_settings_hub(lines 1754-1759)_render_provider_panel(lines 2377-2441)_render_persona_selector_panel(lines 2443-2523)_render_agent_tools_panel(lines 1850-1913)_render_rag_panel(lines 1761-1796)_render_system_prompts_panel(lines 1798-1848)_render_preset_manager_content(lines 1915-2002)_render_persona_editor_window(lines 2205-2375)_render_tool_preset_manager_content(lines 2014-2193)_render_operations_hub(lines 3750-3788)_render_tool_calls_panel(lines 3790-3853)_render_comms_history_panel(lines 3855-3971)_render_mma_dashboard(lines 4664-4695)_render_mma_modals(lines 4711-4785)_render_ticket_queue(lines 4365-4478)_render_task_dag_panel(lines 4480-4621)_render_approve_script_modal(lines 4269-4310)_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_interfacefirst 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_reloadand_hot_reload_errorstate -
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_errorstate 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.runmain 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_controlsor_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 fromrender_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 ofrender_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
- Pressing
Ctrl+Alt+Rreloadssrc.gui_2without losing App state — tested via manual trigger + state verification - GUI "Hot Reload" button triggers same reload — tested via button click in live_gui
- Failed reload shows error tint, preserves last-good state — tested via mock failure injection
- New hot modules can be added via
HotReloader.register()without modifying core — tested in unit tests - 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)