From 8467cdd525eb7288a349421fcb8fe45cdc4f40e2 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sat, 16 May 2026 05:05:22 -0400 Subject: [PATCH] feat(models): Implement ContextPreset and ContextFileEntry --- src/gui_2.py | 8 +-- src/models.py | 25 ++++++-- tests/test_context_presets_models.py | 95 ++++++++++++++++------------ 3 files changed, 75 insertions(+), 53 deletions(-) diff --git a/src/gui_2.py b/src/gui_2.py index 8e601d43..50a581ce 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -568,7 +568,7 @@ class App: for f in self.context_files: path = f.path if hasattr(f, 'path') else str(f) view_mode = f.view_mode if hasattr(f, 'view_mode') else 'summary' - preset_files.append(models.FileViewPreset(path=path, view_mode=view_mode)) + preset_files.append(models.ContextFileEntry(path=path, view_mode=view_mode)) preset = models.ContextPreset(name=name, files=preset_files, screenshots=list(self.screenshots)) self.controller.project['context_presets'][name] = preset.to_dict() @@ -945,7 +945,6 @@ class App: threading.Thread(target=_stats_worker, daemon=True).start() return total_lines, total_ast - def _close_vscode_diff(self) -> None: if hasattr(self, '_vscode_diff_process') and self._vscode_diff_process: try: @@ -997,7 +996,6 @@ class App: self._vscode_diff_process = result except Exception as e: self._patch_error_message = str(e) - def _reorder_ticket(self, src_idx: int, dst_idx: int) -> None: """ @@ -1117,8 +1115,6 @@ def main() -> None: if __name__ == "__main__": main() -#region: Main Interface - def render_main_interface(app: App) -> None: render_error_tint(app) app.perf_monitor.start_frame() @@ -1215,8 +1211,6 @@ def render_custom_title_bar(app: App) -> None: # Controls are now embedded in _show_menus. pass -#endregion: Main Interface - #region: Diagnostics & Analytics def render_usage_analytics_panel(app: App) -> None: diff --git a/src/models.py b/src/models.py index 0d0b2b44..137bf063 100644 --- a/src/models.py +++ b/src/models.py @@ -823,22 +823,27 @@ class WorkspaceProfile: ) @dataclass -class FileViewPreset: +class ContextFileEntry: path: str view_mode: str = "summary" + custom_slices: list = field(default_factory=list) def to_dict(self) -> dict[str, Any]: """ [C: src/personas.py:PersonaManager.save_persona, src/presets.py:PresetManager.save_preset, src/project_manager.py:save_project, src/project_manager.py:save_track_state, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.save_profile, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_serialization_with_annotations, tests/test_event_serialization.py:test_user_request_event_serialization, tests/test_external_editor.py:TestExternalEditorConfig.test_to_dict, tests/test_external_editor.py:TestTextEditorConfig.test_to_dict, tests/test_file_item_model.py:test_file_item_to_dict, tests/test_gui_events_v2.py:test_user_request_event_payload, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_serialization, tests/test_persona_id.py:test_ticket_persona_id_serialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_serialization, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_thinking_gui.py:test_thinking_segment_model_compatibility, tests/test_ticket_queue.py:test_ticket_to_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_to_dict, tests/test_track_state_schema.py:test_track_state_to_dict_with_none, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ - return {"path": self.path, "view_mode": self.view_mode} + return {"path": self.path, "view_mode": self.view_mode, "custom_slices": self.custom_slices} @classmethod - def from_dict(cls, data: dict[str, Any]) -> "FileViewPreset": + def from_dict(cls, data: dict[str, Any]) -> "ContextFileEntry": """ [C: src/personas.py:PersonaManager.load_all, src/presets.py:PresetManager.load_all, src/project_manager.py:load_project, src/project_manager.py:load_track_state, src/tool_presets.py:ToolPresetManager.load_all_bias_profiles, src/tool_presets.py:ToolPresetManager.load_all_presets, src/workspace_manager.py:WorkspaceManager.load_all_profiles, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_from_dict_legacy, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_deserialization_with_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_dict_editors, tests/test_external_editor.py:TestExternalEditorConfig.test_from_dict_with_string_editors, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_with_diff_args, tests/test_external_editor.py:TestTextEditorConfig.test_from_dict_without_diff_args, tests/test_file_item_model.py:test_file_item_from_dict, tests/test_file_item_model.py:test_file_item_from_dict_defaults, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_default_on_deserialize, tests/test_per_ticket_model.py:test_model_override_deserialization, tests/test_persona_id.py:test_ticket_persona_id_deserialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_deserialization, tests/test_project_serialization.py:TestProjectSerialization.test_backward_compatibility_strings, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_ticket_queue.py:test_ticket_from_dict_default_priority, tests/test_ticket_queue.py:test_ticket_from_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_from_dict, tests/test_track_state_schema.py:test_track_state_from_dict_empty_and_missing, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ - return cls(path=data.get("path", ""), view_mode=data.get("view_mode", "summary")) + return cls( + path=data.get("path", ""), + view_mode=data.get("view_mode", "summary"), + custom_slices=data.get("custom_slices", []), + ) @dataclass class NamedViewPreset: @@ -868,14 +873,19 @@ class NamedViewPreset: @dataclass class ContextPreset: name: str - files: list[FileViewPreset] = field(default_factory=list) + files: list[ContextFileEntry] = field(default_factory=list) screenshots: list[str] = field(default_factory=list) + description: str = "" def to_dict(self) -> dict[str, Any]: """ [C: src/personas.py:PersonaManager.save_persona, src/presets.py:PresetManager.save_preset, src/project_manager.py:save_project, src/project_manager.py:save_track_state, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.save_profile, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_serialization_with_annotations, tests/test_event_serialization.py:test_user_request_event_serialization, tests/test_external_editor.py:TestExternalEditorConfig.test_to_dict, tests/test_external_editor.py:TestTextEditorConfig.test_to_dict, tests/test_file_item_model.py:test_file_item_to_dict, tests/test_gui_events_v2.py:test_user_request_event_payload, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_serialization, tests/test_persona_id.py:test_ticket_persona_id_serialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_serialization, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_thinking_gui.py:test_thinking_segment_model_compatibility, tests/test_ticket_queue.py:test_ticket_to_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_to_dict, tests/test_track_state_schema.py:test_track_state_to_dict_with_none, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ - return {"files": [f.to_dict() for f in self.files], "screenshots": self.screenshots} + return { + "files": [f.to_dict() for f in self.files], + "screenshots": self.screenshots, + "description": self.description, + } @classmethod def from_dict(cls, name: str, data: dict[str, Any]) -> "ContextPreset": @@ -885,8 +895,9 @@ class ContextPreset: files_data = data.get("files", []) return cls( name=name, - files=[FileViewPreset.from_dict(f) if isinstance(f, dict) else FileViewPreset(path=str(f)) for f in files_data], + files=[ContextFileEntry.from_dict(f) if isinstance(f, dict) else ContextFileEntry(path=str(f)) for f in files_data], screenshots=data.get("screenshots", []), + description=data.get("description", ""), ) #region: MCP Config diff --git a/tests/test_context_presets_models.py b/tests/test_context_presets_models.py index cb81833b..c197c4a8 100644 --- a/tests/test_context_presets_models.py +++ b/tests/test_context_presets_models.py @@ -1,45 +1,62 @@ import pytest -from src.models import ContextPreset, FileViewPreset +from src.models import ContextPreset, ContextFileEntry -def test_file_view_preset_serialization(): - p = FileViewPreset(path="test.py", view_mode="skeleton") - d = p.to_dict() - assert d == {"path": "test.py", "view_mode": "skeleton"} - - p2 = FileViewPreset.from_dict(d) - assert p2.path == "test.py" - assert p2.view_mode == "skeleton" +def test_context_file_entry_serialization(): + p = ContextFileEntry(path="test.py", view_mode="skeleton") + d = p.to_dict() + # Check for default custom_slices + assert d == {"path": "test.py", "view_mode": "skeleton", "custom_slices": []} + + p2 = ContextFileEntry.from_dict(d) + assert p2.path == "test.py" + assert p2.view_mode == "skeleton" + assert p2.custom_slices == [] + + # Test with custom_slices + p3 = ContextFileEntry(path="test.py", view_mode="summary", custom_slices=["1-10"]) + d3 = p3.to_dict() + assert d3["custom_slices"] == ["1-10"] + p4 = ContextFileEntry.from_dict(d3) + assert p4.custom_slices == ["1-10"] def test_context_preset_serialization(): - f1 = FileViewPreset(path="a.py", view_mode="full") - f2 = FileViewPreset(path="b.py", view_mode="summary") - - preset = ContextPreset( - name="test_preset", - files=[f1, f2], - screenshots=["shot1.png"] - ) - - d = preset.to_dict() - assert len(d["files"]) == 2 - assert d["files"][0]["path"] == "a.py" - assert d["screenshots"] == ["shot1.png"] - - p2 = ContextPreset.from_dict("test_preset", d) - assert p2.name == "test_preset" - assert len(p2.files) == 2 - assert p2.files[0].view_mode == "full" - assert p2.screenshots == ["shot1.png"] + f1 = ContextFileEntry(path="a.py", view_mode="full") + f2 = ContextFileEntry(path="b.py", view_mode="summary", custom_slices=["5-15"]) + + preset = ContextPreset( + name="test_preset", + files=[f1, f2], + screenshots=["shot1.png"], + description="Test description" + ) + + d = preset.to_dict() + assert len(d["files"]) == 2 + assert d["files"][0]["path"] == "a.py" + assert d["files"][1]["custom_slices"] == ["5-15"] + assert d["screenshots"] == ["shot1.png"] + assert d["description"] == "Test description" + + p2 = ContextPreset.from_dict("test_preset", d) + assert p2.name == "test_preset" + assert p2.description == "Test description" + assert len(p2.files) == 2 + assert p2.files[0].view_mode == "full" + assert p2.files[1].custom_slices == ["5-15"] + assert p2.screenshots == ["shot1.png"] def test_context_preset_from_dict_legacy(): - # Test handling of legacy string paths in preset files - d = { - "files": ["legacy.py", {"path": "new.py", "view_mode": "skeleton"}], - "screenshots": [] - } - preset = ContextPreset.from_dict("legacy_test", d) - assert len(preset.files) == 2 - assert preset.files[0].path == "legacy.py" - assert preset.files[0].view_mode == "summary" # Default - assert preset.files[1].path == "new.py" - assert preset.files[1].view_mode == "skeleton" + # Test handling of legacy string paths in preset files and missing description/custom_slices + d = { + "files": ["legacy.py", {"path": "new.py", "view_mode": "skeleton"}], + "screenshots": [] + } + preset = ContextPreset.from_dict("legacy_test", d) + assert len(preset.files) == 2 + assert preset.files[0].path == "legacy.py" + assert preset.files[0].view_mode == "summary" # Default + assert preset.files[0].custom_slices == [] + assert preset.files[1].path == "new.py" + assert preset.files[1].view_mode == "skeleton" + assert preset.files[1].custom_slices == [] + assert preset.description == ""