Private
Public Access
0
0
Files
manual_slop/docs/guide_gui_2.md
T
conductor-tier2 161ebb0da6 docs(fix): correct nav link case + relative-path level
Gitea (and any case-sensitive filesystem) was rendering the [Top]
nav links in /docs as broken because of two bugs:

1. Case-sensitivity: 22 links used '../README.md' (all-uppercase)
   but the actual file is 'docs/Readme.md' (capital R, lowercase
   rest). 21 guide_*.md nav bars were affected, plus 1 internal
   cross-link in Readme.md itself. Works on Windows (case-
   insensitive) but broken on Linux/Gitea.

   Fix: 22 occurrences across 22 files changed
   '../README.md' -> '../Readme.md'

2. Wrong relative-path level: 16 links used '../../conductor/...'
   from 'docs/guide_*.md' to reach 'conductor/'. This goes up 2
   levels to 'projects/', which doesn't exist. The correct path
   from 'docs/guide_*.md' to 'conductor/' is 1 level up
   ('../conductor/...'). 12 unique patterns across 10 files
   affected.

   Fix: 16 occurrences across 10 files changed
   '../../conductor/' -> '../conductor/'

3. Bonus: 1 planned-guide link in guide_context_curation.md
   referenced a never-written 'guide_context_presets.md'. The
   ContextPreset schema is now fully covered in the new
   'guide_context_aggregation.md' (per the 2026-06-08 docs
   refresh). Fix: link target updated.

No content was changed, only link paths. 24 files, 37 link
replacements, 37 deletions.

Verification:
- All .md links in docs/ now resolve to existing files
  (validated by path-resolution check from each file's directory)
- The 3 new guides from the previous docs refresh commit
  (guide_discussions.md, guide_state_lifecycle.md,
  guide_context_aggregation.md) had the case bug inherited from
  guide_architecture.md's existing nav pattern; their top-of-file
  nav bars are now correct
- The 21 pre-existing guide nav bars that had the same bug
  (all 21 of them, except the 3 that used the correct case:
  guide_mma.md, guide_simulations.md, guide_tools.md) are now
  also fixed
- Inter-guide links (e.g. [Discussions](guide_discussions.md))
  were not affected; they were always correct because both the
  link text and the actual filename are lowercase

This is a docs-only fix. No code modified.
2026-06-08 19:51:55 -04:00

23 KiB

src/gui_2.py — Main ImGui Application

Top | Architecture | Discussions | State Lifecycle | Context Aggregation | 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.

The __getattr__ / __setattr__ State Delegation Pattern

The App class (around line 478-487) defines two descriptor hooks that delegate state to the AppController:

def __getattr__(self, name: str) -> Any:
 if name == 'controller':
  raise AttributeError(name)
 return getattr(self.controller, name)

def __setattr__(self, name: str, value: Any) -> None:
 if name != 'controller' and hasattr(self, 'controller') and hasattr(self.controller, name):
  setattr(self.controller, name, value)
 else:
  object.__setattr__(self, name, value)

Why this matters:

  • The Controller is the single source of truth for settable state (e.g. ui_ai_input, ui_separate_tier1, show_windows, temperature).
  • The App is a thin view layer that delegates reads (__getattr__) and writes (__setattr__) to the Controller.
  • This means: do NOT add self.ui_ai_input = "" in App.__init__ for fields that the Controller owns. The Controller initializes them via its own __init__. If the App initializes them too, the App's value shadows the Controller's (and __getattr__ returns the App's value, not the Controller's).

Safe App-only state (no Controller counterpart):

  • ui_separate_context_preview, ui_separate_message_panel, ui_separate_response_panel, ui_separate_tool_calls_panel, ui_separate_external_tools, ui_discussion_split_h — these are NOT in the Controller's _settable_fields, so __setattr__ falls through to object.__setattr__ and stores them on the App.
  • Private App state (_ini_capture_ready, _pending_gui_tasks, etc.) is also App-only.

Subtle gotcha: the hasattr(self.controller, name) check in __setattr__ returns False for App-only fields on the first write (because the Controller doesn't have the attribute yet). The write goes to the App. The Controller never gets the attribute. This is the correct behavior for App-only fields, but wrong for Controller-owned fields that haven't been initialized in the Controller's __init__. Always make sure Controller-owned fields are initialized in AppController.__init__ (or in init_state called from there) so __setattr__'s hasattr check returns True.

Indentation Gotcha (CRITICAL)

The bug: A class method defined with the right intent (2-space indent) may be parsed as nested inside the previous function if indentation is off by even one space. The file "passes" syntactically (imports OK) but the method is not on the class. hasattr(App, 'method_name') returns False. Any production code that calls app.method_name falls through to __getattr__, delegates to the Controller (which also doesn't have the method), and a cryptic AttributeError is raised at runtime.

How to detect: Use AST to list all App methods. The skeleton via manual-slop_py_get_skeleton should show the method as a class member. If the AST walk doesn't find the method, it's nested.

uv run python -c "import ast; tree = ast.parse(open('src/gui_2.py').read()); [print(item.name) for n in ast.walk(tree) if isinstance(n, ast.ClassDef) and n.name == 'App' for item in n.body if isinstance(item, ast.FunctionDef)]"

How to fix: Re-indent the affected method to 2-space class level. This bit the project in 2026-06-05 during a cleanup commit: _capture_workspace_profile was being parsed as nested inside _apply_snapshot due to a 1-space indentation drift, breaking 3 live_gui tests (test_auto_switch_sim, test_workspace_profiles_restoration, test_undo_redo_lifecycle).



See Also