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:
+131
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user