diff --git a/docs/superpowers/plans/2026-05-11-imgui-context-managers-plan.md b/docs/superpowers/plans/2026-05-11-imgui-context-managers-plan.md new file mode 100644 index 0000000..b17d7a3 --- /dev/null +++ b/docs/superpowers/plans/2026-05-11-imgui-context-managers-plan.md @@ -0,0 +1,351 @@ +# ImGui Context Manager Suite 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:** Create a context manager suite that pairs `begin`/`end` ImGui calls using Python's `with` statement to reduce scope pairing errors and improve AI legibility. + +**Architecture:** Base `ImGuiScope` class with factory functions returning scope objects. Each scope handles begin on `__enter__` and end on `__exit__`. + +**Tech Stack:** Python 3.11+, imgui-bundle (Dear PyGui) + +--- + +## File Structure + +| Action | Path | +|--------|------| +| Create | `src/imgui_scopes.py` | +| Create | `tests/test_imgui_scopes.py` | + +--- + +## Task 1: Create `src/imgui_scopes.py` + +**Files:** +- Create: `src/imgui_scopes.py` + +- [ ] **Step 1: Write the module** + +```python +from __future__ import annotations +import imgui_bundle + + +class ImGuiScope: + def __init__(self, begin_fn, end_fn, *args, **kwargs): + self._begin_fn = begin_fn + self._end_fn = end_fn + self._args = args + self._kwargs = kwargs + self._opened: bool | tuple = False + self._entered = False + + def __enter__(self): + result = self._begin_fn(*self._args, **self._kwargs) + if isinstance(result, tuple): + self._opened = result[0] + else: + self._opened = result + self._entered = bool(self._opened) + return self._opened + + def __exit__(self, *args): + if self._entered: + self._end_fn() + return False + + +def imgui_window(name: str, visible: bool = True, flags: int = 0): + return ImGuiScope(imgui_bundle.imgui.begin, imgui_bundle.imgui.end, name, visible, flags) + + +def imgui_table(name: str, columns: int, flags: int = 0): + return ImGuiScope(imgui_bundle.imgui.begin_table, imgui_bundle.imgui.end_table, name, columns, flags) + + +def imgui_menu_bar(): + return ImGuiScope(imgui_bundle.imgui.begin_menu_bar, imgui_bundle.imgui.end_menu_bar) + + +def imgui_menu(label: str): + return ImGuiScope(imgui_bundle.imgui.begin_menu, imgui_bundle.imgui.end_menu, label) + + +def imgui_child(id_str: str, width: float = 0, height: float = 0, flags: int = 0): + return ImGuiScope(imgui_bundle.imgui.begin_child, imgui_bundle.imgui.end_child, id_str, width, height, flags) + + +def imgui_group(): + return ImGuiScope(imgui_bundle.imgui.begin_group, imgui_bundle.imgui.end_group) + + +def imgui_popup(id_str: str): + return ImGuiScope(imgui_bundle.imgui.begin_popup, imgui_bundle.imgui.end_popup, id_str) + + +def imgui_tooltip(): + return ImGuiScope(imgui_bundle.imgui.begin_tooltip, imgui_bundle.imgui.end_tooltip) + + +def imgui_clipper(count: int): + return ImGuiScope( + lambda n: imgui_bundle.imgui.listing_builder.begin_clipper(n, -1), + imgui_bundle.imgui.listing_builder.end_clipper, + count + ) + + +def node_editor_scope(name: str): + ed = imgui_bundle.imgui_node_editor + return ImGuiScope(ed.begin, ed.end, name) +``` + +- [ ] **Step 2: Verify syntax** + +Run: `uv run python -c "import ast; ast.parse(open('src/imgui_scopes.py').read())"` +Expected: No output (parse successful) + +- [ ] **Step 3: Commit** + +```bash +git add src/imgui_scopes.py +git commit -m "feat(gui): Add ImGuiScope base class and scope helpers" +``` + +--- + +## Task 2: Create `tests/test_imgui_scopes.py` + +**Files:** +- Create: `tests/test_imgui_scopes.py` +- Depends on: Task 1 + +- [ ] **Step 1: Write tests** + +```python +from __future__ import annotations +from unittest.mock import patch, MagicMock +import pytest + + +class TestImGuiScope: + def test_enter_calls_begin_with_args(self): + from src.imgui_scopes import ImGuiScope + mock_begin = MagicMock(return_value=True) + mock_end = MagicMock() + scope = ImGuiScope(mock_begin, mock_end, "arg1", kwarg="val") + with scope: + pass + mock_begin.assert_called_once_with("arg1", kwarg="val") + + def test_exit_calls_end_when_entered(self): + from src.imgui_scopes import ImGuiScope + mock_begin = MagicMock(return_value=True) + mock_end = MagicMock() + scope = ImGuiScope(mock_begin, mock_end) + with scope: + pass + mock_end.assert_called_once() + + def test_exit_does_not_call_end_when_not_entered(self): + from src.imgui_scopes import ImGuiScope + mock_begin = MagicMock(return_value=False) + mock_end = MagicMock() + scope = ImGuiScope(mock_begin, mock_end) + with scope: + pass + mock_end.assert_not_called() + + def test_enter_returns_opened_value(self): + from src.imgui_scopes import ImGuiScope + mock_begin = MagicMock(return_value=True) + mock_end = MagicMock() + scope = ImGuiScope(mock_begin, mock_end) + result = scope.__enter__() + assert result is True + + def test_enter_handles_tuple_return(self): + from src.imgui_scopes import ImGuiScope + mock_begin = MagicMock(return_value=(True, "extra")) + mock_end = MagicMock() + scope = ImGuiScope(mock_begin, mock_end) + with scope: + pass + mock_end.assert_called_once() + + +class TestImguiWindow: + def test_imgui_window_returns_scope(self): + from src.imgui_scopes import imgui_window + with patch("src.imgui_scopes.imgui_bundle.imgui.begin", return_value=True) as mock_begin: + with patch("src.imgui_scopes.imgui_bundle.imgui.end") as mock_end: + scope = imgui_window("Test") + assert hasattr(scope, "__enter__") + assert hasattr(scope, "__exit__") + + +class TestImguiTable: + def test_imgui_table_returns_scope(self): + from src.imgui_scopes import imgui_table + with patch("src.imgui_scopes.imgui_bundle.imgui.begin_table", return_value=True) as mock_begin: + with patch("src.imgui_scopes.imgui_bundle.imgui.end_table") as mock_end: + scope = imgui_table("TestTable", 3) + with scope: + pass + mock_begin.assert_called_once() + mock_end.assert_called_once() + + +class TestImguiMenuBar: + def test_imgui_menu_bar_no_args(self): + from src.imgui_scopes import imgui_menu_bar + with patch("src.imgui_scopes.imgui_bundle.imgui.begin_menu_bar", return_value=True) as mock_begin: + with patch("src.imgui_scopes.imgui_bundle.imgui.end_menu_bar") as mock_end: + scope = imgui_menu_bar() + with scope: + pass + mock_begin.assert_called_once_with() + mock_end.assert_called_once() + + +class TestImguiMenu: + def test_imgui_menu_returns_scope(self): + from src.imgui_scopes import imgui_menu + with patch("src.imgui_scopes.imgui_bundle.imgui.begin_menu", return_value=True) as mock_begin: + with patch("src.imgui_scopes.imgui_bundle.imgui.end_menu") as mock_end: + scope = imgui_menu("File") + with scope: + pass + mock_begin.assert_called_once() + mock_end.assert_called_once() + + +class TestImguiChild: + def test_imgui_child_returns_scope(self): + from src.imgui_scopes import imgui_child + with patch("src.imgui_scopes.imgui_bundle.imgui.begin_child", return_value=True) as mock_begin: + with patch("src.imgui_scopes.imgui_bundle.imgui.end_child") as mock_end: + scope = imgui_child("child_id", 100, 200) + with scope: + pass + mock_begin.assert_called_once() + mock_end.assert_called_once() + + +class TestImguiGroup: + def test_imgui_group_no_args(self): + from src.imgui_scopes import imgui_group + with patch("src.imgui_scopes.imgui_bundle.imgui.begin_group", return_value=True) as mock_begin: + with patch("src.imgui_scopes.imgui_bundle.imgui.end_group") as mock_end: + scope = imgui_group() + with scope: + pass + mock_begin.assert_called_once() + mock_end.assert_called_once() + + +class TestImguiPopup: + def test_imgui_popup_returns_scope(self): + from src.imgui_scopes import imgui_popup + with patch("src.imgui_scopes.imgui_bundle.imgui.begin_popup", return_value=True) as mock_begin: + with patch("src.imgui_scopes.imgui_bundle.imgui.end_popup") as mock_end: + scope = imgui_popup("popup_id") + with scope: + pass + mock_begin.assert_called_once() + mock_end.assert_called_once() + + +class TestImguiTooltip: + def test_imgui_tooltip_no_args(self): + from src.imgui_scopes import imgui_tooltip + with patch("src.imgui_scopes.imgui_bundle.imgui.begin_tooltip", return_value=True) as mock_begin: + with patch("src.imgui_scopes.imgui_bundle.imgui.end_tooltip") as mock_end: + scope = imgui_tooltip() + with scope: + pass + mock_begin.assert_called_once() + mock_end.assert_called_once() + + +class TestNodeEditorScope: + def test_node_editor_scope_returns_scope(self): + from src.imgui_scopes import node_editor_scope + mock_ed = MagicMock() + mock_ed.begin = MagicMock(return_value=True) + mock_ed.end = MagicMock() + with patch("src.imgui_scopes.imgui_bundle.imgui_node_editor", mock_ed): + scope = node_editor_scope("TestNode") + with scope: + pass + mock_ed.begin.assert_called_once() + mock_ed.end.assert_called_once() +``` + +- [ ] **Step 2: Run tests** + +Run: `uv run pytest tests/test_imgui_scopes.py -v` +Expected: All tests PASS + +- [ ] **Step 3: Commit** + +```bash +git add tests/test_imgui_scopes.py +git commit -m "test(gui): Add tests for ImGuiScope context managers" +``` + +--- + +## Task 3: Integration Verification + +**Files:** +- Modify: `src/gui_2.py` (sample migration of 2-3 windows) +- Depends on: Task 1 + +- [ ] **Step 1: Add import to gui_2.py** + +Add near line 1 (after existing imports): +```python +from src.imgui_scopes import imgui_window, imgui_table, imgui_menu_bar, imgui_menu, imgui_child, imgui_group, imgui_popup, imgui_tooltip, imgui_clipper, node_editor_scope +``` + +- [ ] **Step 2: Migrate one window as proof-of-concept** + +Pick one window in `_gui_func` (e.g., lines 1061-1056 for "AI Settings"): + +Before: +```python +exp, opened = imgui.begin("AI Settings", self.show_windows["AI Settings"]) +if exp: + # ... content ... + imgui.end() +``` + +After: +```python +if imgui_window("AI Settings", self.show_windows["AI Settings"]): + # ... content ... +# end() called automatically +``` + +- [ ] **Step 3: Run smoke test** + +Run: `uv run pytest tests/test_gui_startup_smoke.py -v` +Expected: PASS (no regression from import) + +- [ ] **Step 4: Commit** + +```bash +git add src/gui_2.py +git commit -m "refactor(gui): Migrate AI Settings window to imgui_window scope" +``` + +--- + +## Self-Review Checklist + +- [ ] All scope helpers implemented with correct signatures +- [ ] Tests mock all begin/end pairs +- [ ] Integration test confirms no regression in gui_2.py +- [ ] No placeholders (TBD, TODO) in any step +- [ ] Each task commits independently \ No newline at end of file