From 94e41d20ff90c4dc330d4d0d37917489df94b551 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Wed, 25 Feb 2026 18:39:36 -0500 Subject: [PATCH] chore(conductor): Archive gemini_cli_headless_20260224 track and update tests --- .gemini/settings.json | 25 +++++-- .../gemini_cli_headless_20260224/index.md | 0 .../metadata.json | 0 .../gemini_cli_headless_20260224/plan.md | 0 .../gemini_cli_headless_20260224/spec.md | 0 conductor/product.md | 3 +- conductor/tech-stack.md | 2 + conductor/tracks.md | 3 - config.toml | 2 +- manualslop_layout.ini | 58 +++++++-------- tests/temp_project.toml | 3 + tests/temp_project_history.toml | 28 ++++++- tests/test_gemini_cli_adapter.py | 11 ++- tests/test_gemini_cli_integration.py | 73 +++++++++++++++++++ tests/test_sync_hooks.py | 36 +++------ 15 files changed, 173 insertions(+), 71 deletions(-) rename conductor/{tracks => archive}/gemini_cli_headless_20260224/index.md (100%) rename conductor/{tracks => archive}/gemini_cli_headless_20260224/metadata.json (100%) rename conductor/{tracks => archive}/gemini_cli_headless_20260224/plan.md (100%) rename conductor/{tracks => archive}/gemini_cli_headless_20260224/spec.md (100%) diff --git a/.gemini/settings.json b/.gemini/settings.json index d27ca26..11e40f6 100644 --- a/.gemini/settings.json +++ b/.gemini/settings.json @@ -1,10 +1,19 @@ { - "hooks": [ - { - "name": "manual-slop-bridge", - "type": "command", - "event": "BeforeTool", - "command": "python C:/projects/manual_slop/scripts/cli_tool_bridge.py" - } - ] + "hooks": { + "BeforeTool": [ + { + "matcher": "*", + "hooks": [ + { + "name": "manual-slop-bridge", + "type": "command", + "command": "python C:/projects/manual_slop/scripts/cli_tool_bridge.py" + } + ] + } + ] + }, + "hooksConfig": { + "enabled": true + } } diff --git a/conductor/tracks/gemini_cli_headless_20260224/index.md b/conductor/archive/gemini_cli_headless_20260224/index.md similarity index 100% rename from conductor/tracks/gemini_cli_headless_20260224/index.md rename to conductor/archive/gemini_cli_headless_20260224/index.md diff --git a/conductor/tracks/gemini_cli_headless_20260224/metadata.json b/conductor/archive/gemini_cli_headless_20260224/metadata.json similarity index 100% rename from conductor/tracks/gemini_cli_headless_20260224/metadata.json rename to conductor/archive/gemini_cli_headless_20260224/metadata.json diff --git a/conductor/tracks/gemini_cli_headless_20260224/plan.md b/conductor/archive/gemini_cli_headless_20260224/plan.md similarity index 100% rename from conductor/tracks/gemini_cli_headless_20260224/plan.md rename to conductor/archive/gemini_cli_headless_20260224/plan.md diff --git a/conductor/tracks/gemini_cli_headless_20260224/spec.md b/conductor/archive/gemini_cli_headless_20260224/spec.md similarity index 100% rename from conductor/tracks/gemini_cli_headless_20260224/spec.md rename to conductor/archive/gemini_cli_headless_20260224/spec.md diff --git a/conductor/product.md b/conductor/product.md index b28a2fd..ff2eb9a 100644 --- a/conductor/product.md +++ b/conductor/product.md @@ -20,4 +20,5 @@ To serve as an expert-level utility for personal developer use on small projects - **Performance Diagnostics:** Built-in telemetry for FPS, Frame Time, and CPU usage, with a dedicated Diagnostics Panel and AI API hooks for performance analysis. - **Automated UX Verification:** A robust IPC mechanism via API hooks and a modular simulation suite allows for human-like simulation walkthroughs and automated regression testing of the full GUI lifecycle across multiple specialized scenarios. - **Headless Backend Service:** Optional headless mode allowing the core AI and tool execution logic to run as a decoupled REST API service (FastAPI), optimized for Docker and server-side environments (e.g., Unraid). -- **Remote Confirmation Protocol:** A non-blocking, ID-based challenge/response mechanism for approving AI actions via the REST API, enabling remote "Human-in-the-Loop" safety. \ No newline at end of file +- **Remote Confirmation Protocol:** A non-blocking, ID-based challenge/response mechanism for approving AI actions via the REST API, enabling remote "Human-in-the-Loop" safety. +- **Gemini CLI Integration:** Allows using the `gemini` CLI as a headless backend provider. This enables leveraging Gemini subscriptions with advanced features like persistent sessions, while maintaining full "Human-in-the-Loop" safety through a dedicated bridge for synchronous tool call approvals within the Manual Slop GUI. \ No newline at end of file diff --git a/conductor/tech-stack.md b/conductor/tech-stack.md index ab91403..0337357 100644 --- a/conductor/tech-stack.md +++ b/conductor/tech-stack.md @@ -18,6 +18,7 @@ - **google-genai:** For Google Gemini API interaction and explicit context caching. - **anthropic:** For Anthropic Claude API interaction, supporting ephemeral prompt caching. +- **Gemini CLI:** Integrated as a headless backend provider, utilizing a custom subprocess adapter and bridge script for tool execution control. ## Configuration & Tooling @@ -32,3 +33,4 @@ ## Architectural Patterns - **Event-Driven Metrics:** Uses a custom `EventEmitter` to decouple API lifecycle events from UI rendering, improving performance and responsiveness. +- **Synchronous IPC Approval Flow:** A specialized bridge mechanism that allows headless AI providers (like Gemini CLI) to synchronously request and receive human approval for tool calls via the GUI's REST API hooks. diff --git a/conductor/tracks.md b/conductor/tracks.md index aefc497..f2dd595 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -35,7 +35,4 @@ This file tracks all major tracks for the project. Each track has its own detail --- -- [x] **Track: Support gemini cli headless as an alternative to the raw client_api route. So that they user may use their gemini subscription and gemini cli features within manual slop for a more discliplined and visually enriched UX.** -*Link: [./tracks/gemini_cli_headless_20260224/](./tracks/gemini_cli_headless_20260224/)* - diff --git a/config.toml b/config.toml index 1f6ea04..8bac0b4 100644 --- a/config.toml +++ b/config.toml @@ -1,5 +1,5 @@ [ai] -provider = "gemini" +provider = "gemini_cli" model = "gemini-2.5-flash-lite" temperature = 0.0 max_tokens = 8192 diff --git a/manualslop_layout.ini b/manualslop_layout.ini index ec19b90..356d17a 100644 --- a/manualslop_layout.ini +++ b/manualslop_layout.ini @@ -12,7 +12,7 @@ ViewportPos=43,95 ViewportId=0x78C57832 Size=897,649 Collapsed=0 -DockId=0x00000007,0 +DockId=0x00000006,0 [Window][Files] ViewportPos=3125,170 @@ -33,7 +33,7 @@ DockId=0x0000000A,0 Pos=0,17 Size=1680,730 Collapsed=0 -DockId=0x00000007,0 +DockId=0x00000006,0 [Window][Provider] ViewportPos=43,95 @@ -41,7 +41,7 @@ ViewportId=0x78C57832 Pos=0,651 Size=897,468 Collapsed=0 -DockId=0x00000007,0 +DockId=0x00000006,0 [Window][Message] Pos=0,749 @@ -79,9 +79,9 @@ DockId=0x0000000F,2 [Window][Theme] Pos=0,17 -Size=432,858 +Size=588,400 Collapsed=0 -DockId=0x00000007,2 +DockId=0x00000005,1 [Window][Text Viewer - Entry #7] Pos=379,324 @@ -89,16 +89,16 @@ Size=900,700 Collapsed=0 [Window][Diagnostics] -Pos=863,794 -Size=817,406 +Pos=590,17 +Size=530,1183 Collapsed=0 -DockId=0x00000006,0 +DockId=0x0000000E,1 [Window][Context Hub] Pos=0,17 -Size=432,858 +Size=588,400 Collapsed=0 -DockId=0x00000007,0 +DockId=0x00000005,0 [Window][AI Settings Hub] Pos=406,17 @@ -107,28 +107,28 @@ Collapsed=0 DockId=0x0000000D,0 [Window][Discussion Hub] -Pos=863,17 -Size=817,775 +Pos=1122,17 +Size=558,1183 Collapsed=0 -DockId=0x00000005,0 +DockId=0x00000004,0 [Window][Operations Hub] -Pos=434,17 -Size=427,858 +Pos=590,17 +Size=530,1183 Collapsed=0 DockId=0x0000000E,0 [Window][Files & Media] -Pos=0,17 -Size=432,858 +Pos=0,419 +Size=588,781 Collapsed=0 -DockId=0x00000007,1 +DockId=0x00000006,1 [Window][AI Settings] -Pos=0,877 -Size=861,323 +Pos=0,419 +Size=588,781 Collapsed=0 -DockId=0x00000013,0 +DockId=0x00000006,0 [Docking][Data] DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y @@ -136,18 +136,16 @@ DockNode ID=0x00000008 Pos=3125,170 Size=593,1157 Split=Y DockNode ID=0x0000000A Parent=0x00000008 SizeRef=1029,145 Selected=0xDF822E02 DockSpace ID=0xAFC85805 Window=0x079D3A04 Pos=0,17 Size=1680,1183 Split=Y DockNode ID=0x0000000C Parent=0xAFC85805 SizeRef=1362,1041 Split=X Selected=0x5D11106F - DockNode ID=0x00000003 Parent=0x0000000C SizeRef=861,1183 Split=X + DockNode ID=0x00000003 Parent=0x0000000C SizeRef=1120,1183 Split=X DockNode ID=0x0000000B Parent=0x00000003 SizeRef=404,1186 Split=Y Selected=0xF4139CA2 - DockNode ID=0x00000002 Parent=0x0000000B SizeRef=1029,1119 Split=Y Selected=0xF4139CA2 - DockNode ID=0x00000010 Parent=0x00000002 SizeRef=432,328 Split=X Selected=0x8CA2375C - DockNode ID=0x00000007 Parent=0x00000010 SizeRef=432,858 CentralNode=1 Selected=0x8CA2375C - DockNode ID=0x0000000E Parent=0x00000010 SizeRef=427,858 Selected=0x418C7449 - DockNode ID=0x00000013 Parent=0x00000002 SizeRef=432,323 Selected=0x7BD57D6A + DockNode ID=0x00000002 Parent=0x0000000B SizeRef=1029,1119 Split=X Selected=0xF4139CA2 + DockNode ID=0x00000007 Parent=0x00000002 SizeRef=588,858 Split=Y Selected=0x8CA2375C + DockNode ID=0x00000005 Parent=0x00000007 SizeRef=295,400 Selected=0xF4139CA2 + DockNode ID=0x00000006 Parent=0x00000007 SizeRef=295,781 CentralNode=1 Selected=0x7BD57D6A + DockNode ID=0x0000000E Parent=0x00000002 SizeRef=530,858 Selected=0x418C7449 DockNode ID=0x00000001 Parent=0x0000000B SizeRef=1029,775 Selected=0x8B4EBFA6 DockNode ID=0x0000000D Parent=0x00000003 SizeRef=435,1186 Selected=0x363E93D6 - DockNode ID=0x00000004 Parent=0x0000000C SizeRef=817,1183 Split=Y Selected=0x418C7449 - DockNode ID=0x00000005 Parent=0x00000004 SizeRef=837,775 Selected=0x6F2B5B04 - DockNode ID=0x00000006 Parent=0x00000004 SizeRef=837,406 Selected=0xB4CBF21A + DockNode ID=0x00000004 Parent=0x0000000C SizeRef=558,1183 Selected=0x6F2B5B04 DockNode ID=0x0000000F Parent=0xAFC85805 SizeRef=1362,451 Selected=0xDD6419BC ;;;<<>>;;; diff --git a/tests/temp_project.toml b/tests/temp_project.toml index 98cd07d..8b417a8 100644 --- a/tests/temp_project.toml +++ b/tests/temp_project.toml @@ -27,3 +27,6 @@ search_files = true get_file_summary = true web_search = true fetch_url = true + +[gemini_cli] +binary_path = "\"C:\\projects\\manual_slop\\.venv\\Scripts\\python.exe\" \"C:\\projects\\manual_slop\\tests\\mock_gemini_cli.py\"" diff --git a/tests/temp_project_history.toml b/tests/temp_project_history.toml index ea7751e..08002c8 100644 --- a/tests/temp_project_history.toml +++ b/tests/temp_project_history.toml @@ -4,12 +4,12 @@ roles = [ "Vendor API", "System", ] -active = "main" +active = "testing gemini cli 2" auto_add = true [discussions.main] git_commit = "" -last_updated = "2026-02-25T13:33:02" +last_updated = "2026-02-25T18:29:54" history = [ "@1772042443.1159382\nUser:\nStress test entry 0 Stress test entry 0 Stress test entry 0 Stress test entry 0 Stress test entry 0", "@1772042443.1159382\nUser:\nStress test entry 1 Stress test entry 1 Stress test entry 1 Stress test entry 1 Stress test entry 1", @@ -62,3 +62,27 @@ history = [ "@1772042443.1159382\nUser:\nStress test entry 48 Stress test entry 48 Stress test entry 48 Stress test entry 48 Stress test entry 48", "@1772042443.1159382\nUser:\nStress test entry 49 Stress test entry 49 Stress test entry 49 Stress test entry 49 Stress test entry 49", ] + +[discussions."testing gemini cli"] +git_commit = "" +last_updated = "2026-02-25T18:29:49" +history = [ + "@2026-02-25T14:38:14\nUser:\nyo gemini cli you there?", + "@2026-02-25T14:38:21\nAI:\nTool execution was denied. Decision: deny", + "@2026-02-25T14:39:03\nSystem:\n[PERFORMANCE ALERT] Frame time high: 7956.5ms. Please consider optimizing recent changes or reducing load.", + "@2026-02-25T14:39:42\nSystem:\n[PERFORMANCE ALERT] Frame time high: 9193.6ms. Please consider optimizing recent changes or reducing load.", + "@2026-02-25T14:41:03\nSystem:\n[PERFORMANCE ALERT] Frame time high: 6645.0ms. Please consider optimizing recent changes or reducing load.", + "@2026-02-25T14:41:37\nSystem:\n[PERFORMANCE ALERT] Frame time high: 10553.5ms. Please consider optimizing recent changes or reducing load.", +] + +[discussions."testing gemini cli 2"] +git_commit = "" +last_updated = "2026-02-25T18:38:17" +history = [ + "@2026-02-25T18:35:22\nAI:\nTool execution was denied. Decision: deny", + "@2026-02-25T18:35:44\nUser:\nPlease read test.txt", + "@2026-02-25T18:35:45\nAI:\nI read the file. It contains: 'Hello from mock!'", + "@2026-02-25T18:35:46\nUser:\nDeny me", + "@2026-02-25T18:35:46\nAI:\nTool execution was denied. Decision: deny", + "@2026-02-25T18:38:17\nUser:\nDeny me", +] diff --git a/tests/test_gemini_cli_adapter.py b/tests/test_gemini_cli_adapter.py index 79651d9..43966c8 100644 --- a/tests/test_gemini_cli_adapter.py +++ b/tests/test_gemini_cli_adapter.py @@ -19,11 +19,12 @@ class TestGeminiCliAdapter(unittest.TestCase): def test_send_starts_subprocess_with_correct_args(self, mock_popen): """ Verify that send(message) correctly starts the subprocess with - --output-format stream-json and the provided message. + --output-format stream-json and the provided message via stdin. """ # Setup mock process with a minimal valid JSONL termination process_mock = MagicMock() process_mock.stdout = io.StringIO(json.dumps({"type": "result", "usage": {}}) + "\n") + process_mock.stdin = MagicMock() process_mock.poll.return_value = 0 process_mock.wait.return_value = 0 mock_popen.return_value = process_mock @@ -40,10 +41,16 @@ class TestGeminiCliAdapter(unittest.TestCase): self.assertIn("gemini", cmd) self.assertIn("--output-format", cmd) self.assertIn("stream-json", cmd) - self.assertIn(message, cmd) + # Message should NOT be in cmd now + self.assertNotIn(message, cmd) + # Verify message was written to stdin + process_mock.stdin.write.assert_called_once_with(message) + process_mock.stdin.close.assert_called_once() + # Check process configuration self.assertEqual(kwargs.get('stdout'), subprocess.PIPE) + self.assertEqual(kwargs.get('stdin'), subprocess.PIPE) self.assertEqual(kwargs.get('text'), True) @patch('subprocess.Popen') diff --git a/tests/test_gemini_cli_integration.py b/tests/test_gemini_cli_integration.py index d1bf984..bc146aa 100644 --- a/tests/test_gemini_cli_integration.py +++ b/tests/test_gemini_cli_integration.py @@ -84,3 +84,76 @@ def test_gemini_cli_full_integration(live_gui): time.sleep(1.0) assert final_message_received, "Final message from mock CLI was not found in the GUI history" + +def test_gemini_cli_rejection_and_history(live_gui): + """ + Integration test for the Gemini CLI provider: Rejection flow and history. + """ + client = ApiHookClient("http://127.0.0.1:8999") + + # 1. Setup paths and configure the GUI + mock_script = os.path.abspath("tests/mock_gemini_cli.py") + cli_cmd = f'"{sys.executable}" "{mock_script}"' + + client.set_value("current_provider", "gemini_cli") + client.set_value("gcli_path", cli_cmd) + + # 2. Trigger a message that will be denied + print("[TEST] Sending user message (to be denied)...") + client.set_value("ai_input", "Deny me") + client.click("btn_gen_send") + + # 3. Wait for 'ask_received' and respond with rejection + request_id = None + timeout = 15 + start_time = time.time() + while time.time() - start_time < timeout: + for ev in client.get_events(): + if ev.get("type") == "ask_received": + request_id = ev.get("request_id") + break + if request_id: break + time.sleep(0.5) + + assert request_id is not None + + print("[TEST] Responding to ask with REJECTION") + requests.post("http://127.0.0.1:8999/api/ask/respond", + json={"request_id": request_id, "response": {"approved": False}}) + + # 4. Verify rejection message in history + print("[TEST] Waiting for rejection message in history...") + rejection_found = False + start_time = time.time() + while time.time() - start_time < timeout: + session = client.get_session() + entries = session.get("session", {}).get("entries", []) + for entry in entries: + if "Tool execution was denied. Decision: deny" in entry.get("content", ""): + rejection_found = True + break + if rejection_found: break + time.sleep(1.0) + + assert rejection_found, "Rejection message not found in history" + + # 5. Send a follow-up message and verify history grows + print("[TEST] Sending follow-up message...") + client.set_value("ai_input", "What happened?") + client.click("btn_gen_send") + + # Wait for mock to finish (it will just return a message) + time.sleep(2) + session = client.get_session() + entries = session.get("session", {}).get("entries", []) + # Should have: + # 1. User: Deny me + # 2. AI: Tool execution was denied... + # 3. User: What happened? + # 4. AI: ... + print(f"[TEST] Final history length: {len(entries)}") + for i, entry in enumerate(entries): + print(f" {i}: {entry.get('role')} - {entry.get('content')[:30]}...") + + assert len(entries) >= 4 + diff --git a/tests/test_sync_hooks.py b/tests/test_sync_hooks.py index 954b2ff..25f0e11 100644 --- a/tests/test_sync_hooks.py +++ b/tests/test_sync_hooks.py @@ -4,15 +4,10 @@ import requests import pytest from api_hook_client import ApiHookClient -def test_api_ask_synchronous_flow(live_gui): +def test_api_ask_client_method(live_gui): """ - Tests the full synchronous lifecycle of the /api/ask endpoint: - 1. A client makes a blocking request. - 2. An event is emitted with a unique request_id. - 3. A separate agent responds to that request_id. - 4. The original blocking request completes with the provided data. + Tests the request_confirmation method in ApiHookClient. """ - # The live_gui fixture starts the Manual Slop application with hooks on 8999. client = ApiHookClient("http://127.0.0.1:8999") # Drain existing events @@ -22,14 +17,11 @@ def test_api_ask_synchronous_flow(live_gui): def make_blocking_request(): try: - # This POST will block until we call /api/ask/respond - # Note: /api/ask returns {'status': 'ok', 'response': ...} - resp = requests.post( - "http://127.0.0.1:8999/api/ask", - json={"prompt": "Should we proceed with the refactor?"}, - timeout=10 + # This call should block until we respond + results["response"] = client.request_confirmation( + tool_name="powershell", + args={"command": "echo hello"} ) - results["response"] = resp.json() except Exception as e: results["error"] = str(e) @@ -37,7 +29,7 @@ def test_api_ask_synchronous_flow(live_gui): t = threading.Thread(target=make_blocking_request) t.start() - # Poll for the 'ask_received' event to find the generated request_id + # Poll for the 'ask_received' event request_id = None start_time = time.time() while time.time() - start_time < 5: @@ -52,8 +44,8 @@ def test_api_ask_synchronous_flow(live_gui): assert request_id is not None, "Timed out waiting for 'ask_received' event" - # Respond to the task via the respond endpoint - expected_response = {"approved": True, "message": "Proceeding as requested."} + # Respond + expected_response = {"approved": True} resp = requests.post( "http://127.0.0.1:8999/api/ask/respond", json={ @@ -63,11 +55,7 @@ def test_api_ask_synchronous_flow(live_gui): ) assert resp.status_code == 200 - # Join the thread and verify the original request received the correct data t.join(timeout=5) - assert not t.is_alive(), "Background thread failed to unblock" - assert results["error"] is None, f"Request failed: {results['error']}" - - # The /api/ask endpoint returns {'status': 'ok', 'response': expected_response} - assert results["response"]["status"] == "ok" - assert results["response"]["response"] == expected_response + assert not t.is_alive() + assert results["error"] is None + assert results["response"] == expected_response