19 KiB
src/gui_2.py — Main ImGui Application
Top | Architecture | Testing
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) -> Nonefunctions that draw individual panels Appclass (~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):
- Module-level functions do the drawing. They take
app: Appas their first parameter. - The
Appclass 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.reloadwithout 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:
- Creates the
AppController(the headless counterpart) - Initializes
history.HistoryManager(max_capacity=100)for undo/redo - Sets up UI state flags:
show_windows,show_*_modalflags - Initializes the workspace manager
- Starts services via
controller.start_services(self) - Registers predefined callbacks on the controller for the Hook API:
self.controller._predefined_callbacks['save_context_preset'] = self.save_context_preset self.controller._predefined_callbacks['_toggle_command_palette'] = self._toggle_command_palette # ... ~10 more - Registers gettable fields for the Hook API:
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:
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:
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. Usesimscope.windowcontext manager.imscopefromsrc/imgui_scopes.py: stack-style context managers forimgui.begin/end,imgui.push_style/pop_style, etc. — replaces the legacy push/pop pattern with Pythonicwithstatements.
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:
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: Appas 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:
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:
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:
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:
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
imscopeto skip work when not visible. - Performance monitor:
app.perf_monitortracks per-component timing. Setapp.perf_profiling_enabled = Trueto 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)
def test_my_helper():
from src.gui_2 import render_some_thing
# ... test the render function with a mock app ...
Integration via live_gui
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
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 for the full test infrastructure.
Common Operations
Adding a New Toggleable Window
- Add the key to
show_windowsin__init__:self.show_windows.setdefault("My Window", False) - Add a render function:
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 ... - Call it from
render_main_interface. - Add a command in
src/commands.pyfor keyboard access:@registry.register def toggle_my_window(app: "App") -> None: from src.commands import _toggle_window _toggle_window(app, "My Window")
Adding a New Modal
- Add a state flag in
__init__:self.show_my_modal: bool = False - Add a render function that uses
imgui.begin_popup_modalor the begin/end pattern. - Call it from
render_main_interface. - Optionally add a Hook API gettable field for testing.
Exposing a New Method via the Hook API
In __init__:
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:
client.push_event("custom_callback", {"callback": "_my_method", "args": []})
value = client.get_value("show_my_thing")
Theme Color-Callable Pattern
Theme color helpers in src/theme_2.py (C_LBL, C_VAL, C_OUT, C_IN, C_OK, C_ERR, etc.) are callable functions, not ImVec4 values. This is intentional: it lets the active theme be swapped at runtime and have the new colors take effect on the next render frame, instead of capturing stale colors at module import time.
Correct usage — call the function at the use site:
imgui.text_colored(C_LBL(), "Completed:")
imgui.text_colored(C_VAL(), str(value))
Common bug — storing the function in a dict keyed by name, then passing the function (not its result) to imgui.text_colored:
DIR_COLORS = {
"request": C_OUT,
"response": C_IN,
}
# ... later ...
d_col_fn = DIR_COLORS.get(direction, C_VAL) # WRONG: stores the function
imgui.text_colored(d_col_fn(), direction) # CORRECT: calls it
This pattern is used in src/gui_2.py:3705-3707 (the render_comms_history_panel DIR_COLORS/KIND_COLORS dicts). The bug shipped in the multi-themes track commit 7ea52cbb and was caught by 1469ecac — imgui.text_colored was being passed a callable instead of an ImVec4, raising TypeError on every render frame.
When writing tests that assert theme color usage, patch src.theme_2.imgui so theme.get_color() returns the mock's ImVec4, and assert with C_LBL() (called), not C_LBL (the function).
Workspace Profile Defer-Not-Catch
_capture_workspace_profile (line 601) calls imgui.save_ini_settings_to_memory() to serialize the current ImGui layout. This C function crashes the Python process with 0xc0000005 access violation when called in the first few render frames because ImGui's internal state (Fonts, DisplaySize, Settings) isn't yet fully initialized. The crash is not catchable from Python — it's a native access violation, not a Python exception.
The fix uses a defer-not-catch pattern: a one-shot _ini_capture_ready flag in the instance state. The first call (during initial startup) returns an empty profile and flips the flag; subsequent calls (when the user actually clicks "Save Profile") invoke the C function. The user's workflow is unaffected because the first call is non-blocking and the user cannot have clicked "Save Profile" before the GUI was fully rendered.
This pattern unblocks 4-5 live_gui tests that were crashing the GUI subprocess during the first render frames after a save_workspace_profile Hook API callback. See guide_testing.md for the broader pattern and how to recognize these crashes.
Sentinel type contract. When implementing a defer-not-catch guard, the early-return sentinel value must match the type contract of the downstream consumer. For WorkspaceProfile.ini_content: str (in this codebase), the sentinel must be "" (str), not b"" (bytes) — tomli_w rejects bytes (TypeError: Object of type 'bytes' is not TOML serializable), and imgui.load_ini_settings_from_memory(ini_data: str, ...) also expects str. A previous version of this fix used b"" and silently broke the save flow via a TypeError raised by tomli_w.dump; tests passed unit-test-wise but failed in the live_gui save+load round-trip. The fix was a 1-character change (b"" → ""). The regression test in tests/test_workspace_profile_serialization.py encodes this contract.
See Also
- guide_architecture.md — Threading model that the App respects
- guide_command_palette.md — The 32 commands accessible via Ctrl+Shift+P
- guide_testing.md — Test infrastructure for GUI tests
- guide_hot_reload.md — How Ctrl+Alt+R reloads this file
- guide_themes.md — TOML theme system; defines the
C_*callable color helpers used throughoutgui_2.py - conductor/product-guidelines.md — The UI delegation pattern rules