diff --git a/src/gui_2.py b/src/gui_2.py index 09209ee..8e94086 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -507,6 +507,37 @@ class App: f.write(data) # ---------------------------------------------------------------- helpers + def _populate_auto_slices(self, f_item: models.FileItem) -> None: + from src import mcp_client + import re + mcp_client.configure([{"path": f_item.path}]) + outline = mcp_client.py_get_code_outline(f_item.path) + if outline.startswith("ERROR") or outline.startswith("ACCESS DENIED"): + return + pattern = re.compile(r'^\s*\[(.*?)\] (.*?) \(Lines (\d+)-(\d+)\)', re.MULTILINE) + try: + with open(f_item.path, "r", encoding="utf-8") as f: + text = f.read() + except Exception: + return + try: + from src.fuzzy_anchor import FuzzyAnchor + except ImportError: + FuzzyAnchor = None + for match in pattern.finditer(outline): + kind, name, s_str, e_str = match.groups() + s_line = int(s_str) + e_line = int(e_str) + if any(s.get('start_line') == s_line and s.get('end_line') == e_line for s in f_item.custom_slices): + continue + if FuzzyAnchor: + slice_data = FuzzyAnchor.create_slice(text, s_line, e_line) + else: + slice_data = {"start_line": s_line, "end_line": e_line} + slice_data['tag'] = 'auto-ast' + slice_data['comment'] = name + f_item.custom_slices.append(slice_data) + def _render_text_viewer(self, label: str, content: str, text_type: str = 'text', force_open: bool = False) -> None: if imgui.button("[+]##" + str(id(content))) or force_open: self.text_viewer_type = text_type @@ -1889,6 +1920,46 @@ class App: imgui.end_popup() + def _render_add_context_files_modal(self) -> None: + if imgui.begin_popup_modal("Select Context Files", None, imgui.WindowFlags_.always_auto_resize)[0]: + imgui.text("Select files from project to add to context:") + imgui.begin_child("ctx_picker_list", imgui.ImVec2(600, 300), True) + + from src import models + # Create a temporary selection set if not initialized + if not hasattr(self, '_ui_picker_selected'): + self._ui_picker_selected = set() + + for f in self.files: + fpath = f.path if hasattr(f, 'path') else str(f) + # Skip if already in context + if any((cf.path if hasattr(cf, 'path') else str(cf)) == fpath for cf in self.context_files): + continue + is_sel = fpath in self._ui_picker_selected + clicked, new_sel = imgui.checkbox(f"{fpath}##picker_{fpath}", is_sel) + if clicked: + if new_sel: + self._ui_picker_selected.add(fpath) + else: + self._ui_picker_selected.discard(fpath) + imgui.end_child() + imgui.separator() + + if imgui.button("Add Selected", imgui.ImVec2(120, 0)): + for fpath in self._ui_picker_selected: + f_item = models.FileItem(path=fpath) + self.context_files.append(f_item) + self._populate_auto_slices(f_item) + self._ui_picker_selected.clear() + imgui.close_current_popup() + + imgui.same_line() + if imgui.button("Cancel", imgui.ImVec2(120, 0)): + if hasattr(self, '_ui_picker_selected'): + self._ui_picker_selected.clear() + imgui.close_current_popup() + imgui.end_popup() + def _render_preset_manager_content(self, is_embedded: bool = False) -> None: avail = imgui.get_content_region_avail() if not hasattr(self, "_prompt_md_preview"): self._prompt_md_preview = False @@ -2953,7 +3024,9 @@ class App: for f in self.files: f_path = f.path if hasattr(f, "path") else str(f) if f_path not in context_paths: - self.context_files.append(copy.deepcopy(f)) + f_copy = copy.deepcopy(f) + self.context_files.append(f_copy) + self._populate_auto_slices(f_copy) imgui.same_line() if imgui.button("Del##batch"): new_files = [] diff --git a/tests/test_auto_slices.py b/tests/test_auto_slices.py new file mode 100644 index 0000000..7f49995 --- /dev/null +++ b/tests/test_auto_slices.py @@ -0,0 +1,92 @@ +import pytest +from unittest.mock import MagicMock, patch, mock_open +from src.gui_2 import App +from src import models + +@pytest.fixture +def mock_app(): + with ( + patch('src.models.load_config', return_value={ + "ai": {"provider": "gemini", "model": "model-1"}, + "projects": {"paths": [], "active": ""}, + "gui": {"show_windows": {}} + }), + patch('src.project_manager.load_project', return_value={}), + patch('src.project_manager.migrate_from_legacy_config', return_value={}), + patch('src.project_manager.save_project'), + patch('src.session_logger.open_session'), + patch('src.session_logger.reset_session'), + patch('src.app_controller.AppController._init_ai_and_hooks'), + patch('src.app_controller.AppController._fetch_models'), + patch('src.gui_2.App._load_fonts'), + patch('src.gui_2.App._post_init') + ): + app = App() + app.files = [] + app.context_files = [] + return app + +def test_populate_auto_slices_basic(mock_app: App) -> None: + f_item = models.FileItem(path="test.py") + mock_outline = "[Class] MyClass (Lines 1-10)\n[Method] my_method (Lines 2-5)\n[Func] top_func (Lines 12-15)" + + with ( + patch('src.mcp_client.configure') as mock_conf, + patch('src.mcp_client.py_get_code_outline', return_value=mock_outline) as mock_outline_tool, + patch('builtins.open', mock_open(read_data="dummy content")) + ): + mock_app._populate_auto_slices(f_item) + + assert len(f_item.custom_slices) == 3 + s0 = f_item.custom_slices[0] + assert s0["start_line"] == 1 + assert s0["end_line"] == 10 + assert s0["tag"] == 'auto-ast' + assert s0["comment"] == "MyClass" + +def test_add_selected_triggers_auto_slices(mock_app): + f_mock = MagicMock() + f_mock.path = "test.py" + mock_app.files = [f_mock] + mock_app._ui_picker_selected = {"test.py"} + + with patch('src.gui_2.imgui') as mock_imgui: + mock_imgui.begin_popup_modal.return_value = (True, True) + mock_imgui.button.side_effect = lambda label, size=None: label == "Add Selected" + mock_imgui.checkbox.return_value = (False, False) + mock_imgui.begin_child.return_value = True + mock_imgui.ImVec2 = MagicMock() + mock_imgui.WindowFlags_ = MagicMock() + + with patch.object(mock_app, '_populate_auto_slices') as mock_populate: + mock_app._render_add_context_files_modal() + + assert mock_populate.called + assert len(mock_app.context_files) == 1 + assert mock_app.context_files[0].path == "test.py" + +def test_add_all_triggers_auto_slices(mock_app): + f_mock = MagicMock() + f_mock.path = "test_all.py" + mock_app.files = [f_mock] + mock_app.context_files = [] + + with patch('src.gui_2.imgui') as mock_imgui: + mock_imgui.button.side_effect = lambda label, size=None: label == "Add All##addall" + mock_imgui.collapsing_header.return_value = True + mock_imgui.begin_child.return_value = True + mock_imgui.begin_table.return_value = True + mock_imgui.tree_node_ex.return_value = False + mock_imgui.checkbox.return_value = (False, False) + mock_imgui.combo.return_value = (False, 0) + mock_imgui.input_text.return_value = (False, "") + mock_imgui.TableFlags_ = MagicMock() + mock_imgui.TableColumnFlags_ = MagicMock() + mock_imgui.TreeNodeFlags_ = MagicMock() + + with patch.object(mock_app, '_populate_auto_slices') as mock_populate: + mock_app._render_context_composition_panel() + + assert mock_populate.called + assert len(mock_app.context_files) == 1 + assert mock_app.context_files[0].path == "test_all.py"