diff --git a/src/external_editor.py b/src/external_editor.py index 774e1891..cc9728d1 100644 --- a/src/external_editor.py +++ b/src/external_editor.py @@ -6,11 +6,64 @@ import subprocess import tempfile # TODO(Ed): Eliminate these? -from pathlib import Path -from typing import Optional, List, Dict, Any +from dataclasses import dataclass, field +from pathlib import Path +from typing import Optional, List, Dict, Any -from src.models import ExternalEditorConfig, TextEditorConfig -from src.result_types import ErrorInfo, ErrorKind, Result +from src.result_types import ErrorInfo, ErrorKind, Result +from src.type_aliases import Metadata + + +@dataclass +class TextEditorConfig: + name: str = "" + path: str = "" + diff_args: List[str] = field(default_factory=list) + + def to_dict(self) -> Metadata: + return { + "name": self.name, + "path": self.path, + "diff_args": self.diff_args, + } + + @classmethod + def from_dict(cls, data: Metadata) -> "TextEditorConfig": + return cls( + name = data["name"], + path = data["path"], + diff_args = data.get("diff_args", []), + ) + + +@dataclass +class ExternalEditorConfig: + editors: Dict[str, TextEditorConfig] = field(default_factory=dict) + default_editor: Optional[str] = None + + def get_default(self) -> TextEditorConfig: + if self.default_editor and self.default_editor in self.editors: + return self.editors[self.default_editor] + if self.editors: + return next(iter(self.editors.values())) + return EMPTY_TEXT_EDITOR_CONFIG + + def to_dict(self) -> Metadata: + return { + "editors": {k: v.to_dict() for k, v in self.editors.items()}, + "default_editor": self.default_editor, + } + + @classmethod + def from_dict(cls, data: Metadata) -> "ExternalEditorConfig": + editors = {} + for name, ed_data in data.get("editors", {}).items(): + if isinstance(ed_data, dict): editors[name] = TextEditorConfig.from_dict(ed_data) + elif isinstance(ed_data, str): editors[name] = TextEditorConfig(name=name, path=ed_data) + return cls(editors=editors, default_editor=data.get("default_editor")) + + +EMPTY_TEXT_EDITOR_CONFIG: TextEditorConfig = TextEditorConfig() class ExternalEditorLauncher: diff --git a/src/models.py b/src/models.py index ece7d78c..db044607 100644 --- a/src/models.py +++ b/src/models.py @@ -280,6 +280,11 @@ def __getattr__(name: str) -> Any: from src.tool_bias import BiasProfile as _BiasProfile globals()[name] = _BiasProfile return _BiasProfile + if name in ("TextEditorConfig", "ExternalEditorConfig", "EMPTY_TEXT_EDITOR_CONFIG"): + from src.external_editor import TextEditorConfig as _TEC, ExternalEditorConfig as _EEC, EMPTY_TEXT_EDITOR_CONFIG as _ETEC + val = {"TextEditorConfig": _TEC, "ExternalEditorConfig": _EEC, "EMPTY_TEXT_EDITOR_CONFIG": _ETEC}[name] + globals()[name] = val + return val raise AttributeError(f"module {__name__!r} has no attribute {name!r}") # MMA Core dataclasses (ThinkingSegment, Ticket, Track, WorkerContext, TrackMetadata) @@ -301,76 +306,19 @@ def __getattr__(name: str) -> Any: # tool_bias both want to import from each other via models). #region: UI/Editor - -@dataclass -class TextEditorConfig: - name: str = "" - path: str = "" - diff_args: List[str] = field(default_factory=list) - - def to_dict(self) -> Metadata: - """ - [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 { - "name": self.name, - "path": self.path, - "diff_args": self.diff_args, - } - - @classmethod - def from_dict(cls, data: Metadata) -> "TextEditorConfig": - """ - [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( - name = data["name"], - path = data["path"], - diff_args = data.get("diff_args", []), - ) - -@dataclass -class ExternalEditorConfig: - editors: Dict[str, TextEditorConfig] = field(default_factory=dict) - default_editor: Optional[str] = None - - def get_default(self) -> TextEditorConfig: - """ - [C: tests/test_external_editor.py:TestExternalEditorConfig.test_get_default_fallback_to_first, tests/test_external_editor.py:TestExternalEditorConfig.test_get_default_returns_configured, tests/test_external_editor.py:TestExternalEditorConfig.test_get_default_returns_none_when_empty] - """ - if self.default_editor and self.default_editor in self.editors: - return self.editors[self.default_editor] - if self.editors: - return next(iter(self.editors.values())) - return EMPTY_TEXT_EDITOR_CONFIG - - def to_dict(self) -> Metadata: - """ - [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 { - "editors": {k: v.to_dict() for k, v in self.editors.items()}, - "default_editor": self.default_editor, - } - - @classmethod - def from_dict(cls, data: Metadata) -> "ExternalEditorConfig": - """ - [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] - """ - editors = {} - for name, ed_data in data.get("editors", {}).items(): - if isinstance(ed_data, dict): editors[name] = TextEditorConfig.from_dict(ed_data) - elif isinstance(ed_data, str): editors[name] = TextEditorConfig(name=name, path=ed_data) - return cls(editors=editors, default_editor=data.get("default_editor")) - - -EMPTY_TEXT_EDITOR_CONFIG: TextEditorConfig = TextEditorConfig() +# TextEditorConfig + ExternalEditorConfig + EMPTY_TEXT_EDITOR_CONFIG moved +# to src/external_editor.py in module_taxonomy_refactor_20260627 Phase 3f. +# The re-exports are LAZY via the __getattr__ below to avoid the cycle +# (external_editor was previously importing them from models; the cycle +# would deadlock on eager re-export). #region: Persona # Persona dataclass moved to src/personas.py in module_taxonomy_refactor_20260627 Phase 3.4. # PersonaManager (the ops layer) is also there. Re-export at the top of this module # preserves backward-compat 'from src.models import Persona'. +# Persona dataclass moved to src/personas.py in module_taxonomy_refactor_20260627 Phase 3.4. +# PersonaManager (the ops layer) is also there. Re-export at the top of this module +# preserves backward-compat 'from src.models import Persona'. #region: Workspace #region: Workspace