diff --git a/docs/guide_gui_2.md b/docs/guide_gui_2.md new file mode 100644 index 00000000..094d931b --- /dev/null +++ b/docs/guide_gui_2.md @@ -0,0 +1,397 @@ +# `src/gui_2.py` — Main ImGui Application + +[Top](../README.md) | [Architecture](guide_architecture.md) | [Testing](guide_testing.md) + +--- + +## Overview + +`src/gui_2.py` is the **largest file** in the project (~260KB, ~5400 lines). It contains the `App` class — the main ImGui application orchestrator — and ~90 module-level render functions that draw the GUI. + +The file is divided into: + +- **Module-level render functions** (~90 functions): pure `def render_xxx(app: App) -> None` functions that draw individual panels +- **`App` class** (~1500 lines): the main application with state, lifecycle, and the per-frame render loop in `_gui_func` + +--- + +## Architecture + +The file follows the project's **UI delegation pattern** (see [guide_architecture.md](guide_architecture.md)): + +- Module-level functions do the drawing. They take `app: App` as their first parameter. +- The `App` class holds state and dispatches. +- Thin wrapper methods on `App` (e.g., `_render_message_panel(self)`) just call the module-level function. + +This pattern enables: +- **Hot Reload**: module-level functions can be reloaded via `importlib.reload` without breaking the App class +- **Testability**: pure functions can be tested in isolation with a mock `App` +- **Consistency**: every render function follows the same calling convention + +``` +┌────────────────────────────────────────────────────┐ +│ _gui_func (App method, ~50 lines) │ +│ - Detects Ctrl+Shift+P, Ctrl+Alt+R, etc. │ +│ - Renders background shader │ +│ - Renders custom title bar │ +│ - Calls render_main_interface(self) ← thin wrapper │ +└──────────────────┬─────────────────────────────────┘ + │ calls + ▼ +┌────────────────────────────────────────────────────┐ +│ render_main_interface(app) │ +│ - Iterates over the major panels │ +│ - Calls render_(app) for each hub │ +│ - Handles modal popups │ +└──────────────────┬─────────────────────────────────┘ + │ calls + ▼ +┌────────────────────────────────────────────────────┐ +│ render_(app) (~80 functions) │ +│ - Each draws one panel (Context, AI, Discussion, │ +│ Operations, MMA Dashboard) │ +│ - Reads from app state, writes to app state │ +│ - Returns nothing; mutates via side effects │ +└────────────────────────────────────────────────────┘ +``` + +--- + +## The `App` Class + +### `App.__init__` + +The constructor (line 131) is long. It: + +1. Creates the `AppController` (the headless counterpart) +2. Initializes `history.HistoryManager(max_capacity=100)` for undo/redo +3. Sets up UI state flags: `show_windows`, `show_*_modal` flags +4. Initializes the workspace manager +5. Starts services via `controller.start_services(self)` +6. Registers predefined callbacks on the controller for the Hook API: + ```python + self.controller._predefined_callbacks['save_context_preset'] = self.save_context_preset + self.controller._predefined_callbacks['_toggle_command_palette'] = self._toggle_command_palette + # ... ~10 more + ``` +7. Registers gettable fields for the Hook API: + ```python + self.controller._gettable_fields['show_command_palette'] = 'show_command_palette' + ``` + +### Key State + +The App holds dozens of state attributes. The most important: + +| Attribute | Type | Purpose | +|---|---|---| +| `self.controller` | `AppController` | Headless counterpart; services and callbacks | +| `self.disc_entries` | `list[dict]` | Current discussion entries | +| `self.disc_roles` | `list[str]` | Roles per entry (User, AI, System, etc.) | +| `self.discussion_history` | `list` | Legacy alias for disc_entries | +| `self.context_files` | `list[FileItem]` | Files in the current context | +| `self.ui_file_paths` | `list[str]` | Tracked file paths | +| `self.ai_status` | `str` | Current AI state ("idle", "sending...", etc.) | +| `self.ai_response` | `str` | Latest AI response text | +| `self.last_md` | `str` | Last generated markdown (for MD-only mode) | +| `self.show_windows` | `dict[str, bool]` | Toggle state for each window | +| `self.workspace_profiles` | `dict` | Loaded workspace profiles | +| `self._comms_log` | `deque` | Communication log (in-memory) | +| `self._tool_log` | `deque` | Tool call log | +| `show_command_palette` | `bool` | Command palette visibility | + +### Render Entry Point: `_gui_func` (line 754) + +The main render loop, called by imgui-bundle's Hello ImGui runner every frame: + +```python +def _gui_func(self) -> None: + io = imgui.get_io() + + # Keyboard shortcuts + if io.key_ctrl and io.key_alt and imgui.is_key_down(imgui.Key.r): + self._trigger_hot_reload() + if (io.key_ctrl and io.key_shift + and not io.key_alt and not io.key_super + and imgui.is_key_pressed(imgui.Key.p)): + self.show_command_palette = not self.show_command_palette + # Reset per-open state... + + # Render background shader (if enabled) + render_custom_title_bar(self) + render_shader_live_editor(self) + render_history_window(self) + # ... background rendering ... + + # Main content + render_main_interface(self) + + # Error tint on hot-reload failure + render_error_tint(self) +``` + +### `render_main_interface` (line 1259) + +The "main content" renderer. Iterates over the major panels and calls the right render function for each: + +```python +def render_main_interface(app: App) -> None: + # Background panels (always rendered if shown) + render_history_window(app) + render_track_proposal_modal(app) + render_patch_modal(app) + # ... all the modals ... + + # Hubs + render_project_settings_hub(app) + render_ai_settings_hub(app) + render_files_and_media(app) + render_discussion_hub(app) + render_operations_hub(app) + render_mma_dashboard(app) + # ... etc +``` + +### Modal Helpers + +- **`_render_window_if_open(name, render_func, flag_condition=True)`** (line 800): helper that renders a window only if its toggle is active. Uses `imscope.window` context manager. +- **`imscope`** from `src/imgui_scopes.py`: stack-style context managers for `imgui.begin/end`, `imgui.push_style/pop_style`, etc. — replaces the legacy push/pop pattern with Pythonic `with` statements. + +--- + +## Keyboard Shortcuts + +Implemented in `_gui_func`: + +| Shortcut | Action | +|---|---| +| `Ctrl+Alt+R` | Hot Reload the GUI module | +| `Ctrl+Shift+P` | Toggle Command Palette | +| `Ctrl+L` | Clear AI input field | +| `Ctrl+Enter` | Generate + Send | +| `Escape` (in modals) | Close the modal | + +Other shortcuts are handled in individual render functions (e.g., `Ctrl+S` in the project settings). + +--- + +## Key Patterns + +### Module-Level Render Functions + +Every render function follows this signature: + +```python +def render_(app: App) -> None: + """One-line docstring describing what this draws.""" + if app.perf_profiling_enabled: + app.perf_monitor.start_component("") + # ... draw ImGui widgets, reading from and writing to app state ... + if app.perf_profiling_enabled: + app.perf_monitor.end_component("") +``` + +- Takes `app: App` as the only positional arg +- Returns `None` +- Defensive: checks `hasattr(app, "...")` before touching state, in case the state was never initialized + +### Custom Title Bar + +`render_custom_title_bar` (line 1358) draws a custom ImGui-drawn title bar (instead of the OS title bar) for the cross-platform PyOpenGL-backed window. The Windows-specific close/min/max/close buttons are added via ctypes in the same function (lines 858-865). + +### Hot Reload Hook + +The Hot Reload module (`src/hot_reloader.py`) registers `src.gui_2` as a hot-reloadable module. The `state_keys` list (line 155) tells the reloader which App attributes to snapshot and restore: + +```python +state_keys=['active_discussion', 'show_windows', 'ui_file_paths', + 'ui_screenshot_paths', 'disc_entries', 'disc_roles'] +``` + +`delegation_targets` (line 156) lists the module-level functions the App calls into: +```python +delegation_targets=['_render_main_interface', '_render_discussion_hub', + '_render_files_and_media', '_render_ai_settings_hub', + '_render_operations_hub', '_render_mma_dashboard'] +``` + +The user presses `Ctrl+Alt+R` → `_trigger_hot_reload()` → `HotReloader.reload("src.gui_2", app)`. The module is re-imported, the App's state is restored, and the next frame uses the new render functions. + +### Snapshots (Undo/Redo) + +`App._take_snapshot` and `App._apply_snapshot` (lines 548, 567) capture and restore UI state for the undo/redo system. Used by the discussion view's edit-in-place operations. + +Snapshots include: `ai_input`, `project_system_prompt`, `global_system_prompt`, `base_system_prompt`, `use_default_base_prompt`, `temperature`, `top_p`, `max_tokens`, `auto_add_history`, `disc_entries`, `files`, `screenshots`. + +--- + +## Window State Management + +The `app.show_windows: dict[str, bool]` is the central toggle for all toggleable windows: + +```python +self.show_windows.setdefault("Text Viewer", False) +self.show_windows.setdefault("Diagnostics", False) +self.show_windows.setdefault("Usage Analytics", False) +self.show_windows.setdefault("Context Preview", False) +self.show_windows.setdefault("Tier 1: Strategy", False) +self.show_windows.setdefault("Tier 2: Tech Lead", False) +self.show_windows.setdefault("Tier 3: Workers", False) +self.show_windows.setdefault("Tier 4: QA", False) +self.show_windows.setdefault('External Tools', False) +self.show_windows.setdefault('Shader Editor', False) +self.show_windows.setdefault('Undo/Redo History', False) +``` + +The Command Palette exposes all of these via the `toggle_*` commands (32 total in `src/commands.py`). + +--- + +## Modal Pattern + +Modals use `imgui.begin_popup_modal` or the `if imgui.begin(...): ... imgui.end()` pattern. The codebase has moved toward using `imscope.window` (line 800) for cleaner scoping: + +```python +def _render_window_if_open(self, name, render_func, flag_condition=True): + if not flag_condition or not self.show_windows.get(name, False): + return + with imscope.window(name, self.show_windows[name]) as (exp, opened): + if not opened: + self.show_windows[name] = False + if exp: + render_func(self) +``` + +Modals that need explicit state (e.g., "command palette" or "approve script") use `app.show__modal: bool` directly. + +--- + +## Public Methods Worth Knowing + +| Method | Line | Purpose | +|---|---|---| +| `__init__` | 131 | Construct the App and all subsystems | +| `run` | ~1252 | Launch the ImGui render loop | +| `reset_session` | ~500 | Clear the AI session and discussion | +| `save_context_preset` / `load_context_preset` | varies | Preset CRUD | +| `_handle_generate_send` | 492 | The "Generate + Send" button handler | +| `_handle_md_only` | 499 | The "Generate MD Only" button handler | +| `_take_snapshot` / `_apply_snapshot` | 548 / 567 | Undo/redo snapshot system | +| `_capture_workspace_profile` | 602 | Capture the current layout as a profile | +| `_apply_workspace_profile` | 625 | Apply a saved profile | +| `_handle_undo` / `_handle_redo` | 632 / 652 | Undo/redo handlers | +| `_toggle_command_palette` | 732 | Test helper for opening the palette via the hook API | +| `_gui_func` | 754 | The main per-frame render function | +| `_show_menus` | 807 | The menu bar (File, Windows, Project, Layout) | + +--- + +## Performance Considerations + +- **Frame budget**: target 60 FPS (16.67ms/frame). Heavy panels like the MMA dashboard and tier stream panels use `imscope` to skip work when not visible. +- **Performance monitor**: `app.perf_monitor` tracks per-component timing. Set `app.perf_profiling_enabled = True` to enable. +- **Heavy text**: `render_heavy_text` (line 4589) is the wrapper for displaying large blocks of text (used by text viewer, logs). +- **Background rendering**: Background shader (`bg_shader.py`) and ImGui draw lists are batched per frame. + +--- + +## Testing + +The App class is the integration target for most `live_gui` tests. Common patterns: + +### Pure Tests (no app) + +```python +def test_my_helper(): + from src.gui_2 import render_some_thing + # ... test the render function with a mock app ... +``` + +### Integration via `live_gui` + +```python +def test_my_thing(live_gui): + client = ApiHookClient() + client.push_event("custom_callback", { + "callback": "_my_method", + "args": [], + }) + time.sleep(0.5) + assert client.get_value("my_field") == "expected" +``` + +### Mock App + +```python +def test_with_mock(mock_app): + mock_app.some_attr = "test" + from src.gui_2 import render_main_interface + render_main_interface(mock_app) + # Assert on side effects +``` + +See [guide_testing.md](guide_testing.md) for the full test infrastructure. + +--- + +## Common Operations + +### Adding a New Toggleable Window + +1. Add the key to `show_windows` in `__init__`: + ```python + self.show_windows.setdefault("My Window", False) + ``` +2. Add a render function: + ```python + def render_my_window(app: App) -> None: + if not app.show_windows.get("My Window", False): + return + with imscope.window("My Window", app.show_windows["My Window"]) as (exp, opened): + if not opened: + app.show_windows["My Window"] = False + if exp: + # ... draw the window ... + ``` +3. Call it from `render_main_interface`. +4. Add a command in `src/commands.py` for keyboard access: + ```python + @registry.register + def toggle_my_window(app: "App") -> None: + from src.commands import _toggle_window + _toggle_window(app, "My Window") + ``` + +### Adding a New Modal + +1. Add a state flag in `__init__`: + ```python + self.show_my_modal: bool = False + ``` +2. Add a render function that uses `imgui.begin_popup_modal` or the begin/end pattern. +3. Call it from `render_main_interface`. +4. Optionally add a Hook API gettable field for testing. + +### Exposing a New Method via the Hook API + +In `__init__`: +```python +self.controller._predefined_callbacks['_my_method'] = self._my_method +self.controller._gettable_fields['show_my_thing'] = 'show_my_thing' +``` + +The test can then invoke via: +```python +client.push_event("custom_callback", {"callback": "_my_method", "args": []}) +value = client.get_value("show_my_thing") +``` + +--- + +## See Also + +- **[guide_architecture.md](guide_architecture.md#the-task-pipeline-producer-consumer-synchronization)** — Threading model that the App respects +- **[guide_command_palette.md](guide_command_palette.md)** — The 32 commands accessible via Ctrl+Shift+P +- **[guide_testing.md](guide_testing.md)** — Test infrastructure for GUI tests +- **[guide_hot_reload.md](guide_hot_reload.md)** — How Ctrl+Alt+R reloads this file +- **[conductor/product-guidelines.md](../../conductor/product-guidelines.md)** — The UI delegation pattern rules