feat(context): Decouple context composition from files and media

This commit is contained in:
2026-05-11 11:00:15 -04:00
parent abaeb56020
commit 9b3a4d6ec6
3 changed files with 74 additions and 12 deletions
+4 -2
View File
@@ -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 = []
self.active_tickets = []]
+23 -10
View File
@@ -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)
@@ -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()