Private
Public Access
0
0

superpowers- plan for hot reload(minimax)

This commit is contained in:
2026-05-16 01:30:44 -04:00
parent 2c0eddc264
commit 14d45e9dd0
@@ -0,0 +1,623 @@
# 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)