feat(presets): Implement NamedViewPresets for per-file view settings
This commit is contained in:
@@ -226,6 +226,7 @@ class AppController:
|
||||
self._pending_actions: Dict[str, ConfirmDialog] = {}
|
||||
self._pending_ask_dialog: bool = False
|
||||
self.mcp_config: models.MCPConfiguration = models.MCPConfiguration()
|
||||
self.view_presets: list[models.NamedViewPreset] = []
|
||||
self.rag_config: Optional[models.RAGConfig] = None
|
||||
self.rag_engine: Optional[rag_engine.RAGEngine] = None
|
||||
self.rag_status: str = 'idle'
|
||||
@@ -2449,6 +2450,12 @@ class AppController:
|
||||
self.tool_presets = self.tool_preset_manager.load_all_presets()
|
||||
self.bias_profiles = self.tool_preset_manager.load_all_bias_profiles()
|
||||
|
||||
raw_presets = proj.get("view_presets", [])
|
||||
if isinstance(raw_presets, dict):
|
||||
self.view_presets = [models.NamedViewPreset.from_dict({"name": name, **data}) for name, data in raw_presets.items()]
|
||||
else:
|
||||
self.view_presets = [models.NamedViewPreset.from_dict(p) for p in raw_presets if isinstance(p, dict)]
|
||||
|
||||
if self.rag_config and self.rag_config.enabled:
|
||||
self._rebuild_rag_index()
|
||||
|
||||
@@ -2561,6 +2568,41 @@ class AppController:
|
||||
self.persona_manager.delete_persona(name, scope)
|
||||
self.personas = self.persona_manager.load_all()
|
||||
|
||||
def _cb_save_view_preset(self, name: str, f_item: models.FileItem) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_files_panel]
|
||||
"""
|
||||
preset = models.NamedViewPreset(
|
||||
name=name,
|
||||
view_mode=f_item.view_mode,
|
||||
ast_mask=copy.deepcopy(f_item.ast_mask) if hasattr(f_item, "ast_mask") else {},
|
||||
custom_slices=copy.deepcopy(f_item.custom_slices) if hasattr(f_item, "custom_slices") else []
|
||||
)
|
||||
for i, vp in enumerate(self.view_presets):
|
||||
if vp.name == name:
|
||||
self.view_presets[i] = preset
|
||||
break
|
||||
else:
|
||||
self.view_presets.append(preset)
|
||||
self._flush_to_project()
|
||||
|
||||
def _cb_apply_view_preset(self, name: str, f_item: models.FileItem) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_files_panel]
|
||||
"""
|
||||
preset = next((vp for vp in self.view_presets if vp.name == name), None)
|
||||
if preset:
|
||||
f_item.view_mode = preset.view_mode
|
||||
f_item.ast_mask = copy.deepcopy(preset.ast_mask)
|
||||
f_item.custom_slices = copy.deepcopy(preset.custom_slices)
|
||||
|
||||
def _cb_delete_view_preset(self, name: str) -> None:
|
||||
"""
|
||||
[C: src/gui_2.py:App._render_files_panel]
|
||||
"""
|
||||
self.view_presets = [vp for vp in self.view_presets if vp.name != name]
|
||||
self._flush_to_project()
|
||||
|
||||
|
||||
def _cb_load_track(self, track_id: str) -> None:
|
||||
"""
|
||||
@@ -3011,6 +3053,7 @@ class AppController:
|
||||
disc_sec["roles"] = self.disc_roles
|
||||
disc_sec["active"] = self.active_discussion
|
||||
disc_sec["auto_add"] = self.ui_auto_add_history
|
||||
proj["view_presets"] = [vp.to_dict() for vp in self.view_presets]
|
||||
# Save MMA State
|
||||
mma_sec = proj.setdefault("mma", {})
|
||||
mma_sec["epic"] = self.ui_epic_input
|
||||
|
||||
@@ -238,6 +238,7 @@ class App:
|
||||
self.shader_uniforms = {'crt': 1.0, 'scanline': 0.5, 'bloom': 0.8}
|
||||
self.shader_uniforms = {'crt': 1.0, 'scanline': 0.5, 'bloom': 0.8}
|
||||
self.ui_new_context_preset_name = ""
|
||||
self.ui_new_vp_name = ""
|
||||
self._focus_md_cache: dict[str, str] = {}
|
||||
self.ui_inspecting_ast_file = None
|
||||
self._show_ast_inspector = False
|
||||
@@ -3048,6 +3049,34 @@ class App:
|
||||
changed_vm, new_idx = imgui.combo(f"##vm{i}", current_idx, view_modes)
|
||||
if changed_vm:
|
||||
f_item.view_mode = view_modes[new_idx]
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button(f"[Save]##vpsave{i}"):
|
||||
imgui.open_popup(f"save_vp_popup{i}")
|
||||
|
||||
if imgui.begin_popup(f"save_vp_popup{i}"):
|
||||
imgui.text("Preset Name:")
|
||||
changed_pname, self.ui_new_vp_name = imgui.input_text(f"##pname{i}", self.ui_new_vp_name)
|
||||
if imgui.button("OK"):
|
||||
if self.ui_new_vp_name.strip():
|
||||
self.controller._cb_save_view_preset(self.ui_new_vp_name.strip(), f_item)
|
||||
self.ui_new_vp_name = ""
|
||||
imgui.close_current_popup()
|
||||
imgui.end_popup()
|
||||
|
||||
imgui.same_line()
|
||||
if imgui.button(f"[Load]##vpload{i}"):
|
||||
imgui.open_popup(f"load_vp_popup{i}")
|
||||
|
||||
if imgui.begin_popup(f"load_vp_popup{i}"):
|
||||
vp_names = sorted([vp.name for vp in self.controller.view_presets])
|
||||
if not vp_names:
|
||||
imgui.text("No presets saved.")
|
||||
for vp_name in vp_names:
|
||||
if imgui.selectable(vp_name):
|
||||
self.controller._cb_apply_view_preset(vp_name, f_item)
|
||||
imgui.close_current_popup()
|
||||
imgui.end_popup()
|
||||
if hasattr(f_item, "custom_slices") and f_item.custom_slices:
|
||||
imgui.same_line()
|
||||
imgui.text_colored(imgui.ImVec4(1.0, 0.5, 0.0, 1.0), "[Slices Active]")
|
||||
|
||||
@@ -939,6 +939,30 @@ class FileViewPreset:
|
||||
view_mode=data.get("view_mode", "summary")
|
||||
)
|
||||
|
||||
@dataclass
|
||||
class NamedViewPreset:
|
||||
name: str
|
||||
view_mode: str
|
||||
ast_mask: dict = field(default_factory=dict)
|
||||
custom_slices: list = field(default_factory=list)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"name": self.name,
|
||||
"view_mode": self.view_mode,
|
||||
"ast_mask": self.ast_mask,
|
||||
"custom_slices": self.custom_slices
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "NamedViewPreset":
|
||||
return cls(
|
||||
name=data.get("name", ""),
|
||||
view_mode=data.get("view_mode", "summary"),
|
||||
ast_mask=data.get("ast_mask", {}),
|
||||
custom_slices=data.get("custom_slices", [])
|
||||
)
|
||||
|
||||
@dataclass
|
||||
class ContextPreset:
|
||||
name: str
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import os
|
||||
import pytest
|
||||
import copy
|
||||
from src import models
|
||||
from src.app_controller import AppController
|
||||
|
||||
@pytest.fixture
|
||||
def controller(tmp_path):
|
||||
# Create a mock project file
|
||||
proj_path = tmp_path / "test_project.toml"
|
||||
proj_path.write_text("[project]\nname = 'test'\n")
|
||||
|
||||
ctrl = AppController()
|
||||
ctrl.active_project_path = str(proj_path)
|
||||
ctrl.project = {"project": {"name": "test"}}
|
||||
ctrl.view_presets = []
|
||||
|
||||
# Initialize missing attributes needed for flush/refresh
|
||||
ctrl.ui_output_dir = "./md_gen"
|
||||
ctrl.ui_files_base_dir = "."
|
||||
ctrl.ui_shots_base_dir = "."
|
||||
ctrl.ui_project_git_dir = ""
|
||||
ctrl.ui_project_conductor_dir = "conductor"
|
||||
ctrl.ui_project_system_prompt = ""
|
||||
ctrl.ui_project_preset_name = None
|
||||
ctrl.ui_gemini_cli_path = "gemini"
|
||||
ctrl.ui_word_wrap = True
|
||||
ctrl.ui_auto_add_history = False
|
||||
ctrl.ui_auto_scroll_comms = True
|
||||
ctrl.ui_auto_scroll_tool_calls = True
|
||||
ctrl.ui_agent_tools = {}
|
||||
ctrl.ui_epic_input = ""
|
||||
ctrl.ui_active_context_preset = ""
|
||||
ctrl.preset_manager = type('Mock', (), {'load_all': lambda self: {}, 'project_root': None})()
|
||||
ctrl.tool_preset_manager = type('Mock', (), {'load_all_presets': lambda self: {}, 'load_all_bias_profiles': lambda self: {}, 'project_root': None})()
|
||||
|
||||
return ctrl
|
||||
|
||||
def test_save_view_preset(controller):
|
||||
f_item = models.FileItem(path="test.py", view_mode="skeleton")
|
||||
f_item.ast_mask = {"test::func": "sig"}
|
||||
f_item.custom_slices = [{"start_line": 1, "end_line": 10}]
|
||||
|
||||
controller._cb_save_view_preset("my_preset", f_item)
|
||||
|
||||
assert any(vp.name == "my_preset" for vp in controller.view_presets)
|
||||
preset = next(vp for vp in controller.view_presets if vp.name == "my_preset")
|
||||
assert preset.view_mode == "skeleton"
|
||||
assert preset.ast_mask == {"test::func": "sig"}
|
||||
assert preset.custom_slices == [{"start_line": 1, "end_line": 10}]
|
||||
|
||||
# Verify persistence
|
||||
controller._flush_to_project()
|
||||
assert "view_presets" in controller.project
|
||||
assert isinstance(controller.project["view_presets"], list)
|
||||
assert any(vp["name"] == "my_preset" for vp in controller.project["view_presets"])
|
||||
|
||||
def test_apply_view_preset(controller):
|
||||
# Setup a preset
|
||||
preset = models.NamedViewPreset(
|
||||
name="my_preset",
|
||||
view_mode="masked",
|
||||
ast_mask={"main::run": "def"},
|
||||
custom_slices=[{"start_line": 5, "end_line": 15}]
|
||||
)
|
||||
controller.view_presets.append(preset)
|
||||
|
||||
# Create a file item to apply to
|
||||
f_item = models.FileItem(path="main.py", view_mode="summary")
|
||||
|
||||
controller._cb_apply_view_preset("my_preset", f_item)
|
||||
|
||||
assert f_item.view_mode == "masked"
|
||||
assert f_item.ast_mask == {"main::run": "def"}
|
||||
assert f_item.custom_slices == [{"start_line": 5, "end_line": 15}]
|
||||
|
||||
def test_delete_view_preset(controller):
|
||||
preset = models.NamedViewPreset(name="to_del", view_mode="full")
|
||||
controller.view_presets.append(preset)
|
||||
|
||||
controller._cb_delete_view_preset("to_del")
|
||||
|
||||
assert not any(vp.name == "to_del" for vp in controller.view_presets)
|
||||
|
||||
def test_load_presets_from_project_list(controller):
|
||||
controller.project["view_presets"] = [
|
||||
{
|
||||
"name": "stored_preset",
|
||||
"view_mode": "outline",
|
||||
"ast_mask": {"a": "b"},
|
||||
"custom_slices": []
|
||||
}
|
||||
]
|
||||
|
||||
controller._refresh_from_project()
|
||||
|
||||
assert any(vp.name == "stored_preset" for vp in controller.view_presets)
|
||||
preset = next(vp for vp in controller.view_presets if vp.name == "stored_preset")
|
||||
assert preset.view_mode == "outline"
|
||||
assert preset.ast_mask == {"a": "b"}
|
||||
|
||||
def test_load_presets_from_project_legacy_dict(controller):
|
||||
# Test backward compatibility
|
||||
controller.project["view_presets"] = {
|
||||
"legacy_preset": {
|
||||
"view_mode": "full",
|
||||
"ast_mask": {"c": "d"},
|
||||
"custom_slices": []
|
||||
}
|
||||
}
|
||||
|
||||
controller._refresh_from_project()
|
||||
|
||||
assert any(vp.name == "legacy_preset" for vp in controller.view_presets)
|
||||
preset = next(vp for vp in controller.view_presets if vp.name == "legacy_preset")
|
||||
assert preset.view_mode == "full"
|
||||
assert preset.ast_mask == {"c": "d"}
|
||||
Reference in New Issue
Block a user