docs(gui_2): add guide_gui_2.md
Comprehensive guide for the main GUI file (~260KB, 5400 lines): - Architecture (UI delegation pattern: module-level render functions + App class) - App class breakdown (init, key state, render entry point) - The render_main_interface flow (calls ~15 hub renderers) - Modal pattern via _render_window_if_open helper - Keyboard shortcuts (Ctrl+Shift+P, Ctrl+Alt+R, Ctrl+L, Ctrl+Enter) - Key patterns: hot reload, snapshots, window state, modal - Public methods worth knowing table - Performance considerations (60 FPS target, perf monitor) - Testing approaches (pure, integration via live_gui, mock_app) - Common operations: adding toggleable window, modal, hook API exposure
This commit is contained in:
@@ -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_<hub>(app) for each hub │
|
||||
│ - Handles modal popups │
|
||||
└──────────────────┬─────────────────────────────────┘
|
||||
│ calls
|
||||
▼
|
||||
┌────────────────────────────────────────────────────┐
|
||||
│ render_<panel>(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_<thing>(app: App) -> None:
|
||||
"""One-line docstring describing what this draws."""
|
||||
if app.perf_profiling_enabled:
|
||||
app.perf_monitor.start_component("<thing>")
|
||||
# ... draw ImGui widgets, reading from and writing to app state ...
|
||||
if app.perf_profiling_enabled:
|
||||
app.perf_monitor.end_component("<thing>")
|
||||
```
|
||||
|
||||
- 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_<thing>_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
|
||||
Reference in New Issue
Block a user