Private
Public Access
0
0

feat(gui_2): add 3 drain-plane render functions (Phase 2, tasks 2.1-2.3)

TIER-2 READ conductor/code_styleguides/error_handling.md end-to-end before Phase 2.

Adds the drain plane that consumes the 8 controller error attributes
(the data plane added by sub-track 3 Phase 6).

Module-level functions in src/gui_2.py (lines 7293-7410):
- _drain_normalize_errors (helper, lines 7295-7326): duck-typed
  normalizer for 3 error-container shapes (Optional[ErrorInfo],
  List[Tuple[str, ErrorInfo]], Dict[str, ErrorInfo])
- render_controller_error_modal (lines 7328-7368): FR-DP-1 Pattern 2
  drain point; reads all 8 controller attrs, opens per-attr popups
- _render_worker_error_indicator (lines 7370-7385): FR-DP-2 status-bar
  widget showing worker error count, clickable
- _render_last_request_errors_modal (lines 7387-7409): FR-DP-3 per-request
  error modal opened after AI request completion

App class delegation wrappers (lines 1138-1148):
- App._render_controller_error_modal -> module-level
- App._render_worker_error_indicator -> module-level
- App._render_last_request_errors_modal -> module-level

Per UI Delegation Pattern: App class has thin wrappers; logic at
module level for hot-reload support. 1-space indentation, CRLF.

Audit: no new violations introduced (gui_2.py still 25 V + 13 S +
2 RETHROW + 2 UNCLEAR + 12 COMPLIANT = 54). Tests: 4/4 pass.
This commit is contained in:
2026-06-19 21:32:24 -04:00
parent 7c93a68f67
commit 5b139e6ab1
2 changed files with 216 additions and 2 deletions
+131 -1
View File
@@ -1135,6 +1135,18 @@ class App:
self.show_windows[name] = bool(opened)
if exp: render_func()
def _render_controller_error_modal(self) -> None:
"""Thin delegation wrapper for render_controller_error_modal (drain plane). [SDM: src/gui_2.py:App._render_controller_error_modal]"""
render_controller_error_modal(self)
def _render_worker_error_indicator(self) -> None:
"""Thin delegation wrapper for _render_worker_error_indicator (drain plane). [SDM: src/gui_2.py:App._render_worker_error_indicator]"""
_render_worker_error_indicator(self)
def _render_last_request_errors_modal(self) -> None:
"""Thin delegation wrapper for _render_last_request_errors_modal (drain plane). [SDM: src/gui_2.py:App._render_last_request_errors_modal]"""
_render_last_request_errors_modal(self)
def _show_menus(self) -> None:
global win32gui, win32con
if win32gui is None:
@@ -7276,7 +7288,125 @@ def render_mma_focus_selector(app: App) -> None:
for tier in ["Tier 2", "Tier 3", "Tier 4"]:
if imgui.selectable(tier, app.ui_focus_agent == tier)[0]: app.ui_focus_agent = tier
imgui.end_combo()
imgui.same_line()
if app.ui_focus_agent and imgui.button("x##clear_focus"): app.ui_focus_agent = None
#region: Drain Plane (result_migration_gui_2_20260619 Phase 2)
def _drain_normalize_errors(value: Any) -> list:
"""Normalize any of the 8 controller error attribute shapes to a flat list.
The 8 attributes (added by sub-track 3 Phase 6) have three distinct shapes:
- Optional[ErrorInfo]: single error or None
(_signal_handler_error, _inject_preview_error, _mcp_config_parse_error,
_save_project_error)
- List[Tuple[str, ErrorInfo]]: append-only (op_name, error) tuples
(_last_request_errors, _worker_errors, _startup_timeline_errors)
- Dict[str, ErrorInfo]: provider -> error
(_model_fetch_errors)
This helper extracts the ErrorInfo objects from each shape so the render
loop can iterate uniformly. Implemented via duck typing (no isinstance
against ErrorInfo, since gui_2.py intentionally does not import the
result_types module to keep the import graph lean).
[SDM: src/gui_2.py:_drain_normalize_errors]
"""
if value is None:
return []
if isinstance(value, dict):
return list(value.values())
if isinstance(value, (list, tuple)):
result = []
for item in value:
if isinstance(item, tuple) and len(item) >= 2:
result.append(item[1])
else:
result.append(item)
return result
return [value]
def render_controller_error_modal(app: "App") -> None:
"""Drain plane: read the 8 controller error attributes; open imgui.open_popup for each non-empty.
This is the canonical Pattern 2 drain point (per error_handling.md:396-407):
the caller (the render loop) calls this every frame; the function is non-blocking
and only triggers popups for non-empty error states.
The 8 controller attributes drained here are the data plane added by
sub-track 3 (result_migration_app_controller_20260618). The render functions
consume them and surface them to the user via ImGui popups.
[C: src/gui_2.py:_render_controller_error_modal delegation wrapper,
src/gui_2.py:_render_main_interface (call site TBD)]
"""
attrs = [
("_last_request_errors", "Last Request Errors"),
("_worker_errors", "Worker Errors"),
("_startup_timeline_errors", "Startup Timeline Errors"),
("_signal_handler_error", "Signal Handler Error"),
("_inject_preview_error", "Inject Preview Error"),
("_mcp_config_parse_error", "MCP Config Parse Error"),
("_save_project_error", "Save Project Error"),
("_model_fetch_errors", "Model Fetch Errors"),
]
for attr_name, popup_title in attrs:
raw = getattr(app.controller, attr_name, None)
errs = _drain_normalize_errors(raw)
if not errs:
continue
popup_id = f"{popup_title}##{attr_name}"
imgui.open_popup(popup_id)
opened, _ = imgui.begin_popup(popup_id)
if opened:
imgui.text(f"{popup_title}: {len(errs)} error{'s' if len(errs) != 1 else ''}")
imgui.separator()
for e in errs:
imgui.text_wrapped(getattr(e, "ui_message", lambda: str(e))())
imgui.separator()
if imgui.button("OK"):
imgui.close_current_popup()
imgui.end_popup()
def _render_worker_error_indicator(app: "App") -> None:
"""Status-bar widget showing worker error count. Click opens the controller error modal.
Visible only when app.controller._worker_errors is non-empty. Per the
UI delegation pattern (conductor/product-guidelines.md), the function lives
at module level and the App class wraps it as a thin delegation.
[C: src/gui_2.py:App._render_worker_error_indicator delegation wrapper]
"""
errs = getattr(app.controller, "_worker_errors", None)
if not errs:
return
n = len(errs)
imgui.text(f"[!] {n} worker error{'s' if n != 1 else ''}")
if imgui.is_item_hovered() and imgui.is_mouse_clicked(0):
render_controller_error_modal(app)
def _render_last_request_errors_modal(app: "App") -> None:
"""Per-request error modal. Surfaced after each AI request via the request completion hook.
Opens a modal only if errors accumulated during the request
(app.controller._last_request_errors is non-empty).
[C: src/gui_2.py:App._render_last_request_errors_modal delegation wrapper]
"""
errs = getattr(app.controller, "_last_request_errors", None)
if not errs:
return
popup_id = "Request Errors##_last_request_errors"
opened, _ = imgui.begin_popup_modal(popup_id)
if opened:
imgui.text(f"{len(errs)} error{'s' if len(errs) != 1 else ''} during last request:")
imgui.separator()
for e in errs:
imgui.text_wrapped(getattr(e, "ui_message", lambda: str(e))())
imgui.separator()
if imgui.button("OK"):
imgui.close_current_popup()
imgui.end_popup()
#endregion: Drain Plane
#endregion: MMA