Private
Public Access
0
0
Files
manual_slop/docs/guide_gui_2.md
T

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) -> 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):

  • 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:
    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:
    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. 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:

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:

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 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)

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

  1. Add the key to show_windows in __init__:
    self.show_windows.setdefault("My Window", False)
    
  2. 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 ...
    
  3. Call it from render_main_interface.
  4. Add a command in src/commands.py for 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

  1. Add a state flag in __init__:
    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__:

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 1469ecacimgui.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