diff --git a/src/api_hook_client.py b/src/api_hook_client.py index 728f8c7..b6f00d8 100644 --- a/src/api_hook_client.py +++ b/src/api_hook_client.py @@ -84,6 +84,11 @@ class ApiHookClient: """Retrieves current MMA status (track, tickets, tier, etc.)""" return self._make_request('GET', '/api/gui/mma_status') + def get_gui_state(self) -> dict | None: + """Retrieves the current GUI state via /api/gui/state.""" + resp = self._make_request("GET", "/api/gui/state") + return resp if resp else None + def push_event(self, event_type: str, payload: dict[str, Any]) -> dict[str, Any] | None: """Pushes an event to the GUI's AsyncEventQueue via the /api/gui endpoint.""" return self.post_gui({ diff --git a/src/api_hooks.py b/src/api_hooks.py index 7ab3d69..3f5a646 100644 --- a/src/api_hooks.py +++ b/src/api_hooks.py @@ -165,6 +165,27 @@ class HookHandler(BaseHTTPRequestHandler): else: self.send_response(504) self.end_headers() + elif self.path == '/api/gui/state': + event = threading.Event() + result = {} + def get_state(): + try: + gettable = _get_app_attr(app, "_gettable_fields", {}) + for key, attr in gettable.items(): + result[key] = _get_app_attr(app, attr, None) + finally: event.set() + lock = _get_app_attr(app, "_pending_gui_tasks_lock") + tasks = _get_app_attr(app, "_pending_gui_tasks") + if lock and tasks is not None: + with lock: tasks.append({"action": "custom_callback", "callback": get_state}) + if event.wait(timeout=10): + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.end_headers() + self.wfile.write(json.dumps(result).encode("utf-8")) + else: + self.send_response(504) + self.end_headers() else: self.send_response(404) self.end_headers() diff --git a/src/app_controller.py b/src/app_controller.py index 5e58430..c4c4ab2 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -707,6 +707,13 @@ class AppController: 'manual_approve': 'ui_manual_approve' } + self._gettable_fields = dict(self._settable_fields) + self._gettable_fields.update({ + 'ui_focus_agent': 'ui_focus_agent', + 'active_discussion': 'active_discussion', + '_track_discussion_active': '_track_discussion_active' + }) + self.hook_server = api_hooks.HookServer(app if app else self) self.hook_server.start() @@ -1004,6 +1011,15 @@ class AppController: """Returns the health status of the API.""" return {"status": "ok"} + @api.get("/api/gui/state", dependencies=[Depends(get_api_key)]) + def get_gui_state() -> dict[str, Any]: + """Returns the current GUI state for specific fields.""" + gettable = getattr(self, "_gettable_fields", {}) + state = {} + for key, attr in gettable.items(): + state[key] = getattr(self, attr, None) + return state + @api.get("/status", dependencies=[Depends(get_api_key)]) def status() -> dict[str, Any]: """Returns the current status of the application.""" diff --git a/tests/test_live_gui_integration.py b/tests/test_live_gui_integration.py index dbba1be..d1d881c 100644 --- a/tests/test_live_gui_integration.py +++ b/tests/test_live_gui_integration.py @@ -4,6 +4,7 @@ import asyncio import time from gui_2 import App from events import UserRequestEvent +from api_hook_client import ApiHookClient @pytest.mark.timeout(10) @pytest.mark.asyncio @@ -38,7 +39,9 @@ async def test_user_request_integration_flow(mock_app: App) -> None: mock_send.assert_called_once_with( "Context", "Hello AI", ".", [], "History", pre_tool_callback=ANY, - qa_callback=ANY + qa_callback=ANY, + stream=ANY, + stream_callback=ANY ) # 4. Wait for the response to propagate to _pending_gui_tasks and update UI # We call _process_pending_gui_tasks manually to simulate a GUI frame update. @@ -85,3 +88,22 @@ async def test_user_request_error_handling(mock_app: App) -> None: break await asyncio.sleep(0.1) assert success, f"Error state was not reflected in UI. status: {app.ai_status}, response: {app.ai_response}" + +def test_api_gui_state_live(live_gui) -> None: + client = ApiHookClient() + client.set_value('current_provider', 'anthropic') + client.set_value('current_model', 'claude-3-haiku-20240307') + + start_time = time.time() + success = False + while time.time() - start_time < 10: + state = client.get_gui_state() + if state and state.get('current_provider') == 'anthropic' and state.get('current_model') == 'claude-3-haiku-20240307': + success = True + break + time.sleep(0.5) + + assert success, f"GUI state did not update. Got: {client.get_gui_state()}" + final_state = client.get_gui_state() + assert final_state['current_provider'] == 'anthropic' + assert final_state['current_model'] == 'claude-3-haiku-20240307'