diff --git a/gui_2.py b/gui_2.py index 0019d53..d9bfd5f 100644 --- a/gui_2.py +++ b/gui_2.py @@ -282,6 +282,7 @@ class App: self.ui_agent_tools: dict[str, bool] = {t: agent_tools_cfg.get(t, True) for t in AGENT_TOOL_NAMES} self.tracks: list[dict[str, Any]] = [] self.mma_streams: dict[str, str] = {} + self._tier_stream_last_len: dict[str, int] = {} self.is_viewing_prior_session = False self.prior_session_entries: list[dict[str, Any]] = [] self.test_hooks_enabled = ("--enable-test-hooks" in sys.argv) or (os.environ.get("SLOP_TEST_HOOKS") == "1") @@ -2716,10 +2717,60 @@ class App: imgui.text(f"{stats.get('output', 0):,}") imgui.end_table() imgui.separator() - imgui.separator() - imgui.text("Strategy (Tier 1)") - strategy_text = self.mma_streams.get("Tier 1", "") - imgui.input_text_multiline("##mma_strategy", strategy_text, imgui.ImVec2(-1, 150), imgui.InputTextFlags_.read_only) + 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: diff --git a/scripts/tasks/gui_ux_1_1_impl.toml b/scripts/tasks/gui_ux_1_1_impl.toml new file mode 100644 index 0000000..9986b45 --- /dev/null +++ b/scripts/tasks/gui_ux_1_1_impl.toml @@ -0,0 +1,55 @@ +role = "tier3-worker" +docs = ["conductor/workflow.md"] +prompt = """ +Implement Task 1.1 in @gui_2.py: replace the single "Strategy (Tier 1)" text box in _render_mma_dashboard with four collapsible tier stream sections. + +CURRENT CODE to replace (around lines 2728-2732 in gui_2.py): + imgui.separator() + imgui.separator() + imgui.text("Strategy (Tier 1)") + strategy_text = self.mma_streams.get("Tier 1", "") + imgui.input_text_multiline("##mma_strategy", strategy_text, imgui.ImVec2(-1, 150), imgui.InputTextFlags_.read_only) + +REPLACE with these four collapsible sections (insert after the token usage table separator, before "# 4. Task DAG Visualizer"): + +TIER DEFINITIONS: +- Tier 1: label="Strategy", stream key="Tier 1" +- Tier 2: label="Tech Lead", stream key="Tier 2 (Tech Lead)" +- Tier 3: label="Workers", aggregates all mma_streams keys containing "Tier 3" +- Tier 4: label="QA", stream key="Tier 4 (QA)" + +IMPLEMENTATION REQUIREMENTS: +1. For Tier 1, 2, 4 (single stream): + if imgui.collapsing_header(f"Tier {N}: {label}"): + content = self.mma_streams.get(stream_key, "") + imgui.begin_child(f"##tier{N}_scroll", imgui.ImVec2(-1, 200), True) + imgui.text_wrapped(content) + imgui.end_child() + +2. For Tier 3 (aggregated): + 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) # sub-header + imgui.begin_child(f"##tier3_{ticket_id}_scroll", imgui.ImVec2(-1, 150), True) + imgui.text_wrapped(self.mma_streams[key]) + imgui.end_child() + +3. Auto-scroll: after imgui.begin_child, if the stream content changed since last frame, call imgui.set_scroll_here_y(1.0). Track last-seen content length in a dict self._tier_stream_last_len: dict[str, int] = {} (init in __init__ if not present). Only scroll if user was at bottom: check imgui.get_scroll_y() >= imgui.get_scroll_max_y() - 10 before deciding to scroll. + +4. REMOVE the old lines completely: + - imgui.separator() (the second duplicate separator before the old strategy box) + - imgui.text("Strategy (Tier 1)") + - strategy_text = self.mma_streams.get("Tier 1", "") + - imgui.input_text_multiline("##mma_strategy", ...) + +5. Also add `self._tier_stream_last_len: dict[str, int] = {}` to __init__ (around line 280, after self.mma_streams = {} line). + +TESTS that must pass after this change: @tests/test_mma_dashboard_streams.py + +Use exactly 1-space indentation for Python code. +""" diff --git a/scripts/tasks/gui_ux_1_1_tests.toml b/scripts/tasks/gui_ux_1_1_tests.toml new file mode 100644 index 0000000..40a9e34 --- /dev/null +++ b/scripts/tasks/gui_ux_1_1_tests.toml @@ -0,0 +1,48 @@ +role = "tier3-worker" +docs = ["conductor/workflow.md"] +prompt = """ +Create tests/test_mma_dashboard_streams.py — failing tests for Task 1.1 of comprehensive_gui_ux track. + +CONTEXT: _render_mma_dashboard in @gui_2.py currently has a single input_text_multiline for "Strategy (Tier 1)" (around line 2729). Task 1.1 replaces it with four collapsible sections using imgui.collapsing_header(), one per tier. + +TESTS TO WRITE (all should FAIL against current code — they test the NEW behavior): + +1. test_all_four_tier_headers_rendered: + - Create a mock App with mma_streams = {"Tier 1": "strat", "Tier 2 (Tech Lead)": "lead", "Tier 3 (Worker): T-001": "work", "Tier 4 (QA)": "qa"} + - Patch imgui.collapsing_header to record calls, patch imgui.begin_child to return True, patch imgui.end_child, patch imgui.input_text_multiline + - Call app._render_mma_dashboard() + - Assert collapsing_header was called with a string containing "Tier 1" AND "Tier 2" AND "Tier 3" AND "Tier 4" + +2. test_tier3_aggregates_multiple_worker_streams: + - mma_streams = {"Tier 3 (Worker): T-001": "output1", "Tier 3 (Worker): T-002": "output2"} + - Patch imgui.collapsing_header to return True, patch begin_child/end_child/text + - Call _render_mma_dashboard() + - Assert imgui.text was called with content containing "T-001" and "T-002" (sub-headers shown) + +3. test_old_single_strategy_box_removed: + - With any mma_streams, call _render_mma_dashboard() + - Assert imgui.input_text_multiline was NOT called with "##mma_strategy" as first arg + - (The old box should be gone, replaced by collapsing_header sections) + +IMPLEMENTATION DETAILS: +- Import: import pytest, unittest.mock (patch, MagicMock, call) +- The App class needs minimal init to avoid full GUI startup. Use a MagicMock for the app instance and manually attach method: + app = MagicMock(spec=App) + app.mma_streams = {...} + app.mma_tier_usage = {} + app.tracks = [] + app.active_track = None + app.active_tickets = [] + app.mma_status = "idle" + app.active_tier = None + app.mma_step_mode = False + app._pending_mma_spawn = None + app._pending_mma_approval = None + app._pending_ask_dialog = False + # Then call the unbound method: + from gui_2 import App + App._render_mma_dashboard(app) +- Patch target: 'gui_2.imgui' module-level +- Use 1-space indentation throughout. +- All 3 tests should FAIL against the current code (which still has the old single text box). +""" diff --git a/tests/test_mma_dashboard_streams.py b/tests/test_mma_dashboard_streams.py new file mode 100644 index 0000000..ab63375 --- /dev/null +++ b/tests/test_mma_dashboard_streams.py @@ -0,0 +1,80 @@ +from __future__ import annotations +import pytest +from unittest.mock import patch, MagicMock + +from gui_2 import App + + +def _make_app(**kwargs): + app = MagicMock(spec=App) + app.mma_streams = kwargs.get("mma_streams", {}) + app.mma_tier_usage = kwargs.get("mma_tier_usage", { + "Tier 1": {"input": 0, "output": 0}, + "Tier 2": {"input": 0, "output": 0}, + "Tier 3": {"input": 0, "output": 0}, + "Tier 4": {"input": 0, "output": 0}, + }) + app.tracks = kwargs.get("tracks", []) + app.active_track = kwargs.get("active_track", None) + app.active_tickets = kwargs.get("active_tickets", []) + 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) + return app + + +def _make_imgui_mock(): + m = MagicMock() + m.begin_table.return_value = False + m.begin_child.return_value = False + m.checkbox.return_value = (False, False) + m.collapsing_header.return_value = False + m.ImVec2.return_value = MagicMock() + return m + + +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", + }) + 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) + 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.""" + 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" + )