From c1487d32bb039956a7b2801c86074575d99fa196 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sun, 17 May 2026 06:55:00 -0400 Subject: [PATCH] progress on context composition --- src/app_controller.py | 22 +++++++++++++++++- src/gui_2.py | 54 +++++++++++++++++++++++++++++++++++-------- src/models.py | 8 ++++++- 3 files changed, 72 insertions(+), 12 deletions(-) diff --git a/src/app_controller.py b/src/app_controller.py index 887c1202..f3f0040d 100644 --- a/src/app_controller.py +++ b/src/app_controller.py @@ -2933,9 +2933,25 @@ class AppController: self.ui_file_paths = [f.path for f in preset.files] self.screenshots = list(preset.screenshots) self._save_active_project() + # We need to tell gui_2 to populate the full FileItem state from the preset, + # which it does in _handle_refresh_from_project. But we also need to pass the detailed properties. + # We will let the project_manager handle merging in the preset files via configuration next turn. + # Wait, project manager doesn't load preset files into self.files automatically here. + # Let's write the preset files into self.project["files"] directly. + import copy + self.project.setdefault("files", {})["paths"] = [ + { + "path": f.path, + "view_mode": f.view_mode, + "custom_slices": copy.deepcopy(f.custom_slices), + "ast_mask": copy.deepcopy(f.ast_mask), + "ast_signatures": getattr(f, "ast_signatures", False), + "ast_definitions": getattr(f, "ast_definitions", False) + } for f in preset.files + ] + self._save_active_project() return preset - def _cb_load_track(self, track_id: str) -> None: """ [C: src/gui_2.py:App._render_mma_track_browser] @@ -3445,6 +3461,10 @@ class AppController: flat = project_manager.flat_config(self.project, self.active_discussion, track_id=track_id) flat.setdefault("files", {})["paths"] = self.context_files + # Configure MCP so that aggregate.py can fetch skeletons for external files (e.g. gencpp) + file_dicts = [f.to_dict() if hasattr(f, 'to_dict') else {"path": str(f)} for f in self.context_files] + mcp_client.configure(file_dicts, [self.active_project_root] if self.active_project_root else None) + persona = self.personas.get(self.ui_active_persona) strategy = persona.aggregation_strategy if persona else "auto" diff --git a/src/gui_2.py b/src/gui_2.py index 5f2fda6a..e328deb4 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -153,12 +153,15 @@ class App: 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)) + slc = copy.deepcopy(f.custom_slices) if hasattr(f, 'custom_slices') else [] + msk = copy.deepcopy(f.ast_mask) if hasattr(f, 'ast_mask') else {} + sig = f.ast_signatures if hasattr(f, 'ast_signatures') else False + dfn = f.ast_definitions if hasattr(f, 'ast_definitions') else False + preset_files.append(models.ContextFileEntry(path=p, view_mode=vm, custom_slices=slc, ast_mask=msk, ast_signatures=sig, ast_definitions=dfn)) 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.show_missing_files_modal = False self.controller._predefined_callbacks['get_app_debug_info'] = lambda: self.app_debug_info self.controller._gettable_fields['app_debug_info'] = 'app_debug_info' self.controller._predefined_callbacks['save_context_preset_force'] = _save_context_preset_force @@ -3011,6 +3014,9 @@ def render_ast_inspector_modal(app: App) -> None: imgui.same_line() imgui.text(f"[{kind}] {name}") + if imgui.is_item_hovered(): + app._hovered_ast_node = full_path + # Calculate space left and align radio buttons to the right btn_width = 150 # Estimated width of the 3 radio buttons avail_width = imgui.get_content_region_avail().x @@ -3061,6 +3067,9 @@ def render_ast_inspector_modal(app: App) -> None: elif mode == 'sig': # Blue, alpha 0.2 draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(0, 0, 255, 0.2))) + elif deepest_node and deepest_node['full_path'] == getattr(app, '_hovered_ast_node', None): + # Yellow, alpha 0.3 for hover + draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(255, 255, 0, 0.3))) imgui.text(f"{line_num:4} | {line_text}") imgui.end_child() @@ -4126,7 +4135,7 @@ def render_text_viewer_window(app: App) -> None: """Renders the standalone text/code/markdown viewer window.""" if not app.show_text_viewer: return imgui.set_next_window_size(imgui.ImVec2(900, 700), imgui.Cond_.first_use_ever) - expanded, opened = imgui.begin(f"Text Viewer - {app.text_viewer_title}", app.show_text_viewer) + expanded, opened = imgui.begin(f"{app.text_viewer_title or 'Text Viewer'}###Text_Viewer", app.show_text_viewer) app.show_text_viewer = bool(opened) if not opened: app.ui_editing_slices_file = None @@ -4148,20 +4157,45 @@ def render_text_viewer_window(app: App) -> None: if imgui.button("Clear Selection"): app._slice_sel_start = -1; app._slice_sel_end = -1 imgui.same_line() if imgui.button("Auto-Populate AST Slices"): app._populate_auto_slices(app.ui_editing_slices_file) - + imgui.same_line() + if imgui.button("Edit Tags"): imgui.open_popup("Edit Context Tags") + + if imgui.begin_popup("Edit Context Tags"): + tags = app.controller.project.setdefault("context_tags", ["auto-ast", "bug", "feature", "important"]) + imgui.text("Context Tags") + imgui.separator() + to_remove_tag = -1 + for i, t in enumerate(tags): + imgui.push_id(f"tag_{i}") + imgui.set_next_item_width(150) + ch, new_t = imgui.input_text("##t", t) + if ch: tags[i] = new_t + imgui.same_line() + if imgui.button("X"): to_remove_tag = i + imgui.pop_id() + if to_remove_tag != -1: tags.pop(to_remove_tag) + if imgui.button("+ Add Tag"): tags.append("new-tag") + if imgui.button("Close"): imgui.close_current_popup() + imgui.end_popup() + to_remove = -1 + tags = app.controller.project.get("context_tags", ["auto-ast", "bug", "feature", "important"]) for idx, slc in enumerate(app.ui_editing_slices_file.custom_slices): imgui.push_id(f"slc_row_{idx}"); imgui.text(f"Slice {idx+1}: {slc['start_line']}-{slc['end_line']}"); imgui.same_line() - imgui.set_next_item_width(100); changed_tag, new_tag = imgui.input_text("Tag", slc.get('tag', '')) - if changed_tag: slc['tag'] = new_tag - imgui.same_line(); imgui.set_next_item_width(200); changed_comm, new_comm = imgui.input_text("Comment", slc.get('comment', '')) + current_tag = slc.get('tag', '') + if current_tag not in tags and current_tag: tags.append(current_tag) + tag_idx = tags.index(current_tag) if current_tag in tags else 0 + imgui.set_next_item_width(150) + ch_tag, new_tag_idx = imgui.combo("Category/Tag", tag_idx, tags) + if ch_tag: slc['tag'] = tags[new_tag_idx] + imgui.same_line(); imgui.set_next_item_width(300); changed_comm, new_comm = imgui.input_text("Note/Comment", slc.get('comment', '')) if changed_comm: slc['comment'] = new_comm imgui.same_line() if imgui.button("Remove"): to_remove = idx imgui.pop_id() if to_remove != -1: app.ui_editing_slices_file.custom_slices.pop(to_remove) - imgui.separator() - if imgui.button("Copy"): imgui.set_clipboard_text(app.text_viewer_content) + imgui.separator() + if imgui.button("Copy"): imgui.set_clipboard_text(app.text_viewer_content) imgui.same_line(); _, app.text_viewer_wrap = imgui.checkbox("Word Wrap", app.text_viewer_wrap) imgui.separator() renderer = markdown_helper.get_renderer(); tv_type = getattr(app, "text_viewer_type", "text") diff --git a/src/models.py b/src/models.py index 137bf063..eb0648a7 100644 --- a/src/models.py +++ b/src/models.py @@ -827,12 +827,15 @@ class ContextFileEntry: path: str view_mode: str = "summary" custom_slices: list = field(default_factory=list) + ast_mask: dict = field(default_factory=dict) + ast_signatures: bool = False + ast_definitions: bool = False def to_dict(self) -> dict[str, Any]: """ [C: src/personas.py:PersonaManager.save_persona, src/presets.py:PresetManager.save_preset, src/project_manager.py:save_project, src/project_manager.py:save_track_state, src/tool_presets.py:ToolPresetManager.save_bias_profile, src/tool_presets.py:ToolPresetManager.save_preset, src/workspace_manager.py:WorkspaceManager.save_profile, tests/test_bias_models.py:test_bias_profile_model, tests/test_bias_models.py:test_tool_model, tests/test_bias_models.py:test_tool_preset_extension, tests/test_context_presets_models.py:test_context_preset_serialization, tests/test_context_presets_models.py:test_file_view_preset_serialization, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_round_trip_annotations, tests/test_custom_slices_annotations.py:test_file_item_custom_slices_serialization_with_annotations, tests/test_event_serialization.py:test_user_request_event_serialization, tests/test_external_editor.py:TestExternalEditorConfig.test_to_dict, tests/test_external_editor.py:TestTextEditorConfig.test_to_dict, tests/test_file_item_model.py:test_file_item_to_dict, tests/test_gui_events_v2.py:test_user_request_event_payload, tests/test_history_manager.py:TestHistoryManager.test_snapshot_roundtrip, tests/test_mcp_config.py:test_mcp_configuration_to_from_dict, tests/test_mcp_config.py:test_mcp_server_config_to_from_dict, tests/test_per_ticket_model.py:test_model_override_serialization, tests/test_persona_id.py:test_ticket_persona_id_serialization, tests/test_persona_models.py:test_persona_defaults, tests/test_persona_models.py:test_persona_serialization, tests/test_slice_editor_behavior.py:test_add_slice_with_annotations, tests/test_thinking_gui.py:test_thinking_segment_model_compatibility, tests/test_ticket_queue.py:test_ticket_to_dict_priority, tests/test_tiered_aggregation.py:test_persona_aggregation_strategy, tests/test_track_state_schema.py:test_track_state_to_dict, tests/test_track_state_schema.py:test_track_state_to_dict_with_none, tests/test_ui_summary_only_removal.py:test_file_item_serialization_with_flags] """ - return {"path": self.path, "view_mode": self.view_mode, "custom_slices": self.custom_slices} + return {"path": self.path, "view_mode": self.view_mode, "custom_slices": self.custom_slices, "ast_mask": self.ast_mask, "ast_signatures": self.ast_signatures, "ast_definitions": self.ast_definitions} @classmethod def from_dict(cls, data: dict[str, Any]) -> "ContextFileEntry": @@ -843,6 +846,9 @@ class ContextFileEntry: path=data.get("path", ""), view_mode=data.get("view_mode", "summary"), custom_slices=data.get("custom_slices", []), + ast_mask=data.get("ast_mask", {}), + ast_signatures=data.get("ast_signatures", False), + ast_definitions=data.get("ast_definitions", False), ) @dataclass