From e3d84bc4dad67d73a350f30b6e4e5e474a608596 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sat, 16 May 2026 11:04:38 -0400 Subject: [PATCH] feat(presets): Implement ContextPresetManager and integrate with AppController --- src/app_controller.py | 17 +++++ src/context_presets.py | 28 +++++++ src/project_manager.py | 33 --------- tests/test_context_presets_manager.py | 101 ++++++++++++++++++++++++++ 4 files changed, 146 insertions(+), 33 deletions(-) create mode 100644 src/context_presets.py create mode 100644 tests/test_context_presets_manager.py diff --git a/src/app_controller.py b/src/app_controller.py index 00fb19f0..b72e9873 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -35,6 +35,7 @@ from src import shell_runner from src import theme_2 as theme from src import thinking_parser from src import tool_presets +from src.context_presets import ContextPresetManager from src.file_cache import ASTParser def parse_symbols(text: str) -> list[str]: @@ -1122,6 +1123,7 @@ class AppController: 'text_viewer_title': 'text_viewer_title', 'text_viewer_type': 'text_viewer_type' }) + self.context_preset_manager = ContextPresetManager() self.perf_monitor = performance_monitor.get_monitor() self._perf_profiling_enabled = False self._gui_task_handlers: Dict[str, Callable] = { @@ -2917,6 +2919,21 @@ class AppController: self.view_presets = [vp for vp in self.view_presets if vp.name != name] self._flush_to_project() + def save_context_preset(self, preset: models.ContextPreset) -> None: + self.context_preset_manager.save_preset(self.project, preset) + self._save_active_project() + + def load_context_preset(self, name: str) -> models.ContextPreset: + presets = self.context_preset_manager.load_all(self.project) + if name not in presets: + raise KeyError(f"Context preset '{name}' not found.") + preset = presets[name] + # Apply it to the current state + self.ui_file_paths = [f.path for f in preset.files] + self.screenshots = list(preset.screenshots) + self._save_active_project() + return preset + def _cb_load_track(self, track_id: str) -> None: """ diff --git a/src/context_presets.py b/src/context_presets.py new file mode 100644 index 00000000..7bcd9740 --- /dev/null +++ b/src/context_presets.py @@ -0,0 +1,28 @@ +from typing import Dict, Any +from src.models import ContextPreset + +class ContextPresetManager: + """Manages context presets within the project dictionary (manual_slop.toml).""" + + def load_all(self, project_dict: Dict[str, Any]) -> Dict[str, ContextPreset]: + """Loads all context presets from the project dictionary.""" + presets: Dict[str, ContextPreset] = {} + presets_data = project_dict.get("context_presets", {}) + for name, data in presets_data.items(): + try: + presets[name] = ContextPreset.from_dict(name, data) + except Exception: + # Silent failure or logging could be added here + pass + return presets + + def save_preset(self, project_dict: Dict[str, Any], preset: ContextPreset) -> None: + """Saves a context preset into the project dictionary.""" + if "context_presets" not in project_dict: + project_dict["context_presets"] = {} + project_dict["context_presets"][preset.name] = preset.to_dict() + + def delete_preset(self, project_dict: Dict[str, Any], name: str) -> None: + """Deletes a context preset from the project dictionary.""" + if "context_presets" in project_dict and name in project_dict["context_presets"]: + del project_dict["context_presets"][name] diff --git a/src/project_manager.py b/src/project_manager.py index 19ea059f..b0f9bc40 100644 --- a/src/project_manager.py +++ b/src/project_manager.py @@ -273,39 +273,6 @@ def flat_config(proj: dict[str, Any], disc_name: Optional[str] = None, track_id: "history": history, }, } - # ── context presets ────────────────────────────────────────────────────────── - -def save_context_preset(project_dict: dict, preset_name: str, files: list[str], screenshots: list[str]) -> None: - """ - - Save a named context preset (files + screenshots) into the project dict. - [C: tests/test_context_presets.py:test_save_context_preset] - """ - if "context_presets" not in project_dict: - project_dict["context_presets"] = {} - project_dict["context_presets"][preset_name] = { - "files": files, - "screenshots": screenshots - } - -def load_context_preset(project_dict: dict, preset_name: str) -> dict: - """ - - Return the files and screenshots for a named preset. - [C: tests/test_context_presets.py:test_load_context_preset, tests/test_context_presets.py:test_load_nonexistent_preset] - """ - if "context_presets" not in project_dict or preset_name not in project_dict["context_presets"]: - raise KeyError(f"Preset '{preset_name}' not found in project context_presets.") - return project_dict["context_presets"][preset_name] - -def delete_context_preset(project_dict: dict, preset_name: str) -> None: - """ - - Remove a named preset if it exists. - [C: tests/test_context_presets.py:test_delete_context_preset, tests/test_context_presets.py:test_delete_nonexistent_preset_no_error] - """ - if "context_presets" in project_dict: - project_dict["context_presets"].pop(preset_name, None) # ── track state persistence ───────────────────────────────────────────────── def save_track_state(track_id: str, state: 'TrackState', base_dir: Union[str, Path] = ".") -> None: diff --git a/tests/test_context_presets_manager.py b/tests/test_context_presets_manager.py new file mode 100644 index 00000000..c440a90b --- /dev/null +++ b/tests/test_context_presets_manager.py @@ -0,0 +1,101 @@ +import pytest +from src.context_presets import ContextPresetManager +from src.models import ContextPreset, ContextFileEntry +from src.app_controller import AppController +from pathlib import Path +import tomli_w +import os +from unittest.mock import MagicMock + +@pytest.fixture +def project_dict(): + return { + "context_presets": { + "test_preset": { + "files": [{"path": "file1.py"}, {"path": "file2.py"}], + "screenshots": ["shot1.png"], + "description": "Test description" + } + } + } + +def test_manager_load_all(project_dict): + manager = ContextPresetManager() + presets = manager.load_all(project_dict) + assert "test_preset" in presets + assert len(presets["test_preset"].files) == 2 + assert presets["test_preset"].files[0].path == "file1.py" + assert presets["test_preset"].screenshots == ["shot1.png"] + assert presets["test_preset"].description == "Test description" + +def test_manager_save_preset(project_dict): + manager = ContextPresetManager() + new_preset = ContextPreset( + name="new_preset", + files=[ContextFileEntry(path="new.py")], + screenshots=["new.png"], + description="New desc" + ) + manager.save_preset(project_dict, new_preset) + assert "new_preset" in project_dict["context_presets"] + assert project_dict["context_presets"]["new_preset"]["description"] == "New desc" + # ContextFileEntry.to_dict() includes view_mode and custom_slices + assert project_dict["context_presets"]["new_preset"]["files"][0]["path"] == "new.py" + assert project_dict["context_presets"]["new_preset"]["files"][0]["view_mode"] == "summary" + +def test_manager_delete_preset(project_dict): + manager = ContextPresetManager() + manager.delete_preset(project_dict, "test_preset") + assert "test_preset" not in project_dict["context_presets"] + +def test_app_controller_save_load(tmp_path, monkeypatch): + # Setup a dummy project + project_file = tmp_path / "test_project.toml" + project_data = { + "project": {"name": "test"}, + "discussion": {"active": "main", "discussions": {"main": {"history": []}}}, + "files": {"paths": []}, + "screenshots": {"paths": []} + } + with open(project_file, "wb") as f: + tomli_w.dump(project_data, f) + + # Mock directories to avoid issues during AppController init + monkeypatch.setenv("SLOP_LOGS_DIR", str(tmp_path / "logs")) + monkeypatch.setenv("SLOP_SCRIPTS_DIR", str(tmp_path / "scripts")) + + # Mocking some parts of AppController that might fail without full environment + monkeypatch.setattr("src.paths.get_config_path", lambda: tmp_path / "config.toml") + + # Create dummy config + with open(tmp_path / "config.toml", "wb") as f: + tomli_w.dump({"ui": {"theme": "dark"}, "projects": []}, f) + + controller = AppController() + controller.active_project_path = str(project_file) + # We don't call init_state() as it does too much, we manually setup what we need + controller.project = project_data + controller._save_active_project = MagicMock() + + # Save preset + preset = ContextPreset( + name="saved_preset", + files=[ContextFileEntry(path="app.py")], + screenshots=["app.png"] + ) + controller.save_context_preset(preset) + + # Verify in project dict + assert "saved_preset" in controller.project["context_presets"] + controller._save_active_project.assert_called_once() + + # Change state + controller.ui_file_paths = [] + controller.screenshots = [] + + # Load preset + loaded = controller.load_context_preset("saved_preset") + assert loaded.name == "saved_preset" + assert controller.ui_file_paths == ["app.py"] + assert controller.screenshots == ["app.png"] + controller._save_active_project.assert_called()