From fcc8822612792d012e7a74b4050aae42534c5912 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sat, 16 May 2026 12:36:20 -0400 Subject: [PATCH] docs(conductor): Synchronize docs for track 'Context Composition Presets' --- conductor/product.md | 2 +- conductor/tech-stack.md | 2 + src/gui_2.py | 128 +++++++++++++++++++++++------- tests/test_gui_context_presets.py | 20 ++++- 4 files changed, 122 insertions(+), 30 deletions(-) diff --git a/conductor/product.md b/conductor/product.md index aaf16e15..8f6f5015 100644 --- a/conductor/product.md +++ b/conductor/product.md @@ -17,7 +17,7 @@ For deep implementation details when planning or implementing tracks, consult `d ## Primary Use Cases - **Full Control over Vendor APIs:** Exposing detailed API metrics and configuring deep agent capabilities directly within the GUI. -- **Context & Memory Management:** Better visualization and management of token usage and context memory. Features an independent **Context Composition** panel decoupled from the project whitelist, with directory-grouped listings and per-file **View Modes** (Full, Summary, Skeleton, Outline, None). Includes a **Visual Slice Editor** for creating fuzzy-anchored line ranges with **Annotations** (tags and comments), and **View Presets** for saving named configurations of view settings. Features a dedicated **'Context' role** for manual injections and **Context Presets** for saving and loading complete compositions. Allows assigning specific context presets to MMA agent personas for granular cognitive load isolation. +- **Context & Memory Management:** Better visualization and management of token usage and context memory. Features an independent **Context Composition** panel decoupled from the project whitelist, with directory-grouped listings and per-file **View Modes** (Full, Summary, Skeleton, Outline, None). Includes a **Visual Slice Editor** for creating fuzzy-anchored line ranges with **Annotations** (tags and comments), and **View Presets** for saving named configurations of view settings. Features a dedicated **'Context' role** for manual injections and **Context Presets** for saving and loading complete compositions with **File Existence Validation**. Includes a high-fidelity **Context Preview** window providing a scrollable Markdown view of the final aggregated context and real-time token estimation before dispatching to agents. Allows assigning specific context presets to MMA agent personas for granular cognitive load isolation. - **Manual "Vibe Coding" Assistant:** Serving as an auxiliary, multi-provider assistant that natively interacts with the codebase via sandboxed PowerShell scripts and MCP-like file tools, emphasizing manual developer oversight and explicit confirmation. - **State-Preserving Hot Reload:** Supports selective, manual hot-reloading of Python modules (including the main GUI logic) via a delegation-based architecture. This allows for rapid UI iteration without losing application state or restarting the session. diff --git a/conductor/tech-stack.md b/conductor/tech-stack.md index 1009ea12..fbf7750b 100644 --- a/conductor/tech-stack.md +++ b/conductor/tech-stack.md @@ -35,6 +35,8 @@ - **src/personas.py:** Implements `PersonaManager` for high-performance CRUD operations on unified agent personas stored in TOML format (`personas.toml`, `project_personas.toml`). Handles consolidation of model settings, prompts, and tool biases. +- **src/context_presets.py:** Implements `ContextPresetManager` for managing complex context compositions (files, view modes, screenshots) within the project configuration. Supports validation of file existence and relative path mapping. + - **src/tool_bias.py:** Implements the `ToolBiasEngine` for semantic tool description nudging and dynamic tooling strategy generation. - **src/tool_presets.py:** Extends `ToolPresetManager` to handle nested `Tool` models, weights, and global `BiasProfile` persistence within `tool_presets.toml`. diff --git a/src/gui_2.py b/src/gui_2.py index 7519a7c6..3570efe9 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -135,11 +135,31 @@ class App: self.controller._predefined_callbacks['load_context_preset'] = self.controller.load_context_preset self.controller._predefined_callbacks['set_ui_file_paths'] = lambda p: setattr(self, 'ui_file_paths', p) self.controller._predefined_callbacks['set_ui_screenshot_paths'] = lambda p: setattr(self, 'ui_screenshot_paths', p) + self.controller._predefined_callbacks['set_context_files_for_test'] = lambda files: setattr(self, 'context_files', [models.FileItem(path=f) for f in files]) + self.controller._predefined_callbacks['set_screenshots_for_test'] = lambda ss: setattr(self, 'screenshots', ss) + + def _save_context_preset_force(name: str): + if not name: return + preset_files = [] + for f in self.context_files: + p = f.path if hasattr(f, 'path') else str(f) + vm = f.view_mode if hasattr(f, 'view_mode') else 'summary' + preset_files.append(models.ContextFileEntry(path=p, view_mode=vm)) + preset = models.ContextPreset(name=name, files=preset_files, screenshots=list(self.screenshots)) + self.controller.save_context_preset(preset) + self.ui_new_context_preset_name = "" + self.show_missing_files_modal = False + + self.controller._predefined_callbacks['save_context_preset_force'] = _save_context_preset_force + self.controller._predefined_callbacks['set_ui_attr'] = lambda k, v: setattr(self, k, v) + self.controller._predefined_callbacks['set_context_files'] = self._set_context_files self.controller._predefined_callbacks['simulate_save_preset'] = self._simulate_save_preset self.controller._clickable_actions.update({ 'btn_undo': self._handle_undo, 'btn_redo': self._handle_redo, 'btn_open_external_editor': self._open_patch_in_external_editor, + 'Save##ctx': self._handle_save_ctx_click, + 'Save Anyway': self._handle_save_anyway_click, }) # --- UI Component State --- self.text_viewer_wrap = True @@ -156,6 +176,11 @@ class App: # --- Preset & Profile Management State --- self.ui_active_context_preset = "" self.ui_new_context_preset_name = "" + self._pending_save_ctx_click = False + self._pending_save_anyway_click = False + self.show_missing_files_modal = False + self.missing_context_files = [] + self.target_context_preset_name = "" self._new_preset_name = "" self._editing_preset_name = "" self._editing_preset_system_prompt = "" @@ -258,6 +283,10 @@ class App: self.shader_uniforms = {'crt': 1.0, 'scanline': 0.5, 'bloom': 0.8} self._hot_reload_error: Optional[str] = None + def _set_context_files(self, paths: list[str]) -> None: + from src import models + self.context_files = [models.FileItem(path=p) for p in paths] + def _simulate_save_preset(self, name: str) -> None: from src import models item = models.FileItem(path='test.py') @@ -269,6 +298,12 @@ class App: """UI-level wrapper for approving a pending tool execution ask.""" self.controller._handle_approve_ask() + def _handle_save_ctx_click(self) -> None: + self._pending_save_ctx_click = True + + def _handle_save_anyway_click(self) -> None: + self._pending_save_anyway_click = True + def _post_init(self) -> None: theme.apply_current() @@ -553,27 +588,6 @@ class App: pass self.controller.shutdown() - def save_context_preset(self, name: str) -> None: - """ - [C: tests/test_context_presets.py:test_save_context_preset] - """ - sys.stderr.write(f"[DEBUG] save_context_preset called with: {name}\n") - sys.stderr.flush() - if 'context_presets' not in self.controller.project: - self.controller.project['context_presets'] = {} - - preset_files = [] - for f in self.context_files: - path = f.path if hasattr(f, 'path') else str(f) - view_mode = f.view_mode if hasattr(f, 'view_mode') else 'summary' - preset_files.append(models.ContextFileEntry(path=path, view_mode=view_mode)) - - preset = models.ContextPreset(name=name, files=preset_files, screenshots=list(self.screenshots)) - self.controller.project['context_presets'][name] = preset.to_dict() - self.controller._save_active_project() - sys.stderr.write(f"[DEBUG] save_context_preset finished. Project keys: {list(self.controller.project.keys())}\n") - sys.stderr.flush() - def load_context_preset(self, name: str) -> None: """ [C: tests/test_context_presets.py:test_load_context_preset, tests/test_context_presets.py:test_load_nonexistent_preset] @@ -1203,6 +1217,7 @@ def render_main_interface(app: App) -> None: # Modals / Popups render_approve_script_modal(app) render_mma_modals(app) + render_context_modals(app) def render_custom_title_bar(app: App) -> None: # Obsolete, removed since it renders behind the full screen dock space. @@ -2845,12 +2860,12 @@ def render_context_composition_panel(app: App) -> None: total_lines, total_ast = app._update_context_file_stats() render_context_batch_actions(app, total_lines, total_ast) render_context_files_table(app) - + + imgui.separator() + render_context_presets(app) imgui.separator() if imgui.collapsing_header("Screenshots"): render_context_screenshots(app) - imgui.separator() - render_context_presets(app) def render_ast_inspector_modal(app: App) -> None: """ @@ -3165,10 +3180,34 @@ def render_context_presets(app: App) -> None: changed, new_name = imgui.input_text("##new_preset", getattr(app, "ui_new_context_preset_name", "")) if changed: app.ui_new_context_preset_name = new_name imgui.same_line() - if imgui.button("Save##ctx"): - if getattr(app, "ui_new_context_preset_name", "").strip(): - app.save_context_preset(app.ui_new_context_preset_name.strip()) - app.ui_new_context_preset_name = "" + if imgui.button("Save##ctx") or getattr(app, "_pending_save_ctx_click", False): + app._pending_save_ctx_click = False + name = getattr(app, "ui_new_context_preset_name", "").strip() + if name: + missing = [] + root = app.controller.active_project_root + for f in app.context_files: + path = f.path if hasattr(f, "path") else str(f) + if not os.path.isabs(path): + full_path = os.path.join(root, path) + else: + full_path = path + if not os.path.exists(full_path): + missing.append(path) + + if missing: + app.missing_context_files = missing + app.show_missing_files_modal = True + app.target_context_preset_name = name + else: + preset_files = [] + for f in app.context_files: + p = f.path if hasattr(f, 'path') else str(f) + vm = f.view_mode if hasattr(f, 'view_mode') else 'summary' + preset_files.append(models.ContextFileEntry(path=p, view_mode=vm)) + preset = models.ContextPreset(name=name, files=preset_files, screenshots=list(app.screenshots)) + app.controller.save_context_preset(preset) + app.ui_new_context_preset_name = "" imgui.same_line() if imgui.button("Delete##ctx"): if getattr(app, "ui_active_context_preset", ""): @@ -5197,3 +5236,36 @@ def render_mma_focus_selector(app: App) -> None: if app.ui_focus_agent and imgui.button("x##clear_focus"): app.ui_focus_agent = None #endregion: MMA + +def render_context_modals(app: App) -> None: + if app.show_missing_files_modal: + imgui.open_popup("Missing Files Warning") + app.show_missing_files_modal = False + + if imgui.begin_popup_modal("Missing Files Warning", True, imgui.WindowFlags_.always_auto_resize)[0]: + imgui.text("The following files are missing from disk:") + imgui.separator() + imgui.begin_child("missing_files_list", imgui.ImVec2(0, 150), True) + for f in app.missing_context_files: + imgui.text_colored(imgui.ImVec4(1, 0.4, 0.4, 1), f) + imgui.end_child() + imgui.separator() + + if imgui.button("Save Anyway") or getattr(app, "_pending_save_anyway_click", False): + app._pending_save_anyway_click = False + name = app.target_context_preset_name + preset_files = [] + for f in app.context_files: + p = f.path if hasattr(f, 'path') else str(f) + vm = f.view_mode if hasattr(f, 'view_mode') else 'summary' + preset_files.append(models.ContextFileEntry(path=p, view_mode=vm)) + preset = models.ContextPreset(name=name, files=preset_files, screenshots=list(app.screenshots)) + app.controller.save_context_preset(preset) + app.ui_new_context_preset_name = "" + imgui.close_current_popup() + + imgui.same_line() + if imgui.button("Cancel"): + imgui.close_current_popup() + + imgui.end_popup() diff --git a/tests/test_gui_context_presets.py b/tests/test_gui_context_presets.py index f9d15036..1c80d82c 100644 --- a/tests/test_gui_context_presets.py +++ b/tests/test_gui_context_presets.py @@ -11,7 +11,20 @@ def test_gui_context_preset_save_load(live_gui) -> None: test_files = ["test.py"] test_screenshots = ["test.png"] - client.push_event("custom_callback", {"callback": "simulate_save_preset", "args": [preset_name]}) + # Switch to Context Composition tab to ensure it's rendered + client.push_event("select_tab", {"tab": "Context Composition"}) + time.sleep(1.0) + + # Inject context state directly + client.push_event("custom_callback", {"callback": "set_context_files_for_test", "args": [test_files]}) + client.push_event("custom_callback", {"callback": "set_screenshots_for_test", "args": [test_screenshots]}) + client.push_event("custom_callback", {"callback": "set_ui_attr", "args": ["ui_new_context_preset_name", preset_name]}) + time.sleep(1.0) + # Trigger Save (which will trigger validation, and since test.py doesn't exist, it opens a modal) + client.push_event("custom_callback", {"callback": "set_ui_attr", "args": ["_pending_save_ctx_click", True]}) + time.sleep(1.0) + # The "Missing Files Warning" modal should be open. Trigger "Save Anyway". + client.push_event("custom_callback", {"callback": "set_ui_attr", "args": ["_pending_save_anyway_click", True]}) time.sleep(1.5) project_data = client.get_project() @@ -25,6 +38,11 @@ def test_gui_context_preset_save_load(live_gui) -> None: assert preset_files == test_files assert preset_entry.get("screenshots", []) == test_screenshots + # Clear current state + client.push_event("custom_callback", {"callback": "set_context_files_for_test", "args": [[]]}) + client.push_event("custom_callback", {"callback": "set_screenshots_for_test", "args": [[]]}) + time.sleep(1.0) + # Load the preset client.push_event("custom_callback", {"callback": "load_context_preset", "args": [preset_name]}) time.sleep(1.0)