# 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** ```python # 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** ```python # 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** ```bash 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** ```python # 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** ```python # 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** ```bash 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** ```python 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__`: ```python # 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** ```bash 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): ```python # 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** ```python 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** ```bash 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** ```python 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** ```python 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: ```python self._hot_reload_error = False ``` - [ ] **Step 4: Add `_trigger_hot_reload()` method to App** ```python 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: ```python # 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** ```bash 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** ```python 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** ```python 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: ```python 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** ```bash 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** ```python 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** ```bash 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)