diff --git a/conductor/tracks/manual_slop_headless_20260225/plan.md b/conductor/tracks/manual_slop_headless_20260225/plan.md index f165102..d4d82f5 100644 --- a/conductor/tracks/manual_slop_headless_20260225/plan.md +++ b/conductor/tracks/manual_slop_headless_20260225/plan.md @@ -2,12 +2,12 @@ ## Phase 1: Project Setup & Headless Scaffold - [x] Task: Update dependencies (02fc847) - - [ ] Add `fastapi` and `uvicorn` to `pyproject.toml` (and sync `requirements.txt` via `uv`). -- [~] Task: Implement headless startup - - [ ] Modify `gui_2.py` (or create `headless.py`) to parse a `--headless` CLI flag. - - [ ] Update config parsing in `config.toml` to support headless configuration sections. - - [ ] Bypass Dear PyGui initialization if headless mode is active. -- [ ] Task: Create foundational API application + - [x] Add `fastapi` and `uvicorn` to `pyproject.toml` (and sync `requirements.txt` via `uv`). +- [x] Task: Implement headless startup + - [x] Modify `gui_2.py` (or create `headless.py`) to parse a `--headless` CLI flag. + - [x] Update config parsing in `config.toml` to support headless configuration sections. + - [x] Bypass Dear PyGui initialization if headless mode is active. +- [~] Task: Create foundational API application - [ ] Set up the core FastAPI application instance. - [ ] Implement `/health` and `/status` endpoints for Docker lifecycle checks. - [ ] Task: Conductor - User Manual Verification 'Project Setup & Headless Scaffold' (Protocol in workflow.md) diff --git a/config.toml b/config.toml index bd3e32b..287ba2a 100644 --- a/config.toml +++ b/config.toml @@ -32,3 +32,7 @@ active = "C:\\projects\\manual_slop\\tests\\temp_project.toml" "Operations Hub" = true Theme = true Diagnostics = true + +[headless] +port = 8000 +api_key = "" diff --git a/gui_2.py b/gui_2.py index 60408d5..67b1e41 100644 --- a/gui_2.py +++ b/gui_2.py @@ -22,6 +22,7 @@ import api_hooks import mcp_client from performance_monitor import PerformanceMonitor +from fastapi import FastAPI from imgui_bundle import imgui, hello_imgui, immapp CONFIG_PATH = Path("config.toml") @@ -303,6 +304,25 @@ class App: self._discussion_names_cache = [] self._discussion_names_dirty = True + def create_api(self) -> FastAPI: + api = FastAPI(title="Manual Slop Headless API") + + @api.get("/health") + def health(): + return {"status": "ok"} + + @api.get("/status") + def status(): + return { + "provider": self.current_provider, + "model": self.current_model, + "active_project": self.active_project_path, + "ai_status": self.ai_status, + "session_usage": self.session_usage + } + + return api + # ---------------------------------------------------------------- project loading def _cb_new_project_automated(self, user_data): @@ -2001,33 +2021,45 @@ class App: def run(self): """Initializes the ImGui runner and starts the main application loop.""" - theme.load_from_config(self.config) + if "--headless" in sys.argv: + print("Headless mode active") + self._fetch_models(self.current_provider) + + import uvicorn + headless_cfg = self.config.get("headless", {}) + port = headless_cfg.get("port", 8000) + + api = self.create_api() + uvicorn.run(api, host="0.0.0.0", port=port) + else: + theme.load_from_config(self.config) - self.runner_params = hello_imgui.RunnerParams() - self.runner_params.app_window_params.window_title = "manual slop" - self.runner_params.app_window_params.window_geometry.size = (1680, 1200) - self.runner_params.imgui_window_params.enable_viewports = False - self.runner_params.imgui_window_params.default_imgui_window_type = hello_imgui.DefaultImGuiWindowType.provide_full_screen_dock_space - self.runner_params.fps_idling.enable_idling = False - self.runner_params.imgui_window_params.show_menu_bar = True - self.runner_params.ini_folder_type = hello_imgui.IniFolderType.current_folder - self.runner_params.ini_filename = "manualslop_layout.ini" + self.runner_params = hello_imgui.RunnerParams() + self.runner_params.app_window_params.window_title = "manual slop" + self.runner_params.app_window_params.window_geometry.size = (1680, 1200) + self.runner_params.imgui_window_params.enable_viewports = False + self.runner_params.imgui_window_params.default_imgui_window_type = hello_imgui.DefaultImGuiWindowType.provide_full_screen_dock_space + self.runner_params.fps_idling.enable_idling = False + self.runner_params.imgui_window_params.show_menu_bar = True + self.runner_params.ini_folder_type = hello_imgui.IniFolderType.current_folder + self.runner_params.ini_filename = "manualslop_layout.ini" - self.runner_params.callbacks.show_gui = self._gui_func - self.runner_params.callbacks.show_menus = self._show_menus - self.runner_params.callbacks.load_additional_fonts = self._load_fonts - self.runner_params.callbacks.post_init = self._post_init - - self._fetch_models(self.current_provider) + self.runner_params.callbacks.show_gui = self._gui_func + self.runner_params.callbacks.show_menus = self._show_menus + self.runner_params.callbacks.load_additional_fonts = self._load_fonts + self.runner_params.callbacks.post_init = self._post_init + + self._fetch_models(self.current_provider) - # Start API hooks server (if enabled) - self.hook_server = api_hooks.HookServer(self) - self.hook_server.start() + # Start API hooks server (if enabled) + self.hook_server = api_hooks.HookServer(self) + self.hook_server.start() - immapp.run(self.runner_params) + immapp.run(self.runner_params) + + # On exit + self.hook_server.stop() - # On exit - self.hook_server.stop() self.perf_monitor.stop() ai_client.cleanup() # Destroy active API caches to stop billing self._flush_to_project() diff --git a/project_history.toml b/project_history.toml index 030e4c6..cb69e4a 100644 --- a/project_history.toml +++ b/project_history.toml @@ -8,5 +8,5 @@ active = "main" [discussions.main] git_commit = "" -last_updated = "2026-02-25T11:19:43" +last_updated = "2026-02-25T13:02:29" history = [] diff --git a/tests/temp_project_history.toml b/tests/temp_project_history.toml index 68b7514..214d838 100644 --- a/tests/temp_project_history.toml +++ b/tests/temp_project_history.toml @@ -9,7 +9,56 @@ auto_add = true [discussions.main] git_commit = "" -last_updated = "2026-02-25T11:21:27" +last_updated = "2026-02-25T13:02:29" history = [ - "@2026-02-25T11:21:23\nSystem:\n[PERFORMANCE ALERT] CPU usage high: 82.8%. Please consider optimizing recent changes or reducing load.", + "@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", + "@1772042443.1159382\nUser:\nStress test entry 2 Stress test entry 2 Stress test entry 2 Stress test entry 2 Stress test entry 2", + "@1772042443.1159382\nUser:\nStress test entry 3 Stress test entry 3 Stress test entry 3 Stress test entry 3 Stress test entry 3", + "@1772042443.1159382\nUser:\nStress test entry 4 Stress test entry 4 Stress test entry 4 Stress test entry 4 Stress test entry 4", + "@1772042443.1159382\nUser:\nStress test entry 5 Stress test entry 5 Stress test entry 5 Stress test entry 5 Stress test entry 5", + "@1772042443.1159382\nUser:\nStress test entry 6 Stress test entry 6 Stress test entry 6 Stress test entry 6 Stress test entry 6", + "@1772042443.1159382\nUser:\nStress test entry 7 Stress test entry 7 Stress test entry 7 Stress test entry 7 Stress test entry 7", + "@1772042443.1159382\nUser:\nStress test entry 8 Stress test entry 8 Stress test entry 8 Stress test entry 8 Stress test entry 8", + "@1772042443.1159382\nUser:\nStress test entry 9 Stress test entry 9 Stress test entry 9 Stress test entry 9 Stress test entry 9", + "@1772042443.1159382\nUser:\nStress test entry 10 Stress test entry 10 Stress test entry 10 Stress test entry 10 Stress test entry 10", + "@1772042443.1159382\nUser:\nStress test entry 11 Stress test entry 11 Stress test entry 11 Stress test entry 11 Stress test entry 11", + "@1772042443.1159382\nUser:\nStress test entry 12 Stress test entry 12 Stress test entry 12 Stress test entry 12 Stress test entry 12", + "@1772042443.1159382\nUser:\nStress test entry 13 Stress test entry 13 Stress test entry 13 Stress test entry 13 Stress test entry 13", + "@1772042443.1159382\nUser:\nStress test entry 14 Stress test entry 14 Stress test entry 14 Stress test entry 14 Stress test entry 14", + "@1772042443.1159382\nUser:\nStress test entry 15 Stress test entry 15 Stress test entry 15 Stress test entry 15 Stress test entry 15", + "@1772042443.1159382\nUser:\nStress test entry 16 Stress test entry 16 Stress test entry 16 Stress test entry 16 Stress test entry 16", + "@1772042443.1159382\nUser:\nStress test entry 17 Stress test entry 17 Stress test entry 17 Stress test entry 17 Stress test entry 17", + "@1772042443.1159382\nUser:\nStress test entry 18 Stress test entry 18 Stress test entry 18 Stress test entry 18 Stress test entry 18", + "@1772042443.1159382\nUser:\nStress test entry 19 Stress test entry 19 Stress test entry 19 Stress test entry 19 Stress test entry 19", + "@1772042443.1159382\nUser:\nStress test entry 20 Stress test entry 20 Stress test entry 20 Stress test entry 20 Stress test entry 20", + "@1772042443.1159382\nUser:\nStress test entry 21 Stress test entry 21 Stress test entry 21 Stress test entry 21 Stress test entry 21", + "@1772042443.1159382\nUser:\nStress test entry 22 Stress test entry 22 Stress test entry 22 Stress test entry 22 Stress test entry 22", + "@1772042443.1159382\nUser:\nStress test entry 23 Stress test entry 23 Stress test entry 23 Stress test entry 23 Stress test entry 23", + "@1772042443.1159382\nUser:\nStress test entry 24 Stress test entry 24 Stress test entry 24 Stress test entry 24 Stress test entry 24", + "@1772042443.1159382\nUser:\nStress test entry 25 Stress test entry 25 Stress test entry 25 Stress test entry 25 Stress test entry 25", + "@1772042443.1159382\nUser:\nStress test entry 26 Stress test entry 26 Stress test entry 26 Stress test entry 26 Stress test entry 26", + "@1772042443.1159382\nUser:\nStress test entry 27 Stress test entry 27 Stress test entry 27 Stress test entry 27 Stress test entry 27", + "@1772042443.1159382\nUser:\nStress test entry 28 Stress test entry 28 Stress test entry 28 Stress test entry 28 Stress test entry 28", + "@1772042443.1159382\nUser:\nStress test entry 29 Stress test entry 29 Stress test entry 29 Stress test entry 29 Stress test entry 29", + "@1772042443.1159382\nUser:\nStress test entry 30 Stress test entry 30 Stress test entry 30 Stress test entry 30 Stress test entry 30", + "@1772042443.1159382\nUser:\nStress test entry 31 Stress test entry 31 Stress test entry 31 Stress test entry 31 Stress test entry 31", + "@1772042443.1159382\nUser:\nStress test entry 32 Stress test entry 32 Stress test entry 32 Stress test entry 32 Stress test entry 32", + "@1772042443.1159382\nUser:\nStress test entry 33 Stress test entry 33 Stress test entry 33 Stress test entry 33 Stress test entry 33", + "@1772042443.1159382\nUser:\nStress test entry 34 Stress test entry 34 Stress test entry 34 Stress test entry 34 Stress test entry 34", + "@1772042443.1159382\nUser:\nStress test entry 35 Stress test entry 35 Stress test entry 35 Stress test entry 35 Stress test entry 35", + "@1772042443.1159382\nUser:\nStress test entry 36 Stress test entry 36 Stress test entry 36 Stress test entry 36 Stress test entry 36", + "@1772042443.1159382\nUser:\nStress test entry 37 Stress test entry 37 Stress test entry 37 Stress test entry 37 Stress test entry 37", + "@1772042443.1159382\nUser:\nStress test entry 38 Stress test entry 38 Stress test entry 38 Stress test entry 38 Stress test entry 38", + "@1772042443.1159382\nUser:\nStress test entry 39 Stress test entry 39 Stress test entry 39 Stress test entry 39 Stress test entry 39", + "@1772042443.1159382\nUser:\nStress test entry 40 Stress test entry 40 Stress test entry 40 Stress test entry 40 Stress test entry 40", + "@1772042443.1159382\nUser:\nStress test entry 41 Stress test entry 41 Stress test entry 41 Stress test entry 41 Stress test entry 41", + "@1772042443.1159382\nUser:\nStress test entry 42 Stress test entry 42 Stress test entry 42 Stress test entry 42 Stress test entry 42", + "@1772042443.1159382\nUser:\nStress test entry 43 Stress test entry 43 Stress test entry 43 Stress test entry 43 Stress test entry 43", + "@1772042443.1159382\nUser:\nStress test entry 44 Stress test entry 44 Stress test entry 44 Stress test entry 44 Stress test entry 44", + "@1772042443.1159382\nUser:\nStress test entry 45 Stress test entry 45 Stress test entry 45 Stress test entry 45 Stress test entry 45", + "@1772042443.1159382\nUser:\nStress test entry 46 Stress test entry 46 Stress test entry 46 Stress test entry 46 Stress test entry 46", + "@1772042443.1159382\nUser:\nStress test entry 47 Stress test entry 47 Stress test entry 47 Stress test entry 47 Stress test entry 47", + "@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", ] diff --git a/tests/test_headless_api.py b/tests/test_headless_api.py new file mode 100644 index 0000000..b5918e9 --- /dev/null +++ b/tests/test_headless_api.py @@ -0,0 +1,35 @@ +import unittest +from fastapi.testclient import TestClient +import gui_2 +from unittest.mock import patch, MagicMock + +class TestHeadlessAPI(unittest.TestCase): + @classmethod + def setUpClass(cls): + # We need an App instance to initialize the API, but we want to avoid GUI stuff + with patch('gui_2.session_logger.open_session'), \ + patch('gui_2.ai_client.set_provider'), \ + patch('gui_2.session_logger.close_session'): + cls.app_instance = gui_2.App() + # We will implement create_api method in App + if hasattr(cls.app_instance, 'create_api'): + cls.api = cls.app_instance.create_api() + else: + cls.api = MagicMock() + cls.client = TestClient(cls.api) + + def test_health_endpoint(self): + response = self.client.get("/health") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"status": "ok"}) + + def test_status_endpoint(self): + response = self.client.get("/status") + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertIn("provider", data) + self.assertIn("model", data) + self.assertIn("active_project", data) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_headless_startup.py b/tests/test_headless_startup.py new file mode 100644 index 0000000..37d1d44 --- /dev/null +++ b/tests/test_headless_startup.py @@ -0,0 +1,48 @@ +import sys +import unittest +from unittest.mock import patch, MagicMock +import gui_2 + +class TestHeadlessStartup(unittest.TestCase): + + @patch('gui_2.immapp.run') + @patch('gui_2.api_hooks.HookServer') + @patch('gui_2.save_config') + @patch('gui_2.ai_client.cleanup') + @patch('uvicorn.run') # Mock uvicorn.run to prevent hanging + def test_headless_flag_prevents_gui_run(self, mock_uvicorn_run, mock_cleanup, mock_save_config, mock_hook_server, mock_immapp_run): + # Setup mock argv with --headless + test_args = ["gui_2.py", "--headless"] + + with patch.object(sys, 'argv', test_args): + with patch('gui_2.session_logger.close_session'), \ + patch('gui_2.session_logger.open_session'): + app = gui_2.App() + + # Mock _fetch_models to avoid network calls + app._fetch_models = MagicMock() + + app.run() + + # Expectation: immapp.run should NOT be called in headless mode + mock_immapp_run.assert_not_called() + # Expectation: uvicorn.run SHOULD be called + mock_uvicorn_run.assert_called_once() + + @patch('gui_2.immapp.run') + def test_normal_startup_calls_gui_run(self, mock_immapp_run): + test_args = ["gui_2.py"] + with patch.object(sys, 'argv', test_args): + # In normal mode, it should still call immapp.run + with patch('gui_2.api_hooks.HookServer'), \ + patch('gui_2.save_config'), \ + patch('gui_2.ai_client.cleanup'), \ + patch('gui_2.session_logger.close_session'), \ + patch('gui_2.session_logger.open_session'): + app = gui_2.App() + app._fetch_models = MagicMock() + app.run() + mock_immapp_run.assert_called_once() + +if __name__ == "__main__": + unittest.main()