From ecc5a660276337d2a6ab7bf0405ca73e41349786 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Tue, 5 May 2026 21:57:08 -0400 Subject: [PATCH] feat(workspace): implement contextual auto-switch layout based on MMA active tier --- conductor/tests/verify_phase_4.py | 23 +++++++++++++++ src/app_controller.py | 16 +++++++++- src/gui_2.py | 14 +++++++++ tests/test_auto_switch_sim.py | 49 +++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 conductor/tests/verify_phase_4.py create mode 100644 tests/test_auto_switch_sim.py diff --git a/conductor/tests/verify_phase_4.py b/conductor/tests/verify_phase_4.py new file mode 100644 index 0000000..8dcc7a3 --- /dev/null +++ b/conductor/tests/verify_phase_4.py @@ -0,0 +1,23 @@ +import subprocess +import sys +import os + +def verify_phase_4(): + print("Verifying Phase 4: Contextual Auto-Switch...") + + result = subprocess.run( + ["uv", "run", "pytest", "tests/test_auto_switch_sim.py"], + capture_output=True, + text=True + ) + + if result.returncode == 0: + print("Phase 4 verification PASSED.") + else: + print("Phase 4 verification FAILED.") + print(result.stdout) + print(result.stderr) + sys.exit(1) + +if __name__ == "__main__": + verify_phase_4() diff --git a/src/app_controller.py b/src/app_controller.py index 1b1456b..0dd9137 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -406,7 +406,9 @@ class AppController: 'text_viewer_title': 'text_viewer_title', 'text_viewer_type': 'text_viewer_type', 'disc_entries': 'disc_entries', - 'ui_file_paths': 'ui_file_paths' + 'ui_file_paths': 'ui_file_paths', + 'ui_auto_switch_layout': 'ui_auto_switch_layout', + 'ui_tier_layout_bindings': 'ui_tier_layout_bindings' } self._gettable_fields = dict(self._settable_fields) self._gettable_fields.update({ @@ -800,8 +802,18 @@ class AppController: sys.stderr.flush() self.mma_status = p.get("status", self.mma_status) + + old_tier = self.active_tier self.active_tier = p.get("active_tier", self.active_tier) + if getattr(self, "ui_auto_switch_layout", False) and self.active_tier and self.active_tier != old_tier: + for tier_prefix in ["Tier 1", "Tier 2", "Tier 3", "Tier 4"]: + if self.active_tier.startswith(tier_prefix): + bound_profile = getattr(self, "ui_tier_layout_bindings", {}).get(tier_prefix) + if bound_profile: + self._cb_load_workspace_profile(bound_profile) + break + # Preserve existing model/provider config if not explicitly in payload new_usage = p.get("tier_usage", {}) for tier, data in new_usage.items(): @@ -1121,6 +1133,8 @@ class AppController: self.ui_project_preset_name = proj_meta.get("active_preset") gui_cfg = self.config.get("gui", {}) + self.ui_auto_switch_layout = gui_cfg.get("auto_switch_layout", False) + self.ui_tier_layout_bindings = gui_cfg.get("tier_layout_bindings", {"Tier 1": "", "Tier 2": "", "Tier 3": "", "Tier 4": ""}) from src import bg_shader bg_shader.get_bg().enabled = gui_cfg.get("bg_shader_enabled", False) diff --git a/src/gui_2.py b/src/gui_2.py index 9253845..1737fd7 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -972,6 +972,20 @@ class App: if imgui.begin_tab_item("External Tools")[0]: self._render_external_tools_panel() imgui.end_tab_item() + if imgui.begin_tab_item("Workspace Layouts")[0]: + imgui.text("Experimental: Auto-switch layout by Tier") + ch, self.controller.ui_auto_switch_layout = imgui.checkbox("Enable Auto-Switch", self.controller.ui_auto_switch_layout) + if self.controller.ui_auto_switch_layout: + imgui.separator() + imgui.text("Tier Bindings (select profile for each tier)") + profiles = [""] + [p.name for p in self.controller.workspace_profiles.values()] + for t in ["Tier 1", "Tier 2", "Tier 3", "Tier 4"]: + curr = self.controller.ui_tier_layout_bindings.get(t, "") + idx = profiles.index(curr) if curr in profiles else 0 + ch_combo, new_idx = imgui.combo(t, idx, profiles) + if ch_combo: + self.controller.ui_tier_layout_bindings[t] = profiles[new_idx] + imgui.end_tab_item() imgui.end_tab_bar() imgui.end() diff --git a/tests/test_auto_switch_sim.py b/tests/test_auto_switch_sim.py new file mode 100644 index 0000000..88165c1 --- /dev/null +++ b/tests/test_auto_switch_sim.py @@ -0,0 +1,49 @@ +import pytest +import time +import sys +import os + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "src"))) + +from src import api_hook_client + +@pytest.mark.integration +def test_auto_switch_sim(live_gui): + client = api_hook_client.ApiHookClient() + assert client.wait_for_server(timeout=15), "Hook server did not start" + + # Reset layout and save a test profile for Tier 3 + client.set_value('show_windows', {'Diagnostics': True}) + client.push_event('custom_callback', {'callback': 'save_workspace_profile', 'args': ['Tier3Profile', 'project']}) + time.sleep(1) + + # Reset layout to something else + client.set_value('show_windows', {'Diagnostics': False}) + + # Enable auto switch and bind + client.set_value('ui_auto_switch_layout', True) + client.set_value('ui_tier_layout_bindings', {'Tier 1': '', 'Tier 2': '', 'Tier 3': 'Tier3Profile', 'Tier 4': ''}) + + # Send mma_state_update event to trigger Tier 2 + # Since we can't send raw asyncio events easily via ApiHookClient without a dedicated endpoint, + # we can simulate it by setting the active_tier via the Hook API if it triggers the logic, + # OR we can just inject an event into the app's event queue via custom_callback. + + def trigger_tier(tier): + # Inject mma_state_update task directly via hook API + client.push_event("mma_state_update", {"status": "running", "active_tier": tier}) + + # First Tier 2 + trigger_tier('Tier 2 (Tech Lead)') + time.sleep(1) + assert client.get_value('show_windows').get('Diagnostics', False) == False + + # Then Tier 3 + trigger_tier('Tier 3 (Worker): task-1') + time.sleep(1) + + # Verify + assert client.get_value('show_windows').get('Diagnostics', False) == True + + print("Contextual auto-switch simulation PASSED.")