From 9b3a4d6ec6f99896404f319766d8c7ed5bed0655 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Mon, 11 May 2026 11:00:15 -0400 Subject: [PATCH] feat(context): Decouple context composition from files and media --- src/app_controller.py | 6 ++- src/gui_2.py | 33 ++++++++++----- tests/test_context_composition_decoupled.py | 47 +++++++++++++++++++++ 3 files changed, 74 insertions(+), 12 deletions(-) create mode 100644 tests/test_context_composition_decoupled.py diff --git a/src/app_controller.py b/src/app_controller.py index 65273ed..f0e9919 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -177,7 +177,8 @@ class AppController: self.disc_entries: List[Dict[str, Any]] = [] self.ui_active_persona: str = "" self.disc_roles: List[str] = [] - self.files: List[str] = [] + self.files: List[models.FileItem] = [] + self.context_files: List[models.FileItem] = [] self.screenshots: List[str] = [] self.event_queue: events.AsyncEventQueue = events.AsyncEventQueue() self._loop_thread: Optional[threading.Thread] = None @@ -3286,6 +3287,7 @@ class AppController: # Use current full markdown context for the track execution track_id_param = track.id flat = project_manager.flat_config(self.project, self.active_discussion, track_id=track_id_param) + flat.setdefault("files", {})["paths"] = self.context_files full_md, _, _ = aggregate.run(flat) # Start the engine in a separate thread threading.Thread(target=engine.run, kwargs={"md_content": full_md}, daemon=True).start() @@ -3506,4 +3508,4 @@ class AppController: if self.active_track: self.active_tickets = [asdict(t) if not isinstance(t, dict) else t for t in self.active_track.tickets] else: - self.active_tickets = [] \ No newline at end of file + self.active_tickets = []] \ No newline at end of file diff --git a/src/gui_2.py b/src/gui_2.py index 75b0a06..a04a497 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -244,6 +244,7 @@ class App: self._cached_ast_nodes = [] self._cached_ast_file_path = '' self.ui_editing_slices_file = None + self.context_files = [] """UI-level wrapper for approving a pending tool execution ask.""" self._handle_approve_ask() @@ -814,6 +815,7 @@ class App: self._render_base_prompt_diff_modal() self._render_save_preset_modal() self._render_save_workspace_profile_modal() + self._render_add_context_files_modal() self._render_preset_manager_window() self._render_tool_preset_manager_window() self._render_persona_editor_window() @@ -2803,7 +2805,7 @@ class App: imgui.text("Batch:") imgui.same_line() if imgui.button("Full##batch"): - for f in self.files: + for f in self.context_files: f_path = f.path if hasattr(f, "path") else str(f) if f_path in self.ui_selected_context_files: f.force_full = True @@ -2813,7 +2815,7 @@ class App: f.ast_definitions = False imgui.same_line() if imgui.button("Agg##batch"): - for f in self.files: + for f in self.context_files: f_path = f.path if hasattr(f, "path") else str(f) if f_path in self.ui_selected_context_files: f.auto_aggregate = True @@ -2823,7 +2825,7 @@ class App: f.ast_definitions = False imgui.same_line() if imgui.button("Sig##batch"): - for f in self.files: + for f in self.context_files: f_path = f.path if hasattr(f, "path") else str(f) if f_path in self.ui_selected_context_files: if hasattr(f, "ast_signatures"): @@ -2833,7 +2835,7 @@ class App: f.ast_definitions = False imgui.same_line() if imgui.button("Def##batch"): - for f in self.files: + for f in self.context_files: f_path = f.path if hasattr(f, "path") else str(f) if f_path in self.ui_selected_context_files: if hasattr(f, "ast_definitions"): @@ -2843,7 +2845,7 @@ class App: f.ast_signatures = False imgui.same_line() if imgui.button("None##batch"): - for f in self.files: + for f in self.context_files: f_path = f.path if hasattr(f, "path") else str(f) if f_path in self.ui_selected_context_files: f.auto_aggregate = False @@ -2853,20 +2855,31 @@ class App: f.ast_definitions = False imgui.same_line() if imgui.button("Sel All##selall"): - for f in self.files: + for f in self.context_files: f_path = f.path if hasattr(f, "path") else str(f) self.ui_selected_context_files.add(f_path) imgui.same_line() if imgui.button("Unsel All##unselall"): self.ui_selected_context_files.clear() imgui.same_line() + if imgui.button("Add Files"): + imgui.open_popup("Select Context Files") + imgui.same_line() + if imgui.button("Add All##addall"): + import copy + context_paths = {f.path if hasattr(f, "path") else str(f) for f in self.context_files} + 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)) + imgui.same_line() if imgui.button("Del##batch"): new_files = [] - for f in self.files: + for f in self.context_files: f_path = f.path if hasattr(f, "path") else str(f) if f_path not in self.ui_selected_context_files: new_files.append(f) - self.files = new_files + self.context_files = new_files self.ui_selected_context_files.clear() #endregion: Batch Action Bar @@ -2876,7 +2889,7 @@ class App: imgui.table_setup_column("File", imgui.TableColumnFlags_.width_stretch) imgui.table_setup_column("Flags", imgui.TableColumnFlags_.width_fixed, 200) imgui.table_headers_row() - for i, f_item in enumerate(self.files): + for i, f_item in enumerate(self.context_files): imgui.table_next_row() imgui.table_set_column_index(0) @@ -2889,7 +2902,7 @@ class App: start = min(self._last_selected_context_index, i) end = max(self._last_selected_context_index, i) for idx in range(start, end + 1): - item = self.files[idx] + item = self.context_files[idx] item_path = item.path if hasattr(item, "path") else str(item) if is_sel: self.ui_selected_context_files.add(item_path) diff --git a/tests/test_context_composition_decoupled.py b/tests/test_context_composition_decoupled.py new file mode 100644 index 0000000..d69af0a --- /dev/null +++ b/tests/test_context_composition_decoupled.py @@ -0,0 +1,47 @@ +import pytest +from src.app_controller import AppController +from src.models import FileItem + +def test_context_files_is_decoupled(): + controller = AppController() + + # Verify both lists exist and are distinct + assert hasattr(controller, 'files') + assert hasattr(controller, 'context_files') + assert controller.files is not controller.context_files + + # Modifying one should not affect the other + controller.files.append(FileItem(path="whitelist.txt")) + controller.context_files.append(FileItem(path="context.txt")) + + assert len(controller.files) == 1 + assert controller.files[0].path == "whitelist.txt" + + assert len(controller.context_files) == 1 + assert controller.context_files[0].path == "context.txt" + +def test_do_generate_uses_context_files(monkeypatch): + controller = AppController() + controller.init_state() + controller.context_files = [FileItem(path="context.txt")] + controller.files = [FileItem(path="whitelist.txt")] + + # Mock project_manager.flat_config and aggregate.run to verify passed data + import src.project_manager as pm + import src.aggregate as agg + + def mock_flat_config(*args, **kwargs): + return {"files": {}} + + def mock_aggregate_run(flat, **kwargs): + assert flat["files"]["paths"] == controller.context_files + return ("md", "path", [], "stable_md", "disc_text") + + monkeypatch.setattr(pm, "flat_config", mock_flat_config) + monkeypatch.setattr(pm, "save_project", lambda *args: None) + monkeypatch.setattr(agg, "run", mock_aggregate_run) + monkeypatch.setattr(agg, "build_markdown_no_history", lambda *args, **kwargs: "stable") + monkeypatch.setattr(agg, "build_discussion_text", lambda *args, **kwargs: "disc") + + # Should not raise assertion error + controller._do_generate()