feat(presets): Implement ContextPresetManager and integrate with AppController
This commit is contained in:
@@ -35,6 +35,7 @@ from src import shell_runner
|
|||||||
from src import theme_2 as theme
|
from src import theme_2 as theme
|
||||||
from src import thinking_parser
|
from src import thinking_parser
|
||||||
from src import tool_presets
|
from src import tool_presets
|
||||||
|
from src.context_presets import ContextPresetManager
|
||||||
from src.file_cache import ASTParser
|
from src.file_cache import ASTParser
|
||||||
|
|
||||||
def parse_symbols(text: str) -> list[str]:
|
def parse_symbols(text: str) -> list[str]:
|
||||||
@@ -1122,6 +1123,7 @@ class AppController:
|
|||||||
'text_viewer_title': 'text_viewer_title',
|
'text_viewer_title': 'text_viewer_title',
|
||||||
'text_viewer_type': 'text_viewer_type'
|
'text_viewer_type': 'text_viewer_type'
|
||||||
})
|
})
|
||||||
|
self.context_preset_manager = ContextPresetManager()
|
||||||
self.perf_monitor = performance_monitor.get_monitor()
|
self.perf_monitor = performance_monitor.get_monitor()
|
||||||
self._perf_profiling_enabled = False
|
self._perf_profiling_enabled = False
|
||||||
self._gui_task_handlers: Dict[str, Callable] = {
|
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.view_presets = [vp for vp in self.view_presets if vp.name != name]
|
||||||
self._flush_to_project()
|
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:
|
def _cb_load_track(self, track_id: str) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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]
|
||||||
@@ -273,39 +273,6 @@ def flat_config(proj: dict[str, Any], disc_name: Optional[str] = None, track_id:
|
|||||||
"history": history,
|
"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 ─────────────────────────────────────────────────
|
# ── track state persistence ─────────────────────────────────────────────────
|
||||||
|
|
||||||
def save_track_state(track_id: str, state: 'TrackState', base_dir: Union[str, Path] = ".") -> None:
|
def save_track_state(track_id: str, state: 'TrackState', base_dir: Union[str, Path] = ".") -> None:
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user