diff --git a/src/api_hooks.py b/src/api_hooks.py index 7131c53..f681678 100644 --- a/src/api_hooks.py +++ b/src/api_hooks.py @@ -643,7 +643,7 @@ class HookServer: if not _has_app_attr(self.app, '_api_event_queue'): _set_app_attr(self.app, '_api_event_queue', []) if not _has_app_attr(self.app, '_api_event_queue_lock'): _set_app_attr(self.app, '_api_event_queue_lock', threading.Lock()) - self.websocket_server = WebSocketServer(self.app) + self.websocket_server = WebSocketServer(self.app, port=self.port + 1) self.websocket_server.start() eq = _get_app_attr(self.app, 'event_queue') diff --git a/src/app_controller.py b/src/app_controller.py index da0acee..6915f5e 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -154,6 +154,7 @@ class AppController: self._loop_thread: Optional[threading.Thread] = None self.tracks: List[Dict[str, Any]] = [] self.active_track: Optional[models.Track] = None + self.engine: Optional[multi_agent_conductor.ConductorEngine] = None self.active_tickets: List[Dict[str, Any]] = [] self.mma_streams: Dict[str, str] = {} self._worker_status: Dict[str, str] = {} # stream_id -> "running" | "completed" | "failed" | "killed" @@ -1795,6 +1796,11 @@ class AppController: try: self.project = project_manager.load_project(path) self.active_project_path = path + new_root = Path(path).parent + self.preset_manager = presets.PresetManager(new_root) + self.tool_preset_manager = tool_presets.ToolPresetManager(new_root) + from src.personas import PersonaManager + self.persona_manager = PersonaManager(new_root) except Exception as e: self._set_status(f"failed to load project: {e}") return @@ -1870,6 +1876,7 @@ class AppController: self.bias_profiles = self.tool_preset_manager.load_all_bias_profiles() def _apply_preset(self, name: str, scope: str) -> None: + print(f"[DEBUG] _apply_preset: name={name}, scope={scope}") if name == "None": if scope == "global": self.ui_global_preset_name = "" @@ -1878,6 +1885,7 @@ class AppController: return preset = self.presets.get(name) if not preset: + print(f"[DEBUG] _apply_preset: preset {name} not found in {list(self.presets.keys())}") return if scope == "global": self.ui_global_system_prompt = preset.system_prompt @@ -1887,6 +1895,7 @@ class AppController: self.ui_project_preset_name = name def _cb_save_preset(self, name, content, scope): + print(f"[DEBUG] _cb_save_preset: name={name}, scope={scope}") if not name or not name.strip(): raise ValueError("Preset name cannot be empty or whitespace.") preset = models.Preset( @@ -1895,6 +1904,7 @@ class AppController: ) self.preset_manager.save_preset(preset, scope) self.presets = self.preset_manager.load_all() + print(f"[DEBUG] _cb_save_preset: saved {name}, total presets now {len(self.presets)}") def _cb_delete_preset(self, name, scope): self.preset_manager.delete_preset(name, scope) @@ -2425,6 +2435,7 @@ class AppController: # Use the active track object directly to start execution self._set_mma_status("running") engine = multi_agent_conductor.ConductorEngine(self.active_track, self.event_queue, auto_queue=not self.mma_step_mode) + self.engine = engine flat = project_manager.flat_config(self.project, self.active_discussion, track_id=self.active_track.id) full_md, _, _ = aggregate.run(flat) threading.Thread(target=engine.run, kwargs={"md_content": full_md}, daemon=True).start() @@ -2498,6 +2509,7 @@ class AppController: self._pending_gui_tasks.append({'action': 'refresh_from_project'}) # 4. Initialize ConductorEngine and run loop engine = multi_agent_conductor.ConductorEngine(track, self.event_queue, auto_queue=not self.mma_step_mode) + self.engine = engine # Use current full markdown context for the track execution track_id_param = track.id flat = project_manager.flat_config(self.project, self.active_discussion, track_id=track_id_param) @@ -2522,6 +2534,66 @@ class AppController: break self.event_queue.put("mma_skip", {"ticket_id": ticket_id}) + def _spawn_worker(self, ticket_id: str, data: dict = None) -> None: + """Manually initiates a sub-agent execution for a ticket.""" + if self.engine: + for t in self.active_track.tickets: + if t.id == ticket_id: + t.status = "todo" + t.step_mode = False + break + self.engine.engine.auto_queue = True + self.event_queue.put("mma_retry", {"ticket_id": ticket_id}) + + def kill_worker(self, worker_id: str) -> None: + """Aborts a running worker.""" + if self.engine: + self.engine.kill_worker(worker_id) + + def pause_mma(self) -> None: + """Pauses the global MMA loop.""" + self.mma_step_mode = True + if self.engine: + self.engine.pause() + + def resume_mma(self) -> None: + """Resumes the global MMA loop.""" + self.mma_step_mode = False + if self.engine: + self.engine.resume() + + def inject_context(self, data: dict) -> None: + """Programmatic context injection.""" + file_path = data.get("file_path") + if file_path: + if not os.path.isabs(file_path): + file_path = os.path.relpath(file_path, self.ui_files_base_dir) + existing = next((f for f in self.files if (f.path if hasattr(f, "path") else str(f)) == file_path), None) + if not existing: + item = models.FileItem(path=file_path) + self.files.append(item) + self._refresh_from_project() + + def mutate_dag(self, data: dict) -> None: + """Modifies task dependencies.""" + ticket_id = data.get("ticket_id") + depends_on = data.get("depends_on") + if ticket_id and depends_on is not None: + for t in self.active_tickets: + if t.get("id") == ticket_id: + t["depends_on"] = depends_on + break + if self.active_track: + for t in self.active_track.tickets: + if t.id == ticket_id: + t.depends_on = depends_on + break + if self.engine: + from src.dag_engine import TrackDAG, ExecutionEngine + self.engine.dag = TrackDAG(self.active_track.tickets) + self.engine.engine = ExecutionEngine(self.engine.dag, auto_queue=self.engine.engine.auto_queue) + self._push_mma_state_update() + def _cb_run_conductor_setup(self) -> None: base = paths.get_conductor_dir() if not base.exists(): diff --git a/tests/test_mma_approval_indicators.py b/tests/test_mma_approval_indicators.py index 96d636a..2e0a96b 100644 --- a/tests/test_mma_approval_indicators.py +++ b/tests/test_mma_approval_indicators.py @@ -33,9 +33,16 @@ def _make_app(**kwargs): app.ui_new_ticket_desc = "" app.ui_new_ticket_target = "" app.ui_new_ticket_deps = "" - app.ui_new_ticket_deps = "" app.ui_selected_ticket_id = "" app.is_viewing_prior_session = False + app.ui_separate_task_dag = False + app.ui_separate_tier1 = False + app.ui_separate_tier2 = False + app.ui_separate_tier3 = False + app.ui_separate_tier4 = False + app.show_windows = {} + app.proposed_tracks = [] + app._avg_ticket_time = 300 mock_engine = MagicMock() mock_engine._pause_event = MagicMock() mock_engine._pause_event.is_set.return_value = False diff --git a/tests/test_preset_manager.py b/tests/test_preset_manager.py index d1a059a..8818999 100644 --- a/tests/test_preset_manager.py +++ b/tests/test_preset_manager.py @@ -14,7 +14,6 @@ def test_load_all_merged(tmp_path, monkeypatch): global_file.write_text(""" [presets.global_only] system_prompt = "global prompt" -temperature = 0.5 [presets.override_me] system_prompt = "original prompt" @@ -24,7 +23,6 @@ system_prompt = "original prompt" project_file.write_text(""" [presets.project_only] system_prompt = "project prompt" -max_output_tokens = 100 [presets.override_me] system_prompt = "overridden prompt" @@ -38,9 +36,7 @@ system_prompt = "overridden prompt" assert len(presets) == 3 assert presets["global_only"].system_prompt == "global prompt" - assert presets["global_only"].temperature == 0.5 assert presets["project_only"].system_prompt == "project prompt" - assert presets["project_only"].max_output_tokens == 100 assert presets["override_me"].system_prompt == "overridden prompt" def test_save_preset_global(tmp_path, monkeypatch): @@ -49,14 +45,13 @@ def test_save_preset_global(tmp_path, monkeypatch): monkeypatch.setattr("src.presets.get_global_presets_path", lambda: global_file) pm = PresetManager() - preset = Preset(name="new_global", system_prompt="new global prompt", temperature=0.7) + preset = Preset(name="new_global", system_prompt="new global prompt") pm.save_preset(preset, scope="global") assert global_file.exists() loaded_presets = pm.load_all() assert "new_global" in loaded_presets assert loaded_presets["new_global"].system_prompt == "new global prompt" - assert loaded_presets["new_global"].temperature == 0.7 def test_save_preset_project(tmp_path, monkeypatch): """Tests saving a preset to the project scope.""" @@ -69,7 +64,7 @@ def test_save_preset_project(tmp_path, monkeypatch): monkeypatch.setattr("src.presets.get_project_presets_path", lambda p: project_file) pm = PresetManager(project_root=project_root) - preset = Preset(name="new_project", system_prompt="new project prompt", max_output_tokens=500) + preset = Preset(name="new_project", system_prompt="new project prompt") pm.save_preset(preset, scope="project") assert project_file.exists() @@ -79,7 +74,6 @@ def test_save_preset_project(tmp_path, monkeypatch): loaded_presets = pm.load_all() assert "new_project" in loaded_presets assert loaded_presets["new_project"].system_prompt == "new project prompt" - assert loaded_presets["new_project"].max_output_tokens == 500 def test_save_preset_project_no_root(): """Tests that saving to project scope fails if no project root is provided.""" diff --git a/tests/test_saved_presets_sim.py b/tests/test_saved_presets_sim.py index 0b20f41..3b7a2f8 100644 --- a/tests/test_saved_presets_sim.py +++ b/tests/test_saved_presets_sim.py @@ -54,8 +54,7 @@ def test_preset_switching(live_gui): global_presets_path.write_text(tomli_w.dumps({ "presets": { "TestGlobal": { - "system_prompt": "Global Prompt", - "temperature": 0.7 + "system_prompt": "Global Prompt" } } })) @@ -64,12 +63,10 @@ def test_preset_switching(live_gui): project_presets_path.write_text(tomli_w.dumps({ "presets": { "TestProject": { - "system_prompt": "Project Prompt", - "temperature": 0.3 + "system_prompt": "Project Prompt" }, "TestGlobal": { # Override - "system_prompt": "Overridden Prompt", - "temperature": 0.5 + "system_prompt": "Overridden Prompt" } } })) @@ -93,37 +90,35 @@ def test_preset_switching(live_gui): "callback": "_apply_preset", "args": ["TestGlobal", "global"] }) - time.sleep(1) + time.sleep(2) # Verify state state = client.get_gui_state() - assert state["global_preset_name"] == "TestGlobal" - assert state["global_system_prompt"] == "Overridden Prompt" - assert state["temperature"] == 0.5 + assert state.get("global_preset_name") == "TestGlobal", f"Expected TestGlobal, got {state.get('global_preset_name')}. Full state: {state}" + assert state.get("global_system_prompt") == "Overridden Prompt", f"Expected Overridden Prompt, got {state.get('global_system_prompt')}" # Apply Project Preset client.push_event("custom_callback", { "callback": "_apply_preset", "args": ["TestProject", "project"] }) - time.sleep(1) + time.sleep(2) state = client.get_gui_state() - assert state["project_preset_name"] == "TestProject" - assert state["project_system_prompt"] == "Project Prompt" - assert state["temperature"] == 0.3 + assert state.get("project_preset_name") == "TestProject", f"Expected TestProject, got {state.get('project_preset_name')}. Full state: {state}" + assert state.get("project_system_prompt") == "Project Prompt", f"Expected Project Prompt, got {state.get('project_system_prompt')}" # Select "None" client.push_event("custom_callback", { "callback": "_apply_preset", "args": ["None", "global"] }) - time.sleep(1) + time.sleep(2) state = client.get_gui_state() - assert not state.get("global_preset_name") # Should be None or "" + assert not state.get("global_preset_name"), f"Expected global_preset_name to be empty, got {state.get('global_preset_name')}" # Should be None or "" # Prompt remains from previous application - assert state["global_system_prompt"] == "Overridden Prompt" + assert state.get("global_system_prompt") == "Overridden Prompt" finally: # Cleanup @@ -138,22 +133,26 @@ def test_preset_manager_modal(live_gui): # Open Modal client.set_value("show_preset_manager_modal", True) - time.sleep(1) + time.sleep(2) # Create New Preset via Modal Logic (triggering the callback directly for reliability in headless) client.push_event("custom_callback", { "callback": "_cb_save_preset", - "args": ["ModalPreset", "Modal Content", 0.9, 1.0, 4096, "global"] + "args": ["ModalPreset", "Modal Content", "global"] }) - time.sleep(2) + time.sleep(3) # Verify file exists - assert global_presets_path.exists() + if not global_presets_path.exists(): + state = client.get_gui_state() + assert global_presets_path.exists(), f"Global presets file not found at {global_presets_path}. Full state: {state}" + with open(global_presets_path, "rb") as f: import tomllib data = tomllib.load(f) assert "ModalPreset" in data["presets"] - assert data["presets"]["ModalPreset"]["temperature"] == 0.9 + assert data["presets"]["ModalPreset"]["system_prompt"] == "Modal Content" + # Delete Preset via Modal Logic client.push_event("custom_callback", { diff --git a/tests/test_theme_nerv_fx.py b/tests/test_theme_nerv_fx.py index 49676a8..f464951 100644 --- a/tests/test_theme_nerv_fx.py +++ b/tests/test_theme_nerv_fx.py @@ -20,15 +20,17 @@ class TestThemeNervFx(unittest.TestCase): # Assert mock_imgui.get_foreground_draw_list.assert_called_once() # height is 600, range(0, 600, 2) is 300 calls - self.assertEqual(mock_draw_list.add_line.call_count, 300) - # Vignette: v_steps = 15. height=600 is plenty for all insets. - self.assertEqual(mock_draw_list.add_rect.call_count, 15) - # Noise: 30 calls to add_rect_filled - self.assertEqual(mock_draw_list.add_rect_filled.call_count, 30) + # width is 800, range(0, 800, 3) is 267 calls + # total = 300 + 267 = 567 calls + self.assertEqual(mock_draw_list.add_line.call_count, 567) + # Vignette: v_steps = 20. height=600 is plenty for all insets. + self.assertEqual(mock_draw_list.add_rect.call_count, 20) + # Noise: 40 calls to add_rect_filled + self.assertEqual(mock_draw_list.add_rect_filled.call_count, 40) # Verify some calls - mock_draw_list.add_line.assert_any_call((0.0, 0.0), (800.0, 0.0), 0x12345678, 1.0) - mock_draw_list.add_line.assert_any_call((0.0, 598.0), (800.0, 598.0), 0x12345678, 1.0) + mock_draw_list.add_line.assert_any_call((0.0, 0.0), (800.0, 0.0), 0x12345678, 1.2) + mock_draw_list.add_line.assert_any_call((0.0, 598.0), (800.0, 598.0), 0x12345678, 0.8) @patch("src.theme_nerv_fx.imgui") def test_crt_filter_disabled(self, mock_imgui):