feat(gui): Replace single strategy box with 4-tier collapsible stream panels
This commit is contained in:
59
gui_2.py
59
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:
|
||||
|
||||
55
scripts/tasks/gui_ux_1_1_impl.toml
Normal file
55
scripts/tasks/gui_ux_1_1_impl.toml
Normal file
@@ -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.
|
||||
"""
|
||||
48
scripts/tasks/gui_ux_1_1_tests.toml
Normal file
48
scripts/tasks/gui_ux_1_1_tests.toml
Normal file
@@ -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).
|
||||
"""
|
||||
80
tests/test_mma_dashboard_streams.py
Normal file
80
tests/test_mma_dashboard_streams.py
Normal file
@@ -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"
|
||||
)
|
||||
Reference in New Issue
Block a user