From 2ce7a8706950103be64086b07305d457708790c0 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sun, 1 Mar 2026 15:57:46 -0500 Subject: [PATCH] feat(gui): Tier stream panels as separate dockable windows (Tier 1-4) --- gui_2.py | 108 +++++++++++----------- scripts/tasks/gui_ux_1_1_panels_impl.toml | 51 ++++++++++ tests/test_mma_dashboard_streams.py | 58 ++++++------ 3 files changed, 132 insertions(+), 85 deletions(-) create mode 100644 scripts/tasks/gui_ux_1_1_panels_impl.toml diff --git a/gui_2.py b/gui_2.py index dc85b92..7ed77cf 100644 --- a/gui_2.py +++ b/gui_2.py @@ -208,6 +208,10 @@ class App: "Files & Media": True, "AI Settings": True, "MMA Dashboard": True, + "Tier 1: Strategy": True, + "Tier 2: Tech Lead": True, + "Tier 3: Workers": True, + "Tier 4: QA": True, "Discussion Hub": True, "Operations Hub": True, "Theme": True, @@ -1592,6 +1596,26 @@ class App: if exp: self._render_mma_dashboard() imgui.end() + if self.show_windows.get("Tier 1: Strategy", False): + exp, self.show_windows["Tier 1: Strategy"] = imgui.begin("Tier 1: Strategy", self.show_windows["Tier 1: Strategy"]) + if exp: + self._render_tier_stream_panel("Tier 1", "Tier 1") + imgui.end() + if self.show_windows.get("Tier 2: Tech Lead", False): + exp, self.show_windows["Tier 2: Tech Lead"] = imgui.begin("Tier 2: Tech Lead", self.show_windows["Tier 2: Tech Lead"]) + if exp: + self._render_tier_stream_panel("Tier 2", "Tier 2 (Tech Lead)") + imgui.end() + if self.show_windows.get("Tier 3: Workers", False): + exp, self.show_windows["Tier 3: Workers"] = imgui.begin("Tier 3: Workers", self.show_windows["Tier 3: Workers"]) + if exp: + self._render_tier_stream_panel("Tier 3", None) + imgui.end() + if self.show_windows.get("Tier 4: QA", False): + exp, self.show_windows["Tier 4: QA"] = imgui.begin("Tier 4: QA", self.show_windows["Tier 4: QA"]) + if exp: + self._render_tier_stream_panel("Tier 4", "Tier 4 (QA)") + imgui.end() if self.show_windows.get("Theme", False): self._render_theme_panel() if self.show_windows.get("Discussion Hub", False): @@ -2730,60 +2754,6 @@ class App: imgui.text(f"{stats.get('output', 0):,}") imgui.end_table() imgui.separator() - if imgui.collapsing_header("Tier 1: Strategy"): - content = self.mma_streams.get("Tier 1", "") - if imgui.begin_child("##tier1_scroll", imgui.ImVec2(-1, 200), True): - imgui.text_wrapped(content) - try: - at_bottom = imgui.get_scroll_y() >= imgui.get_scroll_max_y() - 10 - if at_bottom and len(content) != self._tier_stream_last_len.get("Tier 1", -1): - imgui.set_scroll_here_y(1.0) - self._tier_stream_last_len["Tier 1"] = len(content) - except (TypeError, AttributeError): - pass - imgui.end_child() - if imgui.collapsing_header("Tier 2: Tech Lead"): - content = self.mma_streams.get("Tier 2 (Tech Lead)", "") - if imgui.begin_child("##tier2_scroll", imgui.ImVec2(-1, 200), True): - imgui.text_wrapped(content) - try: - at_bottom = imgui.get_scroll_y() >= imgui.get_scroll_max_y() - 10 - if at_bottom and len(content) != self._tier_stream_last_len.get("Tier 2 (Tech Lead)", -1): - imgui.set_scroll_here_y(1.0) - self._tier_stream_last_len["Tier 2 (Tech Lead)"] = len(content) - except (TypeError, AttributeError): - pass - imgui.end_child() - if imgui.collapsing_header("Tier 3: Workers"): - tier3_keys = [k for k in self.mma_streams if "Tier 3" in k] - if not tier3_keys: - imgui.text_disabled("No worker output yet.") - else: - for key in tier3_keys: - ticket_id = key.split(": ", 1)[-1] if ": " in key else key - imgui.text(ticket_id) - if imgui.begin_child(f"##tier3_{ticket_id}_scroll", imgui.ImVec2(-1, 150), True): - imgui.text_wrapped(self.mma_streams[key]) - try: - at_bottom = imgui.get_scroll_y() >= imgui.get_scroll_max_y() - 10 - if at_bottom and len(self.mma_streams[key]) != self._tier_stream_last_len.get(key, -1): - imgui.set_scroll_here_y(1.0) - self._tier_stream_last_len[key] = len(self.mma_streams[key]) - except (TypeError, AttributeError): - pass - imgui.end_child() - if imgui.collapsing_header("Tier 4: QA"): - content = self.mma_streams.get("Tier 4 (QA)", "") - if imgui.begin_child("##tier4_scroll", imgui.ImVec2(-1, 200), True): - imgui.text_wrapped(content) - try: - at_bottom = imgui.get_scroll_y() >= imgui.get_scroll_max_y() - 10 - if at_bottom and len(content) != self._tier_stream_last_len.get("Tier 4 (QA)", -1): - imgui.set_scroll_here_y(1.0) - self._tier_stream_last_len["Tier 4 (QA)"] = len(content) - except (TypeError, AttributeError): - pass - imgui.end_child() # 4. Task DAG Visualizer imgui.text("Task DAG") if self.active_track: @@ -2808,6 +2778,36 @@ class App: else: imgui.text_disabled("No active MMA track.") + def _render_tier_stream_panel(self, tier_key: str, stream_key: str | None) -> None: + if stream_key is not None: + content = self.mma_streams.get(stream_key, "") + if imgui.begin_child("##stream_content", imgui.ImVec2(-1, -1)): + imgui.text_wrapped(content) + try: + if len(content) != self._tier_stream_last_len.get(stream_key, -1): + imgui.set_scroll_here_y(1.0) + self._tier_stream_last_len[stream_key] = len(content) + except (TypeError, AttributeError): + pass + imgui.end_child() + else: + tier3_keys = [k for k in self.mma_streams if "Tier 3" in k] + if not tier3_keys: + imgui.text_disabled("No worker output yet.") + else: + for key in tier3_keys: + ticket_id = key.split(": ", 1)[-1] if ": " in key else key + imgui.text(ticket_id) + if imgui.begin_child(f"##tier3_{ticket_id}_scroll", imgui.ImVec2(-1, 150), True): + imgui.text_wrapped(self.mma_streams[key]) + try: + if len(self.mma_streams[key]) != self._tier_stream_last_len.get(key, -1): + imgui.set_scroll_here_y(1.0) + self._tier_stream_last_len[key] = len(self.mma_streams[key]) + except (TypeError, AttributeError): + pass + imgui.end_child() + def _render_ticket_dag_node(self, ticket: Ticket, tickets_by_id: dict[str, Ticket], children_map: dict[str, list[str]], rendered: set[str]) -> None: tid = ticket.get('id', '??') target = ticket.get('target_file', 'general') diff --git a/scripts/tasks/gui_ux_1_1_panels_impl.toml b/scripts/tasks/gui_ux_1_1_panels_impl.toml new file mode 100644 index 0000000..f53d26a --- /dev/null +++ b/scripts/tasks/gui_ux_1_1_panels_impl.toml @@ -0,0 +1,51 @@ +role = "tier3-worker" +docs = ["conductor/workflow.md"] +prompt = """ +Refactor the tier stream display in @gui_2.py: replace the 4 collapsing_header sections inside _render_mma_dashboard with 4 separate dockable imgui.begin() windows. + +CHANGES NEEDED: + +1. In __init__ (around line 207), in the _default_windows dict, add 4 new entries after "MMA Dashboard": + "Tier 1: Strategy": True, + "Tier 2: Tech Lead": True, + "Tier 3: Workers": True, + "Tier 4: QA": True, + +2. In _render_mma_dashboard (find the 4 collapsing_header blocks added for Tier 1/2/3/4 and the _tier_stream_last_len auto-scroll logic), REMOVE all of it. The method should no longer render any tier stream content. + +3. Add a new method _render_tier_stream_panel(self, tier_key: str, stream_key: str | None) to App class (place it after _render_mma_dashboard). It renders content for one tier: + - If stream_key is not None (Tier 1, 2, 4): show self.mma_streams.get(stream_key, "") via imgui.text_wrapped inside a begin_child("##stream_content", ImVec2(-1, -1)) + - If stream_key is None (Tier 3): aggregate all keys in self.mma_streams containing "Tier 3", show each with a ticket sub-header (imgui.text(ticket_id) then begin_child per ticket) + - Auto-scroll: if content length changed since last frame (tracked in self._tier_stream_last_len dict), call imgui.set_scroll_here_y(1.0) + +4. In _gui_func, after the "MMA Dashboard" window block (around line 1592), add 4 new window blocks following the exact same pattern as other windows: + if self.show_windows.get("Tier 1: Strategy", False): + exp, self.show_windows["Tier 1: Strategy"] = imgui.begin("Tier 1: Strategy", self.show_windows["Tier 1: Strategy"]) + if exp: + self._render_tier_stream_panel("Tier 1", "Tier 1") + imgui.end() + if self.show_windows.get("Tier 2: Tech Lead", False): + exp, self.show_windows["Tier 2: Tech Lead"] = imgui.begin("Tier 2: Tech Lead", self.show_windows["Tier 2: Tech Lead"]) + if exp: + self._render_tier_stream_panel("Tier 2", "Tier 2 (Tech Lead)") + imgui.end() + if self.show_windows.get("Tier 3: Workers", False): + exp, self.show_windows["Tier 3: Workers"] = imgui.begin("Tier 3: Workers", self.show_windows["Tier 3: Workers"]) + if exp: + self._render_tier_stream_panel("Tier 3", None) + imgui.end() + if self.show_windows.get("Tier 4: QA", False): + exp, self.show_windows["Tier 4: QA"] = imgui.begin("Tier 4: QA", self.show_windows["Tier 4: QA"]) + if exp: + self._render_tier_stream_panel("Tier 4", "Tier 4 (QA)") + imgui.end() + +5. Update @tests/test_mma_dashboard_streams.py — replace the 3 existing tests with new ones that test _render_tier_stream_panel instead: + - test_tier1_renders_stream_content: call App._render_tier_stream_panel(app, "Tier 1", "Tier 1") with mma_streams={"Tier 1": "hello"}, assert imgui.text_wrapped called with "hello" + - test_tier3_renders_worker_subheaders: call App._render_tier_stream_panel(app, "Tier 3", None) with mma_streams={"Tier 3 (Worker): T-001": "out1", "Tier 3 (Worker): T-002": "out2"}, assert imgui.text called with args containing "T-001" and "T-002" + - test_mma_dashboard_no_longer_has_strategy_box: call App._render_mma_dashboard(app), assert imgui.collapsing_header NOT called with any string containing "Tier" + +Also confirm @tests/test_mma_approval_indicators.py still passes unchanged. + +Use exactly 1-space indentation for Python code. +""" diff --git a/tests/test_mma_dashboard_streams.py b/tests/test_mma_dashboard_streams.py index ab63375..0690e88 100644 --- a/tests/test_mma_dashboard_streams.py +++ b/tests/test_mma_dashboard_streams.py @@ -20,6 +20,10 @@ def _make_app(**kwargs): app.mma_status = kwargs.get("mma_status", "idle") app.active_tier = kwargs.get("active_tier", None) app.mma_step_mode = kwargs.get("mma_step_mode", False) + app._pending_mma_spawn = kwargs.get("_pending_mma_spawn", None) + app._pending_mma_approval = kwargs.get("_pending_mma_approval", None) + app._pending_ask_dialog = kwargs.get("_pending_ask_dialog", False) + app._tier_stream_last_len = {} return app @@ -35,46 +39,38 @@ def _make_imgui_mock(): class TestMMADashboardStreams: - def test_all_four_tier_headers_rendered(self): - """collapsing_header must be called for all four tiers (new behaviour).""" - app = _make_app(mma_streams={ - "Tier 1": "strat", - "Tier 2 (Tech Lead)": "lead", - "Tier 3 (Worker): T-001": "work", - "Tier 4 (QA)": "qa", - }) + def test_tier1_renders_stream_content(self): + """_render_tier_stream_panel for Tier 1 must call text_wrapped with the stream content.""" + app = _make_app(mma_streams={"Tier 1": "hello"}) imgui_mock = _make_imgui_mock() - with patch("gui_2.imgui", imgui_mock): - App._render_mma_dashboard(app) - header_args = " ".join(str(c) for c in imgui_mock.collapsing_header.call_args_list) - assert "Tier 1" in header_args, "collapsing_header not called with 'Tier 1'" - assert "Tier 2" in header_args, "collapsing_header not called with 'Tier 2'" - assert "Tier 3" in header_args, "collapsing_header not called with 'Tier 3'" - assert "Tier 4" in header_args, "collapsing_header not called with 'Tier 4'" - - def test_tier3_aggregates_multiple_worker_streams(self): - """Tier 3 section must render a sub-header for each worker stream key.""" - app = _make_app(mma_streams={ - "Tier 3 (Worker): T-001": "output1", - "Tier 3 (Worker): T-002": "output2", - }) - imgui_mock = _make_imgui_mock() - imgui_mock.collapsing_header.return_value = True imgui_mock.begin_child.return_value = True with patch("gui_2.imgui", imgui_mock): - App._render_mma_dashboard(app) + App._render_tier_stream_panel(app, "Tier 1", "Tier 1") + text_wrapped_args = " ".join(str(c) for c in imgui_mock.text_wrapped.call_args_list) + assert "hello" in text_wrapped_args, "text_wrapped not called with stream content 'hello'" + + def test_tier3_renders_worker_subheaders(self): + """_render_tier_stream_panel for Tier 3 must render a sub-header for each worker stream key.""" + app = _make_app(mma_streams={ + "Tier 3 (Worker): T-001": "out1", + "Tier 3 (Worker): T-002": "out2", + }) + imgui_mock = _make_imgui_mock() + imgui_mock.begin_child.return_value = True + with patch("gui_2.imgui", imgui_mock): + App._render_tier_stream_panel(app, "Tier 3", None) text_args = " ".join(str(c) for c in imgui_mock.text.call_args_list) assert "T-001" in text_args, "imgui.text not called with 'T-001' worker sub-header" assert "T-002" in text_args, "imgui.text not called with 'T-002' worker sub-header" - def test_old_single_strategy_box_removed(self): - """input_text_multiline must NOT be called with '##mma_strategy' in the new design.""" + def test_mma_dashboard_no_longer_has_strategy_box(self): + """_render_mma_dashboard must NOT call collapsing_header with any 'Tier' string.""" app = _make_app(mma_streams={"Tier 1": "strategy text"}) imgui_mock = _make_imgui_mock() with patch("gui_2.imgui", imgui_mock): App._render_mma_dashboard(app) - for c in imgui_mock.input_text_multiline.call_args_list: - first_arg = c.args[0] if c.args else None - assert first_arg != "##mma_strategy", ( - "input_text_multiline called with '##mma_strategy' — old single strategy box must be removed" + for c in imgui_mock.collapsing_header.call_args_list: + first_arg = c.args[0] if c.args else "" + assert "Tier" not in str(first_arg), ( + f"collapsing_header called with 'Tier' string — tier panels must be separate windows now" )