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
+85 -1
View File
@@ -116,4 +116,88 @@ def test_phase_1_audit_has_42_migration_target_sites():
f"expected {EXPECTED_SITE_COUNT}. Categories seen: "
f"{sorted({f.get('category') for f in migration_sites})}. "
f"This must match the 42 sites declared in PHASE1_SITE_INVENTORY.md."
)
)
def test_phase_2_invariant_drain_plane_render_functions_exist():
"""
Verify the 3 new module-level render functions exist in src/gui_2.py:
- render_controller_error_modal
- _render_worker_error_indicator
- _render_last_request_errors_modal
These are the drain-plane functions added in Phase 2 of the
result_migration_gui_2_20260619 track. They read the 8 controller
error attributes (added by sub-track 3 Phase 6) and surface them
to the user via ImGui popups and indicators.
The test imports src.gui_2 and inspects the module for the function
names. A failure here means the drain-plane wiring is incomplete.
"""
import inspect
import src.gui_2 as gui2_mod
assert hasattr(gui2_mod, "render_controller_error_modal"), (
"src/gui_2.py is missing the module-level function "
"'render_controller_error_modal'. This is the FR-DP-1 drain plane "
"function; it must be added per the result_migration_gui_2_20260619 "
"Phase 2 spec."
)
assert hasattr(gui2_mod, "_render_worker_error_indicator"), (
"src/gui_2.py is missing the module-level function "
"'_render_worker_error_indicator'. This is the FR-DP-2 drain plane "
"function; it must be added per the result_migration_gui_2_20260619 "
"Phase 2 spec."
)
assert hasattr(gui2_mod, "_render_last_request_errors_modal"), (
"src/gui_2.py is missing the module-level function "
"'_render_last_request_errors_modal'. This is the FR-DP-3 drain plane "
"function; it must be added per the result_migration_gui_2_20260619 "
"Phase 2 spec."
)
assert callable(getattr(gui2_mod, "render_controller_error_modal")), (
"render_controller_error_modal exists but is not callable."
)
assert callable(getattr(gui2_mod, "_render_worker_error_indicator")), (
"_render_worker_error_indicator exists but is not callable."
)
assert callable(getattr(gui2_mod, "_render_last_request_errors_modal")), (
"_render_last_request_errors_modal exists but is not callable."
)
def test_phase_2_invariant_drain_plane_app_delegations_exist():
"""
Verify the 3 new App class delegation methods exist in src/gui_2.py:
- App._render_controller_error_modal
- App._render_worker_error_indicator
- App._render_last_request_errors_modal
Per conductor/product-guidelines.md §"UI Delegation for Hot-Reload",
the App class must contain only thin delegation wrappers; the actual
logic lives in module-level functions. This test locks the
delegation contract for Phase 2.
The test imports src.gui_2, gets the App class via the module
(lazily - via the _LazyModule path or directly), and checks for
the methods.
"""
import src.gui_2 as gui2_mod
app_cls = getattr(gui2_mod, "App", None)
assert app_cls is not None, (
"src.gui_2 has no 'App' class attribute. Cannot verify delegations."
)
for method_name in (
"_render_controller_error_modal",
"_render_worker_error_indicator",
"_render_last_request_errors_modal",
):
assert hasattr(app_cls, method_name), (
f"App class is missing delegation method '{method_name}'. "
f"The drain plane requires the App class to delegate to the "
f"module-level render functions so the UI delegation pattern "
f"supports hot-reload."
)
method = getattr(app_cls, method_name)
assert callable(method), (
f"App.{method_name} exists but is not callable."
)