From f36d539c365d1b23ba61474bc3e3cfe2843a7716 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Mon, 23 Feb 2026 19:20:20 -0500 Subject: [PATCH] feat(hooks): extend ApiHookClient and GUI for tab/listbox control --- api_hook_client.py | 31 ++++++++++++++ gui.py | 29 +++++++++---- tests/test_api_hook_extensions.py | 68 +++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 tests/test_api_hook_extensions.py diff --git a/api_hook_client.py b/api_hook_client.py index d422652..6224f03 100644 --- a/api_hook_client.py +++ b/api_hook_client.py @@ -83,3 +83,34 @@ class ApiHookClient: def post_gui(self, gui_data): return self._make_request('POST', '/api/gui', data=gui_data) + + def select_tab(self, tab_bar, tab): + """Tells the GUI to switch to a specific tab in a tab bar.""" + return self.post_gui({ + "action": "select_tab", + "tab_bar": tab_bar, + "tab": tab + }) + + def select_list_item(self, listbox, item_value): + """Tells the GUI to select an item in a listbox by its value.""" + return self.post_gui({ + "action": "select_list_item", + "listbox": listbox, + "item_value": item_value + }) + + def set_value(self, item, value): + """Sets the value of a GUI item.""" + return self.post_gui({ + "action": "set_value", + "item": item, + "value": value + }) + + def click(self, item): + """Simulates a click on a GUI button or item.""" + return self.post_gui({ + "action": "click", + "item": item + }) diff --git a/gui.py b/gui.py index 87c8637..63e1810 100644 --- a/gui.py +++ b/gui.py @@ -2100,10 +2100,10 @@ class App: height=200, ) with dpg.group(horizontal=True): - dpg.add_button(label="Gen + Send", callback=self.cb_generate_send) - dpg.add_button(label="MD Only", callback=self.cb_md_only) - dpg.add_button(label="Reset", callback=self.cb_reset_session) - dpg.add_button(label="-> History", callback=self.cb_append_message_to_history) + dpg.add_button(label="Gen + Send", tag="btn_gen_send", callback=self.cb_generate_send) + dpg.add_button(label="MD Only", tag="btn_md_only", callback=self.cb_md_only) + dpg.add_button(label="Reset", tag="btn_reset", callback=self.cb_reset_session) + dpg.add_button(label="-> History", tag="btn_to_history", callback=self.cb_append_message_to_history) with dpg.tab(label="AI Response"): dpg.add_input_text( @@ -2133,8 +2133,8 @@ class App: dpg.add_spacer(width=20) dpg.add_text("LIVE", tag="operations_live_indicator", color=(100, 255, 100), show=False) - with dpg.tab_bar(): - with dpg.tab(label="Comms Log"): + with dpg.tab_bar(tag="operations_tabs"): + with dpg.tab(label="Comms Log", tag="tab_comms"): with dpg.group(horizontal=True): dpg.add_text("Status: idle", tag="ai_status", color=(200, 220, 160)) dpg.add_spacer(width=16) @@ -2148,7 +2148,7 @@ class App: with dpg.child_window(tag="comms_scroll", height=-1, border=False, horizontal_scrollbar=True): pass - with dpg.tab(label="Tool Log"): + with dpg.tab(label="Tool Log", tag="tab_tool"): with dpg.group(horizontal=True): dpg.add_text("Tool call history") dpg.add_button(label="Clear", callback=self.cb_clear_tool_log) @@ -2305,6 +2305,21 @@ class App: cb = dpg.get_item_callback(item) if cb: cb() + elif action == "select_tab": + tab_bar = task.get("tab_bar") + tab = task.get("tab") + if tab_bar and dpg.does_item_exist(tab_bar): + dpg.set_value(tab_bar, tab) + elif action == "select_list_item": + listbox = task.get("listbox") + val = task.get("item_value") + if listbox and dpg.does_item_exist(listbox): + dpg.set_value(listbox, val) + cb = dpg.get_item_callback(listbox) + if cb: + # Dear PyGui callbacks for listbox usually receive (sender, app_data, user_data) + # app_data is the selected value. + cb(listbox, val) elif action == "refresh_api_metrics": self._refresh_api_metrics(task.get("payload", {})) except Exception as e: diff --git a/tests/test_api_hook_extensions.py b/tests/test_api_hook_extensions.py new file mode 100644 index 0000000..d4b2a43 --- /dev/null +++ b/tests/test_api_hook_extensions.py @@ -0,0 +1,68 @@ +import pytest +import sys +import os + +# Ensure project root is in path for imports +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +from api_hook_client import ApiHookClient + +def test_api_client_has_extensions(): + client = ApiHookClient() + # These should fail initially as they are not implemented + assert hasattr(client, 'select_tab') + assert hasattr(client, 'select_list_item') + +def test_select_tab_integration(live_gui): + client = ApiHookClient() + # We'll need to make sure the tags exist in gui.py + # For now, this is a placeholder for the integration test + response = client.select_tab("operations_tabs", "tab_tool") + assert response == {'status': 'queued'} + +def test_select_list_item_integration(live_gui): + client = ApiHookClient() + # Assuming 'Default' discussion exists or we can just test that it queues + response = client.select_list_item("disc_listbox", "Default") + assert response == {'status': 'queued'} + +def test_app_processes_new_actions(): + import gui + from unittest.mock import MagicMock, patch + import dearpygui.dearpygui as dpg + + dpg.create_context() + try: + with patch('gui.load_config', return_value={}), \ + patch('gui.PerformanceMonitor'), \ + patch('gui.shell_runner'), \ + patch('gui.project_manager'), \ + patch.object(gui.App, '_load_active_project'): + app = gui.App() + + with patch('dearpygui.dearpygui.set_value') as mock_set_value, \ + patch('dearpygui.dearpygui.does_item_exist', return_value=True), \ + patch('dearpygui.dearpygui.get_item_callback') as mock_get_cb: + + # Test select_tab + app._pending_gui_tasks.append({ + "action": "select_tab", + "tab_bar": "some_tab_bar", + "tab": "some_tab" + }) + app._process_pending_gui_tasks() + mock_set_value.assert_any_call("some_tab_bar", "some_tab") + + # Test select_list_item + mock_cb = MagicMock() + mock_get_cb.return_value = mock_cb + app._pending_gui_tasks.append({ + "action": "select_list_item", + "listbox": "some_listbox", + "item_value": "some_value" + }) + app._process_pending_gui_tasks() + mock_set_value.assert_any_call("some_listbox", "some_value") + mock_cb.assert_called_with("some_listbox", "some_value") + finally: + dpg.destroy_context()