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

482 lines
23 KiB
Markdown

# `src/gui_2.py` — Main ImGui Application
[Top](../Readme.md) | [Architecture](guide_architecture.md) | [Discussions](guide_discussions.md) | [State Lifecycle](guide_state_lifecycle.md) | [Context Aggregation](guide_context_aggregation.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")
```
### 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:
```python
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`:
```python
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](guide_testing.md#known-gotchas-2026-06-05) 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`:
```python
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.
```bash
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
- **[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
- **[guide_themes.md](guide_themes.md)** — TOML theme system; defines the `C_*` callable color helpers used throughout `gui_2.py`
- **[guide_discussions.md](guide_discussions.md)** — The Discussion system that the GUI's `render_discussion_entry`/`render_discussion_selector`/etc. render
- **[guide_state_lifecycle.md](guide_state_lifecycle.md)** — Undo/redo (`HistoryManager` + `UISnapshot`) and `App.__getattr__`/`__setattr__` state delegation
- **[guide_context_aggregation.md](guide_context_aggregation.md)** — The `aggregate.py` pipeline that consumes the GUI's `files` + `context_files` + `history` config
- **[conductor/product-guidelines.md](../conductor/product-guidelines.md)** — The UI delegation pattern rules
- **[conductor/tracks/nagent_review_20260608/report.md](../conductor/tracks/nagent_review_20260608/report.md)** — Deep-dive comparison of Manual Slop's discussion system to nagent's pattern; includes the per-entry (A1-A7) + discussion-level (B1-B11) + undo/redo (C1-C5) operation matrix