# 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