feat(presets): Implement NamedViewPresets for per-file view settings

This commit is contained in:
2026-05-11 18:31:56 -04:00
parent 6e53906715
commit cb0fa89730
4 changed files with 213 additions and 0 deletions
+43
View File
@@ -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
+29
View File
@@ -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]")
+24
View File
@@ -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
+117
View File
@@ -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"}