From bf84058ca80d306ebd739fe351937a735201776d Mon Sep 17 00:00:00 2001 From: Ed_ Date: Wed, 13 May 2026 07:27:41 -0400 Subject: [PATCH] possibly finished reognizating gui_2.py... --- src/gui_2.py | 3254 +++++++++++++++++++++++++------------------------- 1 file changed, 1628 insertions(+), 1626 deletions(-) diff --git a/src/gui_2.py b/src/gui_2.py index 2ea7203..78b4a6c 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -1,6 +1,6 @@ # gui_2.py +# defer: parse from __future__ import annotations -import copy import datetime import difflib import json @@ -257,6 +257,112 @@ class App: """UI-level wrapper for approving a pending tool execution ask.""" self._handle_approve_ask() + def _post_init(self) -> None: + theme.apply_current() + + def run(self) -> None: + """ + Initializes the ImGui runner and starts the main application loop. + [C: simulation/sim_base.py:run_sim, src/mcp_client.py:get_git_diff, src/project_manager.py:get_git_commit, src/project_manager.py:get_git_log, src/rag_engine.py:RAGEngine._search_mcp, src/shell_runner.py:run_powershell, tests/conftest.py:kill_process_tree, tests/conftest.py:live_gui, tests/test_conductor_abort_event.py:test_conductor_abort_event_populated, tests/test_conductor_engine_v2.py:test_conductor_engine_dynamic_parsing_and_execution, tests/test_conductor_engine_v2.py:test_conductor_engine_run_executes_tickets_in_order, tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_extended_sims.py:test_context_sim_live, tests/test_extended_sims.py:test_execution_sim_live, tests/test_extended_sims.py:test_tools_sim_live, tests/test_external_editor_gui.py:get_vscode_processes, tests/test_external_editor_gui.py:test_vscode_launches_with_diff_view, tests/test_gui_custom_window.py:test_app_window_is_borderless, tests/test_headless_simulation.py:module, tests/test_headless_verification.py:test_headless_verification_error_and_qa_interceptor, tests/test_headless_verification.py:test_headless_verification_full_run, tests/test_mock_gemini_cli.py:run_mock, tests/test_orchestration_logic.py:test_conductor_engine_run, tests/test_parallel_execution.py:test_conductor_engine_pool_integration, tests/test_sim_ai_settings.py:test_ai_settings_simulation_run, tests/test_sim_context.py:test_context_simulation_run, tests/test_sim_execution.py:test_execution_simulation_run, tests/test_sim_tools.py:test_tools_simulation_run] + """ + if "--headless" in sys.argv: + print("Headless mode active") + self._fetch_models(self.current_provider) + import uvicorn + headless_cfg = self.config.get("headless", {}) + port = headless_cfg.get("port", 8000) + api = self.create_api() + uvicorn.run(api, host="0.0.0.0", port=port) + else: + theme.load_from_config(self.config) + self.runner_params = hello_imgui.RunnerParams() + self.runner_params.app_window_params.window_title = "manual slop" + + if sys.platform == "win32": + self.runner_params.app_window_params.borderless = True + self.runner_params.app_window_params.borderless_closable = False + self.runner_params.app_window_params.borderless_movable = False + self.runner_params.app_window_params.borderless_resizable = True + + self.runner_params.app_window_params.window_geometry.size = (1680, 1200) + self.runner_params.imgui_window_params.enable_viewports = getattr(self, "ui_multi_viewport", False) + self.runner_params.imgui_window_params.remember_theme = True + self.runner_params.imgui_window_params.tweaked_theme = theme.get_tweaked_theme() + self.runner_params.imgui_window_params.default_imgui_window_type = hello_imgui.DefaultImGuiWindowType.provide_full_screen_dock_space + + # Enforce DPI Awareness and User Scale + user_scale = theme.get_current_scale() + self.runner_params.dpi_aware_params.dpi_window_size_factor = user_scale + + # Detect Monitor Refresh Rate for capping (Win32 only) + fps_cap = 60.0 + if sys.platform == "win32": + try: + # Use PowerShell to get max refresh rate across all controllers + cmd = "powershell -NoProfile -Command \"Get-CimInstance -ClassName Win32_VideoController | Select-Object -ExpandProperty CurrentRefreshRate\"" + out = subprocess.check_output(cmd, shell=True).decode().splitlines() + rates = [float(r.strip()) for r in out if r.strip().isdigit()] + if rates: fps_cap = max(rates) + except Exception: pass + + # Enable idling with monitor refresh rate to effectively cap FPS + self.runner_params.fps_idling.enable_idling = True + self.runner_params.fps_idling.fps_idle = fps_cap + + self.runner_params.imgui_window_params.show_menu_bar = True + self.runner_params.imgui_window_params.show_menu_view_themes = True + self.runner_params.ini_folder_type = hello_imgui.IniFolderType.current_folder + self.runner_params.ini_filename = "manualslop_layout.ini" + self.runner_params.callbacks.show_gui = self._gui_func + self.runner_params.callbacks.show_menus = self._show_menus + self.runner_params.callbacks.load_additional_fonts = self._load_fonts + self.runner_params.callbacks.setup_imgui_style = theme.apply_current + self.runner_params.callbacks.post_init = self._post_init + self._fetch_models(self.current_provider) + md_options = markdown_helper.get_renderer().options + immapp.run(self.runner_params, add_ons_params=immapp.AddOnsParams(with_markdown_options=md_options)) + # On exit + self.shutdown() + session_logger.close_session() + + def _load_fonts(self) -> None: + # Set hello_imgui assets folder to the actual absolute path + assets_dir = Path(__file__).parent.parent / "assets" + if assets_dir.exists(): + hello_imgui.set_assets_folder(str(assets_dir.absolute())) + + # Improved font rendering with oversampling + config = imgui.ImFontConfig() + config.oversample_h = 3 + config.oversample_v = 3 + + font_path, font_size = theme.get_font_loading_params() + + if font_path: + p = Path(font_path) + if p.is_absolute(): + try: + if p.is_relative_to(assets_dir): + font_path = str(p.relative_to(assets_dir)).replace("\\", "/") + except (ValueError, AttributeError): + pass # Fallback to original font_path if relative_to fails or on old Python + + # Just try loading it directly; hello_imgui will look in the assets folder + try: + self.main_font = hello_imgui.load_font_ttf_with_font_awesome_icons(font_path, font_size, config) + except Exception as e: + print(f"Failed to load main font {font_path}: {e}") + self.main_font = None + else: + self.main_font = None + + try: + params = hello_imgui.FontLoadingParams(font_config=config) + self.mono_font = hello_imgui.load_font("fonts/MapleMono-Regular.ttf", font_size, params) + except Exception as e: + print(f"Failed to load mono font: {e}") + self.mono_font = None + def _handle_approve_mma_step(self, user_data=None) -> None: """UI-level wrapper for approving a pending MMA step.""" self._handle_mma_respond(approved=True) @@ -614,6 +720,30 @@ class App: if width > 0: imgui.set_next_item_width(width) imgui.input_text("##" + label, value, imgui.InputTextFlags_.read_only) + def _render_save_preset_modal(self) -> None: + if not self._show_save_preset_modal: return + imgui.open_popup("Save Layout Preset") + with imscope.popup_modal("Save Layout Preset", True, imgui.WindowFlags_.always_auto_resize) as (opened, _): + if opened: + imgui.text("Preset Name:") + _, self._new_preset_name = imgui.input_text("##preset_name", self._new_preset_name) + if imgui.button("Save", imgui.ImVec2(120, 0)): + if self._new_preset_name.strip(): + ini_data = imgui.save_ini_settings_to_memory() + self.layout_presets[self._new_preset_name.strip()] = { + "ini": ini_data, + "multi_viewport": self.ui_multi_viewport + } + self.config["layout_presets"] = self.layout_presets + models.save_config(self.config) + self._show_save_preset_modal = False + self._new_preset_name = "" + imgui.close_current_popup() + imgui.same_line() + if imgui.button("Cancel", imgui.ImVec2(120, 0)): + self._show_save_preset_modal = False + imgui.close_current_popup() + def _gui_func(self) -> None: self._render_custom_title_bar() self._render_shader_live_editor() @@ -949,6 +1079,131 @@ class App: traceback.print_exc(file=sys.stderr) sys.stderr.flush() + def _render_theme_panel(self) -> None: + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_theme_panel") + exp, opened = imgui.begin("Theme", self.show_windows["Theme"]) + self.show_windows["Theme"] = bool(opened) + if exp: + imgui.text("Palette") + cp = theme.get_current_palette() + if imgui.begin_combo("##pal", cp): + for p in theme.get_palette_names(): + if imgui.selectable(p, p == cp)[0]: + theme.apply(p) + self._flush_to_config() + models.save_config(self.config) + imgui.end_combo() + + imgui.separator() + ch1, self.ui_separate_message_panel = imgui.checkbox("Separate Message Panel", self.ui_separate_message_panel) + ch2, self.ui_separate_response_panel = imgui.checkbox("Separate Response Panel", self.ui_separate_response_panel) + ch3, self.ui_separate_tool_calls_panel = imgui.checkbox("Separate Tool Calls Panel", self.ui_separate_tool_calls_panel) + if ch1: self.show_windows["Message"] = self.ui_separate_message_panel + if ch2: self.show_windows["Response"] = self.ui_separate_response_panel + if ch3: self.show_windows["Tool Calls"] = self.ui_separate_tool_calls_panel + imgui.separator() + imgui.text("Font") + imgui.push_item_width(-150) + ch, path = imgui.input_text("##fontp", theme.get_current_font_path()) + imgui.pop_item_width() + if ch: theme._current_font_path = path + imgui.same_line() + if imgui.button("Browse##font"): + r = hide_tk_root() + p = filedialog.askopenfilename(filetypes=[("Fonts", "*.ttf *.otf"), ("All", "*.*")]) + r.destroy() + if p: theme._current_font_path = p + imgui.text("Size (px)") + imgui.same_line() + imgui.push_item_width(100) + ch, size = imgui.input_float("##fonts", theme.get_current_font_size(), 1.0, 1.0, "%.0f") + if ch: theme._current_font_size = size + imgui.pop_item_width() + imgui.same_line() + if imgui.button("Apply Font (Requires Restart)"): + self._flush_to_config() + models.save_config(self.config) + self.ai_status = "Font settings saved. Restart required." + imgui.separator() + imgui.text("UI Scale (DPI)") + ch, scale = imgui.slider_float("##scale", theme.get_current_scale(), 0.5, 3.0, "%.2f") + if ch: + theme.set_scale(scale) + self._flush_to_config() + models.save_config(self.config) + + imgui.text("Panel Transparency") + ch_t, trans = imgui.slider_float("##trans", theme.get_transparency(), 0.1, 1.0, "%.2f") + if ch_t: + theme.set_transparency(trans) + self._flush_to_config() + models.save_config(self.config) + + imgui.text("Panel Item Transparency") + ch_ct, ctrans = imgui.slider_float("##ctrans", theme.get_child_transparency(), 0.1, 1.0, "%.2f") + if ch_ct: + theme.set_child_transparency(ctrans) + bg = bg_shader.get_bg() + ch_bg, bg.enabled = imgui.checkbox("Animated Background Shader", bg.enabled) + if ch_bg: + gui_cfg = self.config.setdefault("gui", {}) + gui_cfg["bg_shader_enabled"] = bg.enabled + self._flush_to_config() + models.save_config(self.config) + + ch_crt, self.ui_crt_filter = imgui.checkbox("CRT Filter", self.ui_crt_filter) + if ch_crt: + gui_cfg = self.config.setdefault("gui", {}) + gui_cfg["crt_filter_enabled"] = self.ui_crt_filter + self._flush_to_config() + models.save_config(self.config) + self._flush_to_config() + models.save_config(self.config) + imgui.end() + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_theme_panel") + + if not imgui.collapsing_header("Discussions", imgui.TreeNodeFlags_.default_open): return + names = self._get_discussion_names(); grouped = {} + for name in names: + base = name.split("_take_")[0]; grouped.setdefault(base, []).append(name) + active_base = self.active_discussion.split("_take_")[0] + if active_base not in grouped: active_base = names[0] if names else "" + base_names = sorted(grouped.keys()) + if imgui.begin_combo("##disc_sel", active_base): + for bname in base_names: + is_selected = (bname == active_base) + if imgui.selectable(bname, is_selected)[0]: + target = bname if bname in names else grouped[bname][0] + if target != self.active_discussion: self._switch_discussion(target) + if is_selected: imgui.set_item_default_focus() + imgui.end_combo() + active_base = self.active_discussion.split("_take_")[0]; current_takes = grouped.get(active_base, []) + if imgui.begin_tab_bar("discussion_takes_tabs"): + for take_name in current_takes: + label = "Original" if take_name == active_base else take_name.replace(f"{active_base}_", "").replace("_", " ").title() + flags = imgui.TabItemFlags_.set_selected if take_name == self.active_discussion else 0 + with imscope.tab_item(f"{label}###{take_name}", flags) as (exp, _): + if exp and take_name != self.active_discussion: self._switch_discussion(take_name) + with imscope.tab_item("Synthesis###Synthesis") as (exp, _): + if exp: self._render_synthesis_panel() + imgui.end_tab_bar() + if "_take_" in self.active_discussion: + if imgui.button("Promote Take"): + base_name = self.active_discussion.split("_take_")[0]; new_name = f"{base_name}_promoted"; counter = 1 + while new_name in names: new_name = f"{base_name}_promoted_{counter}"; counter += 1 + project_manager.promote_take(self.project, self.active_discussion, new_name); self._switch_discussion(new_name) + imgui.same_line() + if self.active_track: + imgui.same_line(); ch, self._track_discussion_active = imgui.checkbox("Track Discussion", self._track_discussion_active) + if ch: + if self._track_discussion_active: + self._flush_disc_entries_to_project() + history_strings = project_manager.load_track_history(self.active_track.id, self.active_project_root) + with self._disc_entries_lock: self.disc_entries = models.parse_history_entries(history_strings, self.disc_roles) + self.ai_status = f"track discussion: {self.active_track.id}" + else: self._flush_disc_entries_to_project(); self._switch_discussion(self.active_discussion); self.ai_status = "track discussion disabled" + self._render_discussion_metadata() + def _render_shader_live_editor(self) -> None: """ [C: tests/test_shader_live_editor.py:test_shader_live_editor_renders] @@ -961,52 +1216,6 @@ class App: changed_scan, self.shader_uniforms['scanline'] = imgui.slider_float('Scanline Intensity', self.shader_uniforms['scanline'], 0.0, 1.0) changed_bloom, self.shader_uniforms['bloom'] = imgui.slider_float('Bloom Threshold', self.shader_uniforms['bloom'], 0.0, 1.0) - def _render_approve_script_modal(self) -> None: - """Renders the modal dialog for approving AI-generated PowerShell scripts.""" - with self._pending_dialog_lock: - dlg = self._pending_dialog - if dlg: - if not self._pending_dialog_open: - imgui.open_popup("Approve PowerShell Command") - self._pending_dialog_open = True - else: - self._pending_dialog_open = False - - if imgui.begin_popup_modal("Approve PowerShell Command", None, imgui.WindowFlags_.always_auto_resize)[0]: - if not dlg: - imgui.close_current_popup() - else: - imgui.text("The AI wants to run the following PowerShell script:") - imgui.text_colored(vec4(200, 200, 100), f"base_dir: {dlg._base_dir}") - imgui.separator() - # Checkbox to toggle full preview inside modal - _, self.show_text_viewer = imgui.checkbox("Show Full Preview", self.show_text_viewer) - if self.show_text_viewer: - imgui.begin_child("preview_child", imgui.ImVec2(600, 300), True) - imgui.text_unformatted(dlg._script) - imgui.end_child() - else: - ch, dlg._script = imgui.input_text_multiline("##confirm_script", dlg._script, imgui.ImVec2(-1, 200)) - imgui.separator() - if imgui.button("Approve & Run", imgui.ImVec2(120, 0)): - with dlg._condition: - dlg._approved = True - dlg._done = True - dlg._condition.notify_all() - with self._pending_dialog_lock: - self._pending_dialog = None - imgui.close_current_popup() - imgui.same_line() - if imgui.button("Reject", imgui.ImVec2(120, 0)): - with dlg._condition: - dlg._approved = False - dlg._done = True - dlg._condition.notify_all() - with self._pending_dialog_lock: - self._pending_dialog = None - imgui.close_current_popup() - imgui.end_popup() - #region: Diangostics & Analytics def _render_usage_analytics_panel(self) -> None: @@ -1321,6 +1530,303 @@ class App: #endregion: Diangostics & Analytics +#region: Logging + + def _render_log_management(self) -> None: + """ + [C: tests/test_log_management_ui.py:test_render_log_management_logic] + """ + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_log_management") + with imscope.window("Log Management", self.show_windows["Log Management"]) as (exp, opened): + self.show_windows["Log Management"] = bool(opened) + if exp: + if self._log_registry is None: self._log_registry = log_registry.LogRegistry(str(paths.get_logs_dir() / "log_registry.toml")) + else: + if imgui.button("Refresh Registry"): self._log_registry = log_registry.LogRegistry(str(paths.get_logs_dir() / "log_registry.toml")) + imgui.same_line() + if imgui.button("Load Log"): self.cb_load_prior_log() + imgui.same_line() + if imgui.button("Force Prune Logs"): self.controller.event_queue.put("gui_task", {"action": "click", "item": "btn_prune_logs"}) + + registry = self._log_registry + sessions = registry.data + if imgui.begin_table("sessions_table", 7, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable): + imgui.table_setup_column("Session ID") + imgui.table_setup_column("Start Time") + imgui.table_setup_column("Star") + imgui.table_setup_column("Reason") + imgui.table_setup_column("Size (KB)") + imgui.table_setup_column("Msgs") + imgui.table_setup_column("Actions") + imgui.table_headers_row() + for session_id, s_data in sessions.items(): + imgui.table_next_row() + imgui.table_next_column() + imgui.text(session_id) + imgui.table_next_column() + imgui.text(s_data.get("start_time", "")) + imgui.table_next_column() + whitelisted = s_data.get("whitelisted", False) + if whitelisted: + imgui.text_colored(vec4(255, 215, 0), "YES") + else: + imgui.text("NO") + metadata = s_data.get("metadata") or {} + imgui.table_next_column() + imgui.text(metadata.get("reason", "")) + imgui.table_next_column() + imgui.text(str(metadata.get("size_kb", ""))) + imgui.table_next_column() + imgui.text(str(metadata.get("message_count", ""))) + imgui.table_next_column() + if imgui.button(f"Load##{session_id}"): + self.cb_load_prior_log(s_data.get("path")) + imgui.same_line() + if whitelisted: + if imgui.button(f"Unstar##{session_id}"): + registry.update_session_metadata( + session_id, + message_count=int(metadata.get("message_count") or 0), + errors=int(metadata.get("errors") or 0), + size_kb=int(metadata.get("size_kb") or 0), + whitelisted=False, + reason=str(metadata.get("reason") or "") + ) + else: + if imgui.button(f"Star##{session_id}"): + registry.update_session_metadata( + session_id, + message_count=int(metadata.get("message_count") or 0), + errors=int(metadata.get("errors") or 0), + size_kb=int(metadata.get("size_kb") or 0), + whitelisted=True, + reason="Manually whitelisted" + ) + imgui.end_table() + + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_log_management") + + def cb_load_prior_log(self, path: Optional[str] = None) -> None: + if path is None: + root = hide_tk_root() + path = filedialog.askdirectory(title='Select Session Directory', initialdir=str(paths.get_logs_dir())) + root.destroy() + if path: + self.controller.cb_load_prior_log(path) + +#endregion: Logging + +#region: Project Management + + def _render_project_settings_hub(self) -> None: + with imscope.tab_bar('context_hub_tabs'): + with imscope.tab_item('Projects') as (exp, _): + if exp: self._render_projects_panel() + with imscope.tab_item('Paths') as (exp, _): + if exp: self._render_paths_panel() + + def _render_projects_panel(self) -> None: + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_projects_panel") + proj_name = self.project.get("project", {}).get("name", Path(self.active_project_path).stem) + imgui.text_colored(C_IN, f"Active: {proj_name}") + imgui.separator() + imgui.text("Execution Mode") + modes = ["native", "beads"] + current_idx = modes.index(self.ui_project_execution_mode) if self.ui_project_execution_mode in modes else 0 + ch, new_idx = imgui.combo("##exec_mode", current_idx, modes) + if ch: + self.ui_project_execution_mode = modes[new_idx] + imgui.separator() + imgui.text("Git Directory") + ch, self.ui_project_git_dir = imgui.input_text("##git_dir", self.ui_project_git_dir) + imgui.same_line() + if imgui.button("Browse##git"): + r = hide_tk_root() + d = filedialog.askdirectory(title="Select Git Directory") + r.destroy() + if d: self.ui_project_git_dir = d + imgui.separator() + imgui.text("Output Dir") + ch, self.ui_output_dir = imgui.input_text("##out_dir", self.ui_output_dir) + imgui.same_line() + if imgui.button("Browse##out"): + r = hide_tk_root() + d = filedialog.askdirectory(title="Select Output Dir") + r.destroy() + if d: self.ui_output_dir = d + imgui.separator() + imgui.text("Conductor Directory") + ch, self.ui_project_conductor_dir = imgui.input_text("##cond_dir", self.ui_project_conductor_dir) + imgui.same_line() + if imgui.button("Browse##cond"): + r = hide_tk_root() + d = filedialog.askdirectory(title="Select Conductor Directory") + r.destroy() + if d: self.ui_project_conductor_dir = d + imgui.separator() + imgui.text("Project Files") + imgui.begin_child("proj_files", imgui.ImVec2(0, 150), True) + for i, pp in enumerate(self.project_paths): + is_active = (pp == self.active_project_path) + if imgui.button(f"x##p{i}"): + removed = self.project_paths.pop(i) + if removed == self.active_project_path and self.project_paths: + self._switch_project(self.project_paths[0]) + break + imgui.same_line() + marker = " *" if is_active else "" + if is_active: imgui.push_style_color(imgui.Col_.text, C_IN) + if imgui.button(f"{Path(pp).stem}{marker}##ps{i}"): + self._switch_project(pp) + if is_active: imgui.pop_style_color() + imgui.same_line() + imgui.text_colored(C_LBL, pp) + imgui.end_child() + if imgui.button("Add Project"): + r = hide_tk_root() + p = filedialog.askopenfilename( + title="Select Project .toml", + filetypes=[("TOML", "*.toml"), ("All", "*.*")], + ) + r.destroy() + if p and p not in self.project_paths: + self.project_paths.append(p) + imgui.same_line() + if imgui.button("New Project"): + r = hide_tk_root() + p = filedialog.asksaveasfilename(title="Create New Project .toml", defaultextension=".toml", filetypes=[("TOML", "*.toml"), ("All", "*.*")]) + r.destroy() + if p: + name = Path(p).stem + proj = project_manager.default_project(name) + project_manager.save_project(proj, p) + if p not in self.project_paths: + self.project_paths.append(p) + self._switch_project(p) + imgui.same_line() + if imgui.button("Save All"): + self._flush_to_project() + self._flush_to_config() + models.save_config(self.config) + self.ai_status = "config saved" + ch, self.ui_word_wrap = imgui.checkbox("Word-Wrap (Read-only panels)", self.ui_word_wrap) + ch, self.ui_auto_scroll_comms = imgui.checkbox("Auto-scroll Comms History", self.ui_auto_scroll_comms) + ch, self.ui_auto_scroll_tool_calls = imgui.checkbox("Auto-scroll Tool History", self.ui_auto_scroll_tool_calls) + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_projects_panel") + + def _render_paths_panel(self) -> None: + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_paths_panel") + path_info = paths.get_full_path_info() + + imgui.text_colored(C_IN, "System Path Configuration") + imgui.separator() + + def render_path_field(label: str, attr: str, key: str, tooltip: str): + info = path_info.get(key, {'source': 'unknown'}) + imgui.text(label) + if imgui.is_item_hovered(): imgui.set_tooltip(tooltip) + imgui.same_line() + imgui.text_disabled(f"(Source: {info['source']})") + + val = getattr(self, attr) + changed, new_val = imgui.input_text(f"##{key}", val) + if imgui.is_item_hovered(): imgui.set_tooltip(tooltip) + if changed: setattr(self, attr, new_val) + imgui.same_line() + if imgui.button(f"Browse##{key}"): + r = hide_tk_root() + d = filedialog.askdirectory(title=f"Select {label}") + r.destroy() + if d: setattr(self, attr, d) + + def _render_external_tools_panel(self) -> None: + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_external_tools_panel") + if imgui.button("Refresh External MCPs"): + self.event_queue.put("refresh_external_mcps", None) + + imgui.separator() + + # Server status indicators + manager = mcp_client.get_external_mcp_manager() + statuses = manager.get_servers_status() + if statuses: + imgui.text("Servers:") + for sname, status in statuses.items(): + imgui.same_line() + # Green for running, Yellow for starting, Red for error, Gray for idle + col = (0.5, 0.5, 0.5, 1.0) + if status == 'running': + col = (0.0, 1.0, 0.0, 1.0) + elif status == 'starting': + col = (1.0, 1.0, 0.0, 1.0) + elif status == 'error': + col = (1.0, 0.0, 0.0, 1.0) + imgui.color_button(f"##status_{sname}", col) + imgui.same_line() + imgui.text(sname) + imgui.separator() + + tools = manager.get_all_tools() + if not tools: + imgui.text_disabled("No external tools found.") + else: + if imgui.begin_table("external_tools_table", 3, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable): + imgui.table_setup_column("Name") + imgui.table_setup_column("Server") + imgui.table_setup_column("Description") + imgui.table_headers_row() + + for tname, tinfo in tools.items(): + imgui.table_next_row() + imgui.table_next_column() + imgui.text(tname) + imgui.table_next_column() + imgui.text(tinfo.get('server', 'unknown')) + imgui.table_next_column() + imgui.text(tinfo.get('description', '')) + imgui.end_table() + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_external_tools_panel") + + def _set_external_editor_default(self, editor_name: str) -> None: + from src import models + if "tools" not in self.config: self.config["tools"] = {} + if "default_editor" not in self.config["tools"]: self.config["tools"]["default_editor"] = {} + self.config["tools"]["default_editor"]["default_editor"] = editor_name + models.save_config(self.config) + self.ai_status = f"Default editor set to: {editor_name}" + + + render_path_field("Logs Directory", "ui_logs_dir", "logs_dir", "Directory where session JSON-L logs and artifacts are stored.") + render_path_field("Scripts Directory", "ui_scripts_dir", "scripts_dir", "Directory for AI-generated PowerShell scripts.") + + imgui.separator() + if imgui.button("Apply", imgui.ImVec2(120, 0)): + self._save_paths() + imgui.same_line() + if imgui.button("Reset", imgui.ImVec2(120, 0)): + self.init_state() + self.ai_status = "paths reset to defaults" + + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_paths_panel") + + def _save_paths(self): + """ + [C: tests/test_gui_paths.py:test_save_paths] + """ + self.config["paths"] = { + "logs_dir": self.ui_logs_dir, + "scripts_dir": self.ui_scripts_dir + } + cfg_path = paths.get_config_path() + if cfg_path.exists(): + shutil.copy(cfg_path, str(cfg_path) + ".bak") + models.save_config(self.config) + paths.reset_resolved() + self.init_state() + self.ai_status = 'paths applied and session reset' + +#endregion: Project Management + #region: AI Settings def _render_ai_settings_hub(self) -> None: @@ -2014,6 +2520,94 @@ class App: ai_client._gemini_cli_adapter.binary_path = self.ui_gemini_cli_path if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_provider_panel") + def _render_persona_selector_panel(self) -> None: + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_persona_selector_panel") + imgui.text("Persona") + if not hasattr(self, 'ui_active_persona'): + self.ui_active_persona = "" + personas = getattr(self.controller, 'personas', {}) + if imgui.begin_combo("##persona", self.ui_active_persona or "None"): + if imgui.selectable("None", not self.ui_active_persona)[0]: + self.ui_active_persona = "" + for pname in sorted(personas.keys()): + if not pname: + continue + if imgui.selectable(pname, pname == self.ui_active_persona)[0]: + self.ui_active_persona = pname + if pname in personas: + persona = personas[pname] + self._editing_persona_name = persona.name + self._editing_persona_system_prompt = persona.system_prompt or "" + self._editing_persona_tool_preset_id = persona.tool_preset or "" + self._editing_persona_bias_profile_id = persona.bias_profile or "" + self._editing_persona_context_preset_id = getattr(persona, 'context_preset', '') or "" + self._editing_persona_aggregation_strategy = getattr(persona, 'aggregation_strategy', '') or "" + import copy + self._editing_persona_preferred_models_list = copy.deepcopy(persona.preferred_models) if persona.preferred_models else [] + self._editing_persona_is_new = False + + # Apply persona to current state immediately + if persona.preferred_models and len(persona.preferred_models) > 0: + first_model = persona.preferred_models[0] + if first_model.get("provider"): + self.current_provider = first_model.get("provider") + if first_model.get("model"): + self.current_model = first_model.get("model") + if first_model.get("temperature") is not None: + ai_client.temperature = first_model.get("temperature") + self.temperature = first_model.get("temperature") + if first_model.get("max_output_tokens"): + ai_client.max_output_tokens = first_model.get("max_output_tokens") + self.max_tokens = first_model.get("max_output_tokens") + if first_model.get("history_trunc_limit"): + self.history_trunc_limit = first_model.get("history_trunc_limit") + + if persona.system_prompt: + self.ui_project_system_prompt = persona.system_prompt + if persona.tool_preset: + self.ui_active_tool_preset = persona.tool_preset + ai_client.set_tool_preset(persona.tool_preset) + if persona.bias_profile: + self.ui_active_bias_profile = persona.bias_profile + ai_client.set_bias_profile(persona.bias_profile) + if getattr(persona, 'context_preset', None): + self.ui_active_context_preset = persona.context_preset + self.load_context_preset(persona.context_preset) + imgui.end_combo() + imgui.same_line() + if imgui.button("Manage Personas"): + self.show_persona_editor_window = True + if self.ui_active_persona and self.ui_active_persona in personas: + persona = personas[self.ui_active_persona] + self._editing_persona_name = persona.name + self._editing_persona_system_prompt = persona.system_prompt or "" + self._editing_persona_tool_preset_id = persona.tool_preset or "" + self._editing_persona_bias_profile_id = persona.bias_profile or "" + self._editing_persona_context_preset_id = getattr(persona, 'context_preset', '') or "" + self._editing_persona_aggregation_strategy = getattr(persona, 'aggregation_strategy', '') or "" + import copy + self._editing_persona_preferred_models_list = copy.deepcopy(persona.preferred_models) if persona.preferred_models else [] + self._editing_persona_scope = self.controller.persona_manager.get_persona_scope(persona.name) + self._editing_persona_is_new = False + else: + self._editing_persona_name = "" + self._editing_persona_system_prompt = "" + self._editing_persona_tool_preset_id = "" + self._editing_persona_bias_profile_id = "" + self._editing_persona_context_preset_id = "" + self._editing_persona_aggregation_strategy = "" + self._editing_persona_preferred_models_list = [{ + "provider": self.current_provider, + "model": self.current_model, + "temperature": getattr(self, "temperature", 0.7), + "max_output_tokens": getattr(self, "max_tokens", 4096), + "history_trunc_limit": getattr(self, "history_trunc_limit", 900000) + }] + self._editing_persona_scope = "project" + self._editing_persona_is_new = True + imgui.separator() + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_persona_selector_panel") + #endregion: AI Settings #region: Context Management @@ -2204,6 +2798,40 @@ class App: if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_files_panel") + def _render_screenshots_panel(self, height_override: float = 0) -> None: + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_screenshots_panel") + imgui.text("Paths"); imgui.same_line(); imgui.text("| Base Dir:"); imgui.same_line(); + imgui.set_next_item_width(-100) + ch, self.ui_shots_base_dir = imgui.input_text("##s_base", self.ui_shots_base_dir) + imgui.same_line() + if imgui.button("Browse##sb"): + r = hide_tk_root(); d = filedialog.askdirectory(); r.destroy() + if d: self.ui_shots_base_dir = d + imgui.separator() + # Calculate content-based height: use override if provided, else content-based + if height_override > 0: shot_h = height_override + else: + shot_count = max(len(self.screenshots), 1) + shot_h = min(shot_count * 28 + 40, 200) + # BEGIN s_paths child window + imgui.begin_child("s_paths", imgui.ImVec2(0, shot_h), True) + for i, s in enumerate(self.screenshots): + if imgui.button(f"x##s{i}"): + self.screenshots.pop(i) + break + imgui.same_line(); imgui.text(s) + imgui.end_child() + if imgui.button("Add Screenshot(s)"): + r = hide_tk_root() + paths = filedialog.askopenfilenames( + title="Select Screenshots", + filetypes=[("Images", "*.png *.jpg *.jpeg *.gif *.bmp *.webp"), ("All", "*.*")], + ) + r.destroy() + for p in paths: + if p not in self.screenshots: self.screenshots.append(p) + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_screenshots_panel") + def _render_context_composition_panel(self) -> None: if imgui.collapsing_header("Context Composition##panel"): total_lines, total_ast = self._update_context_file_stats() @@ -2483,6 +3111,228 @@ class App: for i, s in enumerate(self.screenshots): imgui.text(s) + def _render_context_batch_actions(self, total_lines: int, total_ast: int) -> None: + imgui.text("Batch:") + for mode in ["full", "summary", "skeleton", "outline", "masked", "none"]: + if imgui.button(f"{mode.capitalize()}##batch"): + 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.view_mode = mode + imgui.same_line() + if imgui.button("Sel All##selall"): + 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: + 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 = [] + 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.context_files = new_files + self.ui_selected_context_files.clear() + imgui.same_line() + imgui.text(f" | Total: {len(self.context_files)} files, {total_lines} lines, {total_ast} AST elements") + + def _render_context_files_table(self) -> None: + imgui.dummy(imgui.ImVec2(0, 4)) + grouped_files = aggregate.group_files_by_dir(self.context_files) + + with imscope.table("ctx_comp_table", 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders) as active: + if active: + imgui.table_setup_column("File", imgui.TableColumnFlags_.width_stretch) + imgui.table_setup_column("Flags", imgui.TableColumnFlags_.width_fixed, 200) + imgui.table_headers_row() + + file_indices = {id(f): idx for idx, f in enumerate(self.context_files)} + + for dir_name, g_files in grouped_files.items(): + imgui.table_next_row() + imgui.table_set_column_index(0) + with imscope.tree_node_ex(f"{dir_name}##dir_{dir_name}", imgui.TreeNodeFlags_.default_open) as is_open: + imgui.table_set_column_index(1) + if is_open: + for f_item in g_files: + i = file_indices[id(f_item)] + imgui.table_next_row() + imgui.table_set_column_index(0) + + f_path = f_item.path if hasattr(f_item, "path") else str(f_item) + is_sel = f_path in self.ui_selected_context_files + changed_sel, is_sel = imgui.checkbox(f"##sel{i}", is_sel) + if changed_sel: + if imgui.get_io().key_shift and self._last_selected_context_index != -1: + 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.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) + else: + self.ui_selected_context_files.discard(item_path) + else: + if is_sel: + self.ui_selected_context_files.add(f_path) + else: + self.ui_selected_context_files.discard(f_path) + self._last_selected_context_index = i + imgui.same_line() + + mtime = os.path.getmtime(f_path) if os.path.exists(f_path) else 0 + cache_key = f"{f_path}_{mtime}" + stats = self._file_stats_cache.get(cache_key, {"lines": 0, "ast_elements": 0}) + f_name = os.path.basename(f_path) + imgui.text(f"{f_name} (L: {stats.get('lines', 0)}, AST: {stats.get('ast_elements', 0)})") + + if f_path.lower().endswith(('.c', '.cpp', '.h', '.hpp', '.cxx', '.cc')): + imgui.same_line() + if imgui.button(f"[Inspect]##{i}"): + self.ui_inspecting_ast_file = f_item + self._show_ast_inspector = True + + imgui.same_line() + if imgui.button(f"[Slices]##{i}"): + self.ui_editing_slices_file = f_item + f_path = f_item.path if hasattr(f_item, "path") else str(f_item) + self.text_viewer_title = f"Slices: {f_path}" + try: + self.text_viewer_content = mcp_client.read_file(f_path) + except Exception as e: + self.text_viewer_content = f"Error reading file: {e}" + self.text_viewer_type = 'cpp' if f_path.endswith(('.cpp', '.hpp', '.h')) else 'python' if f_path.endswith('.py') else 'text' + self.show_text_viewer = True + + imgui.table_set_column_index(1) + if not hasattr(f_item, "view_mode"): + f_item.view_mode = "summary" + view_modes = ["full", "summary", "skeleton", "outline", "masked", "none"] + try: + current_idx = view_modes.index(f_item.view_mode) + except ValueError: + current_idx = 1 + f_item.view_mode = "summary" + imgui.set_next_item_width(120) + changed_vm, new_idx = imgui.combo(f"##vm{i}", current_idx, view_modes) + if changed_vm: + f_item.view_mode = view_modes[new_idx] + + imgui.same_line() + if imgui.button(f"[Save]##vpsave{i}"): + imgui.open_popup(f"save_vp_popup{i}") + + if imgui.begin_popup(f"save_vp_popup{i}"): + imgui.text("Preset Name:") + changed_pname, self.ui_new_vp_name = imgui.input_text(f"##pname{i}", self.ui_new_vp_name) + if imgui.button("OK"): + if self.ui_new_vp_name.strip(): + self.controller._cb_save_view_preset(self.ui_new_vp_name.strip(), f_item) + self.ui_new_vp_name = "" + imgui.close_current_popup() + imgui.end_popup() + + imgui.same_line() + if imgui.button(f"[Load]##vpload{i}"): + imgui.open_popup(f"load_vp_popup{i}") + + if imgui.begin_popup(f"load_vp_popup{i}"): + vp_names = sorted([vp.name for vp in self.controller.view_presets]) + if not vp_names: + imgui.text("No presets saved.") + for vp_name in vp_names: + if imgui.selectable(vp_name): + self.controller._cb_apply_view_preset(vp_name, f_item) + imgui.close_current_popup() + imgui.end_popup() + if hasattr(f_item, "custom_slices") and f_item.custom_slices: + imgui.same_line() + imgui.text_colored(imgui.ImVec4(1.0, 0.5, 0.0, 1.0), "[Slices Active]") + + def _render_context_presets(self) -> None: + imgui.text("Presets") + presets = self.controller.project.get('context_presets', {}) + preset_names = [""] + sorted(presets.keys()) + active = getattr(self, "ui_active_context_preset", "") + if active not in preset_names: + active = "" + try: + idx = preset_names.index(active) + except ValueError: + idx = 0 + ch, new_idx = imgui.combo("##ctx_preset", idx, preset_names) + if ch: + self.ui_active_context_preset = preset_names[new_idx] + if preset_names[new_idx]: + self.load_context_preset(preset_names[new_idx]) + imgui.same_line() + changed, new_name = imgui.input_text("##new_preset", getattr(self, "ui_new_context_preset_name", "")) + if changed: + self.ui_new_context_preset_name = new_name + imgui.same_line() + if imgui.button("Save##ctx"): + if getattr(self, "ui_new_context_preset_name", "").strip(): + self.save_context_preset(self.ui_new_context_preset_name.strip()) + self.ui_new_context_preset_name = "" + imgui.same_line() + if imgui.button("Delete##ctx"): + if getattr(self, "ui_active_context_preset", ""): + self.delete_context_preset(self.ui_active_context_preset) + self.ui_active_context_preset = "" + + def _update_context_file_stats(self) -> tuple[int, int]: + if not hasattr(self, '_file_stats_cache'): + self._file_stats_cache = {} + if not hasattr(self, '_file_stats_queue'): + self._file_stats_queue = [] + if not hasattr(self, '_file_stats_worker_active'): + self._file_stats_worker_active = False + + total_lines = 0 + total_ast = 0 + + missing_keys = [] + for f in self.context_files: + f_path = f.path if hasattr(f, "path") else str(f) + mtime = os.path.getmtime(f_path) if os.path.exists(f_path) else 0 + cache_key = f"{f_path}_{mtime}" + if cache_key not in self._file_stats_cache: + missing_keys.append((f_path, cache_key)) + else: + stats = self._file_stats_cache[cache_key] + total_lines += stats.get("lines", 0) + total_ast += stats.get("ast_elements", 0) + + if missing_keys and not self._file_stats_worker_active: + def _stats_worker(): + self._file_stats_worker_active = True + try: + for path, key in missing_keys[:10]: + self._file_stats_cache[key] = aggregate.compute_file_stats(path) + finally: + self._file_stats_worker_active = False + + threading.Thread(target=_stats_worker, daemon=True).start() + + return total_lines, total_ast + #endregion: Context Management #region: Discussions @@ -2813,48 +3663,6 @@ class App: self.disc_entries.append({"role": "user", "content": prompt, "collapsed": False, "ts": project_manager.now_ts()}) self._handle_generate_send() -#endregion: Discussions - - def _render_operations_hub(self) -> None: - imgui.push_style_var(imgui.StyleVar_.item_spacing, imgui.ImVec2(10, 4)) - ch1, self.ui_separate_tool_calls_panel = imgui.checkbox("Pop Out Tool Calls", self.ui_separate_tool_calls_panel) - if ch1: self.show_windows["Tool Calls"] = self.ui_separate_tool_calls_panel - imgui.same_line() - ch2, self.ui_separate_usage_analytics = imgui.checkbox("Pop Out Usage Analytics", self.ui_separate_usage_analytics) - if ch2: self.show_windows["Usage Analytics"] = self.ui_separate_usage_analytics - imgui.same_line() - ch3, self.ui_separate_external_tools = imgui.checkbox('Pop Out External Tools', self.ui_separate_external_tools) - if ch3: self.show_windows['External Tools'] = self.ui_separate_external_tools - imgui.pop_style_var() - show_tc_tab, show_usage_tab = not self.ui_separate_tool_calls_panel, not self.ui_separate_usage_analytics - with imscope.tab_bar("ops_tabs"): - with imscope.tab_item("Comms History") as (exp, _): - if exp: self._render_comms_history_panel() - if show_tc_tab: - with imscope.tab_item("Tool Calls") as (exp, _): - if exp: self._render_tool_calls_panel() - if show_usage_tab: - with imscope.tab_item("Usage Analytics") as (exp, _): - if exp: self._render_usage_analytics_panel() - if not self.ui_separate_external_tools: - with imscope.tab_item("External Tools") as (exp, _): - if exp: - self._render_external_tools_panel() - imgui.separator(); imgui.text("") - try: self._render_external_editor_panel() - except Exception as e: imgui.text_colored(vec4(1, 0.3, 0.3, 1), f"Error: {str(e)}") - with imscope.tab_item("Workspace Layouts") as (exp, _): - if exp: - imgui.text("Experimental: Auto-switch layout by Tier") - ch, self.controller.ui_auto_switch_layout = imgui.checkbox("Enable Auto-Switch", self.controller.ui_auto_switch_layout) - if self.controller.ui_auto_switch_layout: - imgui.separator(); imgui.text("Tier Bindings (select profile for each tier)") - profiles = [""] + [p.name for p in self.controller.workspace_profiles.values()] - for t in ["Tier 1", "Tier 2", "Tier 3", "Tier 4"]: - curr = self.controller.ui_tier_layout_bindings.get(t, ""); idx = profiles.index(curr) if curr in profiles else 0 - ch_combo, new_idx = imgui.combo(t, idx, profiles) - if ch_combo: self.controller.ui_tier_layout_bindings[t] = profiles[new_idx] - def _render_prior_session_view(self) -> None: with imscope.style_color(imgui.Col_.child_bg, vec4(50, 40, 20)): if imgui.button("Exit Prior Session"): self.controller.cb_exit_prior_session(); self._comms_log_dirty = True @@ -2879,13 +3687,6 @@ class App: with theme.ai_text_style(): markdown_helper.render(content, context_id=f'prior_disc_{idx}') imgui.separator() - - def _render_project_settings_hub(self) -> None: - with imscope.tab_bar('context_hub_tabs'): - with imscope.tab_item('Projects') as (exp, _): - if exp: self._render_projects_panel() - with imscope.tab_item('Paths') as (exp, _): - if exp: self._render_paths_panel() def _render_thinking_indicator(self) -> None: is_thinking = self.ai_status in ['sending...', 'streaming...', 'running powershell...'] @@ -2942,6 +3743,736 @@ class App: self._handle_reset_session() if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_message_panel") + def _render_synthesis_panel(self) -> None: + """ + Renders a panel for synthesizing multiple discussion takes. + [C: tests/test_gui_synthesis.py:test_render_synthesis_panel] + """ + imgui.text("Select takes to synthesize:") + discussions = self.project.get('discussion', {}).get('discussions', {}) + if not hasattr(self, 'ui_synthesis_selected_takes'): + self.ui_synthesis_selected_takes = {name: False for name in discussions} + if not hasattr(self, 'ui_synthesis_prompt'): + self.ui_synthesis_prompt = "" + for name in discussions: + _, self.ui_synthesis_selected_takes[name] = imgui.checkbox(name, self.ui_synthesis_selected_takes.get(name, False)) + imgui.spacing() + imgui.text("Synthesis Prompt:") + _, self.ui_synthesis_prompt = imgui.input_text_multiline("##synthesis_prompt", self.ui_synthesis_prompt, imgui.ImVec2(-1, 100)) + if imgui.button("Generate Synthesis"): + selected = [name for name, sel in self.ui_synthesis_selected_takes.items() if sel] + if len(selected) > 1: + from src import synthesis_formatter + discussions_dict = self.project.get('discussion', {}).get('discussions', {}) + takes_dict = {name: discussions_dict.get(name, {}).get('history', []) for name in selected} + diff_text = synthesis_formatter.format_takes_diff(takes_dict) + prompt = f"{self.ui_synthesis_prompt}\n\nHere are the variations:\n{diff_text}" + + new_name = "synthesis_take" + counter = 1 + while new_name in discussions_dict: + new_name = f"synthesis_take_{counter}" + counter += 1 + + self._create_discussion(new_name) + with self._disc_entries_lock: self.disc_entries.append({"role": "User", "content": prompt, "collapsed": False, "ts": project_manager.now_ts()}) + self._handle_generate_send() + + def _render_snapshot_tab(self) -> None: + if imgui.begin_tab_bar("snapshot_tabs"): + if imgui.begin_tab_item("Aggregate MD")[0]: + display_md = self.last_aggregate_markdown + if self.ui_focus_agent: + tier_usage = self.mma_tier_usage.get(self.ui_focus_agent) + if tier_usage: + persona_name = tier_usage.get("persona") + if persona_name: + persona = self.controller.personas.get(persona_name) + if persona and persona.context_preset: + cp_name = persona.context_preset + if cp_name in self._focus_md_cache: + display_md = self._focus_md_cache[cp_name] + else: + flat = src.project_manager.flat_config(self.controller.project, self.active_discussion) + cp = self.controller.project.get('context_presets', {}).get(cp_name) + if cp: + flat["files"]["paths"] = cp.get("files", []) + flat["screenshots"]["paths"] = cp.get("screenshots", []) + full_md, _, _ = src.aggregate.run(flat) + self._focus_md_cache[cp_name] = full_md + display_md = full_md + if imgui.button("Copy"): + imgui.set_clipboard_text(display_md) + imgui.begin_child("last_agg_md", imgui.ImVec2(0, 0), True) + markdown_helper.render(display_md, context_id="snapshot_agg") + imgui.end_child() + imgui.end_tab_item() + if imgui.begin_tab_item("System Prompt")[0]: + if imgui.button("Copy"): + imgui.set_clipboard_text(self.last_resolved_system_prompt) + imgui.begin_child("last_sys_prompt", imgui.ImVec2(0, 0), True) + markdown_helper.render(self.last_resolved_system_prompt, context_id="snapshot_sys") + imgui.end_child() + imgui.end_tab_item() + imgui.end_tab_bar() + + def _render_response_panel(self) -> None: + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_response_panel") + if self._trigger_blink: + self._trigger_blink = False + self._is_blinking = True + self._blink_start_time = time.time() + try: + imgui.set_window_focus("Response") # type: ignore[call-arg] + except: + pass + is_blinking = False + blink_color = vec4(0, 0, 0, 0) + if self._is_blinking: + elapsed = time.time() - self._blink_start_time + if elapsed > 1.5: + self._is_blinking = False + else: + is_blinking = True + val = math.sin(elapsed * 8 * math.pi) + alpha = 50/255 if val > 0 else 0 + blink_color = vec4(0, 255, 0, alpha) + + with imscope.style_color(imgui.Col_.frame_bg, blink_color) if is_blinking else nullcontext(): + with imscope.style_color(imgui.Col_.child_bg, blink_color) if is_blinking else nullcontext(): + with imscope.child("response_scroll_area", imgui.ImVec2(0, -40), True): + with theme.ai_text_style(): + segments, parsed_response = thinking_parser.parse_thinking_trace(self.ai_response) + if segments: + self._render_thinking_trace([{"content": s.content, "marker": s.marker} for s in segments], 9999) + markdown_helper.render(parsed_response, context_id="response") + + imgui.separator() + if imgui.button("-> History"): + if self.ai_response: + segments, response = thinking_parser.parse_thinking_trace(self.ai_response) + entry = {"role": "AI", "content": response, "collapsed": True, "ts": project_manager.now_ts()} + if segments: + entry["thinking_segments"] = [{"content": s.content, "marker": s.marker} for s in segments] + self.disc_entries.append(entry) + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_response_panel") + +#endregion: Discussions + +#region: Operations Monitor + + def _render_operations_hub(self) -> None: + imgui.push_style_var(imgui.StyleVar_.item_spacing, imgui.ImVec2(10, 4)) + ch1, self.ui_separate_tool_calls_panel = imgui.checkbox("Pop Out Tool Calls", self.ui_separate_tool_calls_panel) + if ch1: self.show_windows["Tool Calls"] = self.ui_separate_tool_calls_panel + imgui.same_line() + ch2, self.ui_separate_usage_analytics = imgui.checkbox("Pop Out Usage Analytics", self.ui_separate_usage_analytics) + if ch2: self.show_windows["Usage Analytics"] = self.ui_separate_usage_analytics + imgui.same_line() + ch3, self.ui_separate_external_tools = imgui.checkbox('Pop Out External Tools', self.ui_separate_external_tools) + if ch3: self.show_windows['External Tools'] = self.ui_separate_external_tools + imgui.pop_style_var() + show_tc_tab, show_usage_tab = not self.ui_separate_tool_calls_panel, not self.ui_separate_usage_analytics + with imscope.tab_bar("ops_tabs"): + with imscope.tab_item("Comms History") as (exp, _): + if exp: self._render_comms_history_panel() + if show_tc_tab: + with imscope.tab_item("Tool Calls") as (exp, _): + if exp: self._render_tool_calls_panel() + if show_usage_tab: + with imscope.tab_item("Usage Analytics") as (exp, _): + if exp: self._render_usage_analytics_panel() + if not self.ui_separate_external_tools: + with imscope.tab_item("External Tools") as (exp, _): + if exp: + self._render_external_tools_panel() + imgui.separator(); imgui.text("") + try: self._render_external_editor_panel() + except Exception as e: imgui.text_colored(vec4(1, 0.3, 0.3, 1), f"Error: {str(e)}") + with imscope.tab_item("Workspace Layouts") as (exp, _): + if exp: + imgui.text("Experimental: Auto-switch layout by Tier") + ch, self.controller.ui_auto_switch_layout = imgui.checkbox("Enable Auto-Switch", self.controller.ui_auto_switch_layout) + if self.controller.ui_auto_switch_layout: + imgui.separator(); imgui.text("Tier Bindings (select profile for each tier)") + profiles = [""] + [p.name for p in self.controller.workspace_profiles.values()] + for t in ["Tier 1", "Tier 2", "Tier 3", "Tier 4"]: + curr = self.controller.ui_tier_layout_bindings.get(t, ""); idx = profiles.index(curr) if curr in profiles else 0 + ch_combo, new_idx = imgui.combo(t, idx, profiles) + if ch_combo: self.controller.ui_tier_layout_bindings[t] = profiles[new_idx] + + def _render_tool_calls_panel(self) -> None: + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_tool_calls_panel") + imgui.text("Tool call history") + imgui.same_line() + if imgui.button("Clear##tc"): + self._tool_log.clear() + self._tool_log_dirty = True + imgui.separator() + + log_to_render = self._tool_log_cache + flags = imgui.TableFlags_.resizable | imgui.TableFlags_.hideable | imgui.TableFlags_.borders_inner_v | imgui.TableFlags_.row_bg | imgui.TableFlags_.scroll_y + + if imgui.begin_table("tool_calls_table", 4, flags, imgui.ImVec2(0, 0)): + imgui.table_setup_column("#", imgui.TableColumnFlags_.width_fixed, 40) + imgui.table_setup_column("Tier", imgui.TableColumnFlags_.width_fixed, 60) + imgui.table_setup_column("Script", imgui.TableColumnFlags_.width_stretch) + imgui.table_setup_column("Result", imgui.TableColumnFlags_.width_fixed, 100) + + imgui.table_headers_row() + + clipper = imgui.ListClipper() + clipper.begin(len(log_to_render)) + while clipper.step(): + for i in range(clipper.display_start, clipper.display_end): + entry = log_to_render[i] + imgui.table_next_row() + + imgui.table_next_column() + imgui.text_colored(C_LBL, f"#{i+1}") + + imgui.table_next_column() + imgui.text_colored(C_SUB, f"[{entry.get('source_tier', 'main')}]") + + imgui.table_next_column() + script = entry.get("script", "") + res = entry.get("result", "") + # Use a clear, formatted combined view for the detail window + combined = f"**COMMAND:**\n```powershell\n{script}\n```\n\n---\n**OUTPUT:**\n```text\n{res}\n```" + + script_preview = script.replace("\n", " ")[:150] + if len(script) > 150: script_preview += "..." + self._render_selectable_label(f'tc_script_{i}', script_preview, width=-1) + if imgui.is_item_clicked(): + self.text_viewer_title = f"Tool Call #{i+1} Details" + self.text_viewer_content = combined + self.text_viewer_type = 'markdown' + self.show_text_viewer = True + + imgui.table_next_column() + res_preview = res.replace("\n", " ")[:30] + if len(res) > 30: res_preview += "..." + self._render_selectable_label(f'tc_res_{i}', res_preview, width=-1) + if imgui.is_item_clicked(): + self.text_viewer_title = f"Tool Call #{i+1} Details" + self.text_viewer_content = combined + self.text_viewer_type = 'markdown' + self.show_text_viewer = True + + imgui.end_table() + + if self._scroll_tool_calls_to_bottom: + imgui.set_scroll_here_y(1.0) + self._scroll_tool_calls_to_bottom = False + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_tool_calls_panel") + + def _render_comms_history_panel(self) -> None: + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_comms_history_panel") + st_col = vec4(200, 220, 160) + if theme.is_nerv_active(): + st_col = vec4(80, 255, 80) # DATA_GREEN for status in NERV + imgui.text_colored(st_col, f"Status: {self.ai_status}") + imgui.same_line() + if imgui.button("Clear##comms"): + ai_client.clear_comms_log() + self._comms_log.clear() + self._comms_log_dirty = True + if self.is_viewing_prior_session: + imgui.same_line() + if imgui.button("Exit Prior Session"): + self.controller.cb_exit_prior_session() + self._comms_log_dirty = True + imgui.separator() + + imgui.text_colored(C_OUT, "OUT"); imgui.same_line() + imgui.text_colored(C_REQ, "request"); imgui.same_line() + imgui.text_colored(C_TC, "tool_call"); imgui.same_line() + imgui.text(" "); imgui.same_line() + imgui.text_colored(C_IN, "IN"); imgui.same_line() + imgui.text_colored(C_RES, "response"); imgui.same_line() + imgui.text_colored(C_TR, "tool_result") + imgui.separator() + + # Use tinted background for prior session + if self.is_viewing_prior_session: imgui.push_style_color(imgui.Col_.child_bg, vec4(40, 30, 20)) + + imgui.begin_child("comms_scroll", imgui.ImVec2(0, 0), False, imgui.WindowFlags_.horizontal_scrollbar) + + log_to_render = self._comms_log_cache + + clipper = imgui.ListClipper() + clipper.begin(len(log_to_render)) + while clipper.step(): + for i in range(clipper.display_start, clipper.display_end): + entry = log_to_render[i] + imgui.push_id(f"comms_entry_{i}") + + i_display = i + 1 + ts = entry.get("ts", "00:00:00") + direction = entry.get("direction", "??") + kind = entry.get("kind", entry.get("type", "??")) + provider = entry.get("provider", "?") + model = entry.get("model", "?") + tier = entry.get("source_tier", "main") + payload = entry.get("payload", {}) + if not payload and kind not in ("request", "response", "tool_call", "tool_result"): + payload = entry # legacy + + # Row 1: #Idx TS DIR KIND Provider/Model [Tier] + imgui.text_colored(C_LBL, f"#{i_display}"); imgui.same_line() + imgui.text_colored(vec4(160, 160, 160), ts) + + latency = entry.get("latency") or entry.get("metadata", {}).get("latency") + if latency: + imgui.same_line() + imgui.text_colored(C_SUB, f" ({latency:.2f}s)") + + ticket_id = entry.get("mma_ticket_id") + if ticket_id: + imgui.same_line() + imgui.text_colored(vec4(255, 120, 120), f"[{ticket_id}]") + imgui.same_line() + d_col = DIR_COLORS.get(direction, C_VAL) + imgui.text_colored(d_col, direction); imgui.same_line() + k_col = KIND_COLORS.get(kind, C_VAL) + imgui.text_colored(k_col, kind); imgui.same_line() + imgui.text_colored(C_LBL, f"{provider}/{model}"); imgui.same_line() + imgui.text_colored(C_SUB, f"[{tier}]") + + # Optimized content rendering using _render_heavy_text logic + idx_str = str(i) + if kind == "request": + usage = payload.get("usage", {}) + if usage: + inp = usage.get("input_tokens", 0) + imgui.text_colored(C_LBL, f" tokens in:{inp}") + self._render_heavy_text("message", payload.get("message", ""), idx_str) + if payload.get("system"): + self._render_heavy_text("system", payload.get("system", ""), idx_str) + elif kind == "response": + r = payload.get("round", 0) + sr = payload.get("stop_reason", "STOP") + usage = payload.get("usage", {}) + usage_str = "" + if usage: + inp = usage.get("input_tokens", 0) + out = usage.get("output_tokens", 0) + cache = usage.get("cache_read_input_tokens", 0) + usage_str = f" in:{inp} out:{out}" + if cache: usage_str += f" cache:{cache}" + imgui.text_colored(C_LBL, f"round: {r} stop_reason: {sr}{usage_str}") + + text_content = payload.get("text", "") + segments, parsed_response = thinking_parser.parse_thinking_trace(text_content) + if segments: self._render_thinking_trace([{"content": s.content, "marker": s.marker} for s in segments], i, is_standalone=not bool(parsed_response.strip())) + if parsed_response: self._render_heavy_text("text", parsed_response, idx_str) + + tcs = payload.get("tool_calls", []) + if tcs: self._render_heavy_text("tool_calls", json.dumps(tcs, indent=1), idx_str) + + elif kind == "tool_call": self._render_heavy_text(payload.get("name", "call"), payload.get("script") or json.dumps(payload.get("args", {}), indent=1), idx_str) + elif kind == "tool_result": self._render_heavy_text(payload.get("name", "result"), payload.get("output", ""), idx_str) + else: self._render_heavy_text("data", str(payload), idx_str) + + imgui.separator() + imgui.pop_id() + + if self._scroll_comms_to_bottom: + imgui.set_scroll_here_y(1.0) + self._scroll_comms_to_bottom = False + + imgui.end_child() + if self.is_viewing_prior_session: + imgui.pop_style_color() + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_comms_history_panel") + +#endregion: Operations Monitor + +#region: Misc Tools + + def _render_text_viewer_window(self) -> None: + """Renders the standalone text/code/markdown viewer window.""" + if not self.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 - {self.text_viewer_title}", self.show_text_viewer) + self.show_text_viewer = bool(opened) + if not opened: + self.ui_editing_slices_file = None + self._slice_sel_start = -1 + self._slice_sel_end = -1 + if expanded: + if self.ui_editing_slices_file is not None: + imgui.text_colored(C_IN, "Slice Management (Click-drag lines to select range)") + if imgui.button("Add Selection as Slice"): + if self._slice_sel_start != -1 and self._slice_sel_end != -1: + s_line = min(self._slice_sel_start, self._slice_sel_end) + e_line = max(self._slice_sel_start, self._slice_sel_end) + from src.fuzzy_anchor import FuzzyAnchor + slice_data = FuzzyAnchor.create_slice(self.text_viewer_content, s_line, e_line) + slice_data['tag'] = ""; slice_data['comment'] = "" + self.ui_editing_slices_file.custom_slices.append(slice_data) + self._slice_sel_start = -1; self._slice_sel_end = -1 + imgui.same_line() + if imgui.button("Clear Selection"): self._slice_sel_start = -1; self._slice_sel_end = -1 + to_remove = -1 + for idx, slc in enumerate(self.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', '')) + if changed_comm: slc['comment'] = new_comm + imgui.same_line() + if imgui.button("Remove"): to_remove = idx + imgui.pop_id() + if to_remove != -1: self.ui_editing_slices_file.custom_slices.pop(to_remove) + imgui.separator() + if imgui.button("Copy"): imgui.set_clipboard_text(self.text_viewer_content) + imgui.same_line(); _, self.text_viewer_wrap = imgui.checkbox("Word Wrap", self.text_viewer_wrap) + imgui.separator() + renderer = markdown_helper.get_renderer(); tv_type = getattr(self, "text_viewer_type", "text") + if tv_type == 'markdown': + with imscope.child("tv_md_scroll", -1, -1, True): markdown_helper.render(self.text_viewer_content, context_id='text_viewer') + elif self.ui_editing_slices_file is not None: + with imscope.child("slice_editor_content", -1, -1, True): + lines = self.text_viewer_content.splitlines(); draw_list = imgui.get_window_draw_list() + for i, line_text in enumerate(lines): + line_num = i + 1; pos = imgui.get_cursor_screen_pos(); line_height = imgui.get_text_line_height() + is_sliced = any(slc['start_line'] <= line_num <= slc['end_line'] for slc in self.ui_editing_slices_file.custom_slices) + if is_sliced: 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, 165, 0, 0.2))) + if self._slice_sel_start != -1 and self._slice_sel_end != -1: + s, e = min(self._slice_sel_start, self._slice_sel_end), max(self._slice_sel_start, self._slice_sel_end) + if s <= line_num <= e: 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(100, 100, 255, 0.3))) + imgui.selectable(f"{line_num:4} | {line_text}##ln{line_num}", False) + if imgui.is_item_clicked(): self._slice_sel_start = line_num; self._slice_sel_end = line_num + if imgui.is_item_hovered() and imgui.is_mouse_down(0): self._slice_sel_end = line_num + elif tv_type in renderer._lang_map: + if self._text_viewer_editor is None: + self._text_viewer_editor = ced.TextEditor(); self._text_viewer_editor.set_read_only_enabled(True); self._text_viewer_editor.set_show_line_numbers_enabled(True) + try: + self._text_viewer_editor.set_text(self.text_viewer_content) + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_text_viewer_ced") + self._text_viewer_editor.render(f"##ced_{self.text_viewer_title}", imgui.ImVec2(-1, -1)) + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_text_viewer_ced") + except Exception as e: imgui.text_colored(vec4(255, 100, 100), f"CED Error: {e}"); imgui.text_unformatted(self.text_viewer_content) + else: + with imscope.child("tv_scroll", -1, -1, True): + if self.text_viewer_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) + imgui.text_unformatted(self.text_viewer_content) + if self.text_viewer_wrap: imgui.pop_text_wrap_pos() + imgui.end() + # Sync text and language + + #region: Inject File Modal + if getattr(self, "show_inject_modal", False): + imgui.open_popup("Inject File") + self.show_inject_modal = False + + if imgui.begin_popup_modal("Inject File", None, imgui.WindowFlags_.always_auto_resize)[0]: + files = self.project.get('files', {}).get('paths', []) + imgui.text("Select File to Inject:") + imgui.begin_child("inject_file_list", imgui.ImVec2(0, 200), True) + for f_path in files: + is_selected = (self._inject_file_path == f_path) + if imgui.selectable(f_path, is_selected)[0]: + self._inject_file_path = f_path + self.controller._update_inject_preview() + imgui.end_child() + imgui.separator() + if imgui.radio_button("Skeleton", self._inject_mode == "skeleton"): + self._inject_mode = "skeleton" + self.controller._update_inject_preview() + imgui.same_line() + if imgui.radio_button("Full", self._inject_mode == "full"): + self._inject_mode = "full" + self.controller._update_inject_preview() + imgui.separator() + imgui.text("Preview:") + imgui.begin_child("inject_preview_area", imgui.ImVec2(600, 300), True) + imgui.text_unformatted(self._inject_preview) + imgui.end_child() + imgui.separator() + if imgui.button("Inject", imgui.ImVec2(120, 0)): + formatted = f"## File: {self._inject_file_path}\n```python\n{self._inject_preview}\n```\n" + with self._disc_entries_lock: + self.disc_entries.append({ + "role": "Context", + "content": formatted, + "collapsed": True, + "ts": project_manager.now_ts() + }) + self._scroll_disc_to_bottom = True + imgui.close_current_popup() + imgui.same_line() + if imgui.button("Cancel", imgui.ImVec2(120, 0)): + imgui.close_current_popup() + imgui.end_popup() + #endregion: Inject File Modal + + self._render_ast_inspector_modal() + return + + def _render_base_prompt_diff_modal(self) -> None: + if not getattr(self.controller, "_show_base_prompt_diff_modal", False): + return + imgui.open_popup("Base Prompt Diff") + if imgui.begin_popup_modal("Base Prompt Diff", True, imgui.WindowFlags_.always_auto_resize)[0]: + imgui.text_colored(C_IN, "Difference between Default and Custom Base System Prompt") + imgui.separator() + + default_lines = ai_client._SYSTEM_PROMPT.splitlines(keepends=True) + custom_lines = self.ui_base_system_prompt.splitlines(keepends=True) + diff = list(difflib.unified_diff(default_lines, custom_lines, fromfile='Default', tofile='Custom')) + + if not diff: + imgui.text("No differences found.") + else: + imgui.begin_child("base_prompt_diff_scroll", imgui.ImVec2(800, 500), True) + for line in diff: + if line.startswith("+++") or line.startswith("---") or line.startswith("@@"): imgui.text_colored(vec4(77, 178, 255), line.rstrip()) + elif line.startswith("+"): imgui.text_colored(vec4(51, 230, 51), line.rstrip()) + elif line.startswith("-"): imgui.text_colored(vec4(230, 51, 51), line.rstrip()) + else: imgui.text(line.rstrip()) + imgui.end_child() + + imgui.separator() + if imgui.button("Close", imgui.ImVec2(120, 0)): + self.controller._show_base_prompt_diff_modal = False + imgui.close_current_popup() + imgui.end_popup() + + def _close_vscode_diff(self) -> None: + if hasattr(self, '_vscode_diff_process') and self._vscode_diff_process: + try: + self._vscode_diff_process.terminate() + except Exception: + pass + self._vscode_diff_process = None + + def _render_patch_modal(self) -> None: + if not self._show_patch_modal: + return + imgui.open_popup("Apply Patch?") + with imscope.popup_modal("Apply Patch?", True, imgui.WindowFlags_.always_auto_resize) as (opened, _): + if opened: + from src import shaders + p_min = imgui.get_window_pos() + p_max = imgui.ImVec2(p_min.x + imgui.get_window_size().x, p_min.y + imgui.get_window_size().y) + shaders.draw_soft_shadow(imgui.get_background_draw_list(), p_min, p_max, imgui.ImVec4(0, 0, 0, 0.6), 25.0, 6.0) + + imgui.text_colored(vec4(255, 230, 77), "Tier 4 QA Generated a Patch") + imgui.separator() + if self._pending_patch_files: + imgui.text("Files to modify:") + for f in self._pending_patch_files: + imgui.text(f" - {f}") + imgui.separator() + if self._patch_error_message: + imgui.text_colored(vec4(255, 77, 77), f"Error: {self._patch_error_message}") + imgui.separator() + imgui.text("Diff Preview:") + imgui.begin_child("patch_diff_scroll", imgui.ImVec2(-1, 280), True) + if self._pending_patch_text: + diff_lines = self._pending_patch_text.split("\n") + for line in diff_lines: + if line.startswith("+++") or line.startswith("---") or line.startswith("@@"): + imgui.text_colored(vec4(77, 178, 255), line) + elif line.startswith("+"): + imgui.text_colored(vec4(51, 230, 51), line) + elif line.startswith("-"): + imgui.text_colored(vec4(230, 51, 51), line) + else: + imgui.text(line) + imgui.end_child() + imgui.separator() + if imgui.button("Open in External Editor"): + self._open_patch_in_external_editor() + imgui.same_line() + if imgui.button("Apply Patch"): + self._apply_pending_patch() + self._close_vscode_diff() + imgui.same_line() + if imgui.button("Reject"): + self._close_vscode_diff() + self._show_patch_modal = False + self._pending_patch_text = None + self._pending_patch_files = [] + self._patch_error_message = None + imgui.close_current_popup() + + def _apply_pending_patch(self) -> None: + if not self._pending_patch_text: + self._patch_error_message = "No patch to apply" + return + try: + from src.diff_viewer import apply_patch_to_file + base_dir = str(self.controller.current_project_dir) if hasattr(self.controller, 'current_project_dir') else "." + success, msg = apply_patch_to_file(self._pending_patch_text, base_dir) + if success: + self._show_patch_modal = False + self._pending_patch_text = None + self._pending_patch_files = [] + self._patch_error_message = None + imgui.close_current_popup() + else: + self._patch_error_message = msg + except Exception as e: + self._patch_error_message = str(e) + + def _open_patch_in_external_editor(self) -> None: + self._external_editor_clicked = True + try: + from src.external_editor import get_default_launcher, create_temp_modified_file + import os + if not self._pending_patch_files: + self._patch_error_message = "No files to edit" + return + launcher = get_default_launcher() + editor = launcher.config.get_default() + if not editor: + self._patch_error_message = "No external editor configured" + return + original_path = self._pending_patch_files[0] + if not os.path.exists(original_path): + self._patch_error_message = f"Original file not found: {original_path}" + return + temp_path = create_temp_modified_file(self._pending_patch_text) + result = launcher.launch_diff(None, original_path, temp_path) + if result is None: + self._patch_error_message = "Failed to launch external editor" + else: + self._patch_error_message = None + self._vscode_diff_process = result + except Exception as e: + self._patch_error_message = str(e) + + def _render_external_editor_panel(self) -> None: + from src.external_editor import get_default_launcher + imgui.text("External Editor for Diff Viewing") + imgui.separator() + try: + launcher = get_default_launcher() + editors = launcher.config.editors + default_name = launcher.config.default_editor + if not editors: + imgui.text_colored(C_REQ, " No editors configured") + imgui.text("") + imgui.text("Add editors in config.toml:") + imgui.text(" [tools.text_editors.vscode]") + imgui.text(' path = "C:\\\\path\\\\to\\\\code.exe"') + imgui.text(' diff_args = ["--diff"]') + imgui.text("") + imgui.text(" [tools.text_editors.notepadpp]") + imgui.text(' path = "C:\\\\path\\\\to\\\\notepad++.exe"') + imgui.text(' diff_args = ["-multiInst", "-nosession"]') + imgui.text("") + imgui.text("Then set default in [tools.default_editor]") + else: + imgui.text("Default Editor:") + editor_names = sorted(list(editors.keys())) + if default_name and default_name in editor_names: + current_idx = editor_names.index(default_name) + else: + current_idx = 0 + changed, new_idx = imgui.combo("##editor_combo", current_idx, editor_names) + if changed: + self._set_external_editor_default(editor_names[new_idx]) + imgui.text("") + imgui.text("Configured Editors:") + imgui.separator() + for name in editor_names: + editor = editors.get(name) + if not editor: + continue + is_default = name == default_name + marker = " (default)" if is_default else "" + if is_default: + imgui.text_colored(C_IN, f" {name}{marker}") + else: + imgui.text(f" {name}{marker}") + imgui.text(f" {editor.path}") + if editor.diff_args: + imgui.textDisabled(f" diff: {editor.diff_args}") + imgui.text("") + imgui.text("Config: config.toml [tools.text_editors]") + imgui.text("Override: manual_slop.toml default_editor") + except Exception as e: + imgui.text_colored(C_TC, f"Error: {str(e)}") + + def _render_approve_script_modal(self) -> None: + """Renders the modal dialog for approving AI-generated PowerShell scripts.""" + with self._pending_dialog_lock: + dlg = self._pending_dialog + if dlg: + if not self._pending_dialog_open: + imgui.open_popup("Approve PowerShell Command") + self._pending_dialog_open = True + else: + self._pending_dialog_open = False + + if imgui.begin_popup_modal("Approve PowerShell Command", None, imgui.WindowFlags_.always_auto_resize)[0]: + if not dlg: + imgui.close_current_popup() + else: + imgui.text("The AI wants to run the following PowerShell script:") + imgui.text_colored(vec4(200, 200, 100), f"base_dir: {dlg._base_dir}") + imgui.separator() + # Checkbox to toggle full preview inside modal + _, self.show_text_viewer = imgui.checkbox("Show Full Preview", self.show_text_viewer) + if self.show_text_viewer: + imgui.begin_child("preview_child", imgui.ImVec2(600, 300), True) + imgui.text_unformatted(dlg._script) + imgui.end_child() + else: + ch, dlg._script = imgui.input_text_multiline("##confirm_script", dlg._script, imgui.ImVec2(-1, 200)) + imgui.separator() + if imgui.button("Approve & Run", imgui.ImVec2(120, 0)): + with dlg._condition: + dlg._approved = True + dlg._done = True + dlg._condition.notify_all() + with self._pending_dialog_lock: + self._pending_dialog = None + imgui.close_current_popup() + imgui.same_line() + if imgui.button("Reject", imgui.ImVec2(120, 0)): + with dlg._condition: + dlg._approved = False + dlg._done = True + dlg._condition.notify_all() + with self._pending_dialog_lock: + self._pending_dialog = None + imgui.close_current_popup() + imgui.end_popup() + +#endregion: Misc Tools + +#region: Sanity Tests + + def _render_markdown_test(self) -> None: + imgui.text("Markdown Test Panel") + imgui.separator() + md = """ +# Header 1 +## Header 2 +### Header 3 +This is **bold** text and *italic* text. +And ***bold italic*** text. + +* List item 1 +* List item 2 + * Sub-item + +[Link to Google](https://google.com) + +```python +def hello(): + print("Markdown works!") +``` +""" + markdown_helper.render(md) + +#endregion: Sanity Tests + #region: MMA def _reorder_ticket(self, src_idx: int, dst_idx: int) -> None: @@ -3638,464 +5169,6 @@ class App: imgui.close_current_popup() imgui.end_popup() -#enregion: MMA - -#region: Operations Monitor - - - -#endregion: Operations Monitor - -#region: Project Management - - def _render_projects_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_projects_panel") - proj_name = self.project.get("project", {}).get("name", Path(self.active_project_path).stem) - imgui.text_colored(C_IN, f"Active: {proj_name}") - imgui.separator() - imgui.text("Execution Mode") - modes = ["native", "beads"] - current_idx = modes.index(self.ui_project_execution_mode) if self.ui_project_execution_mode in modes else 0 - ch, new_idx = imgui.combo("##exec_mode", current_idx, modes) - if ch: - self.ui_project_execution_mode = modes[new_idx] - imgui.separator() - imgui.text("Git Directory") - ch, self.ui_project_git_dir = imgui.input_text("##git_dir", self.ui_project_git_dir) - imgui.same_line() - if imgui.button("Browse##git"): - r = hide_tk_root() - d = filedialog.askdirectory(title="Select Git Directory") - r.destroy() - if d: self.ui_project_git_dir = d - imgui.separator() - imgui.text("Output Dir") - ch, self.ui_output_dir = imgui.input_text("##out_dir", self.ui_output_dir) - imgui.same_line() - if imgui.button("Browse##out"): - r = hide_tk_root() - d = filedialog.askdirectory(title="Select Output Dir") - r.destroy() - if d: self.ui_output_dir = d - imgui.separator() - imgui.text("Conductor Directory") - ch, self.ui_project_conductor_dir = imgui.input_text("##cond_dir", self.ui_project_conductor_dir) - imgui.same_line() - if imgui.button("Browse##cond"): - r = hide_tk_root() - d = filedialog.askdirectory(title="Select Conductor Directory") - r.destroy() - if d: self.ui_project_conductor_dir = d - imgui.separator() - imgui.text("Project Files") - imgui.begin_child("proj_files", imgui.ImVec2(0, 150), True) - for i, pp in enumerate(self.project_paths): - is_active = (pp == self.active_project_path) - if imgui.button(f"x##p{i}"): - removed = self.project_paths.pop(i) - if removed == self.active_project_path and self.project_paths: - self._switch_project(self.project_paths[0]) - break - imgui.same_line() - marker = " *" if is_active else "" - if is_active: imgui.push_style_color(imgui.Col_.text, C_IN) - if imgui.button(f"{Path(pp).stem}{marker}##ps{i}"): - self._switch_project(pp) - if is_active: imgui.pop_style_color() - imgui.same_line() - imgui.text_colored(C_LBL, pp) - imgui.end_child() - if imgui.button("Add Project"): - r = hide_tk_root() - p = filedialog.askopenfilename( - title="Select Project .toml", - filetypes=[("TOML", "*.toml"), ("All", "*.*")], - ) - r.destroy() - if p and p not in self.project_paths: - self.project_paths.append(p) - imgui.same_line() - if imgui.button("New Project"): - r = hide_tk_root() - p = filedialog.asksaveasfilename(title="Create New Project .toml", defaultextension=".toml", filetypes=[("TOML", "*.toml"), ("All", "*.*")]) - r.destroy() - if p: - name = Path(p).stem - proj = project_manager.default_project(name) - project_manager.save_project(proj, p) - if p not in self.project_paths: - self.project_paths.append(p) - self._switch_project(p) - imgui.same_line() - if imgui.button("Save All"): - self._flush_to_project() - self._flush_to_config() - models.save_config(self.config) - self.ai_status = "config saved" - ch, self.ui_word_wrap = imgui.checkbox("Word-Wrap (Read-only panels)", self.ui_word_wrap) - ch, self.ui_auto_scroll_comms = imgui.checkbox("Auto-scroll Comms History", self.ui_auto_scroll_comms) - ch, self.ui_auto_scroll_tool_calls = imgui.checkbox("Auto-scroll Tool History", self.ui_auto_scroll_tool_calls) - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_projects_panel") - - def _render_paths_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_paths_panel") - path_info = paths.get_full_path_info() - - imgui.text_colored(C_IN, "System Path Configuration") - imgui.separator() - - def render_path_field(label: str, attr: str, key: str, tooltip: str): - info = path_info.get(key, {'source': 'unknown'}) - imgui.text(label) - if imgui.is_item_hovered(): imgui.set_tooltip(tooltip) - imgui.same_line() - imgui.text_disabled(f"(Source: {info['source']})") - - val = getattr(self, attr) - changed, new_val = imgui.input_text(f"##{key}", val) - if imgui.is_item_hovered(): imgui.set_tooltip(tooltip) - if changed: setattr(self, attr, new_val) - imgui.same_line() - if imgui.button(f"Browse##{key}"): - r = hide_tk_root() - d = filedialog.askdirectory(title=f"Select {label}") - r.destroy() - if d: setattr(self, attr, d) - -#endregion: Project Management - -#region: Misc Tools - - def _render_text_viewer_window(self) -> None: - """Renders the standalone text/code/markdown viewer window.""" - if not self.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 - {self.text_viewer_title}", self.show_text_viewer) - self.show_text_viewer = bool(opened) - if not opened: - self.ui_editing_slices_file = None - self._slice_sel_start = -1 - self._slice_sel_end = -1 - if expanded: - if self.ui_editing_slices_file is not None: - imgui.text_colored(C_IN, "Slice Management (Click-drag lines to select range)") - if imgui.button("Add Selection as Slice"): - if self._slice_sel_start != -1 and self._slice_sel_end != -1: - s_line = min(self._slice_sel_start, self._slice_sel_end) - e_line = max(self._slice_sel_start, self._slice_sel_end) - from src.fuzzy_anchor import FuzzyAnchor - slice_data = FuzzyAnchor.create_slice(self.text_viewer_content, s_line, e_line) - slice_data['tag'] = ""; slice_data['comment'] = "" - self.ui_editing_slices_file.custom_slices.append(slice_data) - self._slice_sel_start = -1; self._slice_sel_end = -1 - imgui.same_line() - if imgui.button("Clear Selection"): self._slice_sel_start = -1; self._slice_sel_end = -1 - to_remove = -1 - for idx, slc in enumerate(self.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', '')) - if changed_comm: slc['comment'] = new_comm - imgui.same_line() - if imgui.button("Remove"): to_remove = idx - imgui.pop_id() - if to_remove != -1: self.ui_editing_slices_file.custom_slices.pop(to_remove) - imgui.separator() - if imgui.button("Copy"): imgui.set_clipboard_text(self.text_viewer_content) - imgui.same_line(); _, self.text_viewer_wrap = imgui.checkbox("Word Wrap", self.text_viewer_wrap) - imgui.separator() - renderer = markdown_helper.get_renderer(); tv_type = getattr(self, "text_viewer_type", "text") - if tv_type == 'markdown': - with imscope.child("tv_md_scroll", -1, -1, True): markdown_helper.render(self.text_viewer_content, context_id='text_viewer') - elif self.ui_editing_slices_file is not None: - with imscope.child("slice_editor_content", -1, -1, True): - lines = self.text_viewer_content.splitlines(); draw_list = imgui.get_window_draw_list() - for i, line_text in enumerate(lines): - line_num = i + 1; pos = imgui.get_cursor_screen_pos(); line_height = imgui.get_text_line_height() - is_sliced = any(slc['start_line'] <= line_num <= slc['end_line'] for slc in self.ui_editing_slices_file.custom_slices) - if is_sliced: 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, 165, 0, 0.2))) - if self._slice_sel_start != -1 and self._slice_sel_end != -1: - s, e = min(self._slice_sel_start, self._slice_sel_end), max(self._slice_sel_start, self._slice_sel_end) - if s <= line_num <= e: 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(100, 100, 255, 0.3))) - imgui.selectable(f"{line_num:4} | {line_text}##ln{line_num}", False) - if imgui.is_item_clicked(): self._slice_sel_start = line_num; self._slice_sel_end = line_num - if imgui.is_item_hovered() and imgui.is_mouse_down(0): self._slice_sel_end = line_num - elif tv_type in renderer._lang_map: - if self._text_viewer_editor is None: - self._text_viewer_editor = ced.TextEditor(); self._text_viewer_editor.set_read_only_enabled(True); self._text_viewer_editor.set_show_line_numbers_enabled(True) - try: - self._text_viewer_editor.set_text(self.text_viewer_content) - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_text_viewer_ced") - self._text_viewer_editor.render(f"##ced_{self.text_viewer_title}", imgui.ImVec2(-1, -1)) - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_text_viewer_ced") - except Exception as e: imgui.text_colored(vec4(255, 100, 100), f"CED Error: {e}"); imgui.text_unformatted(self.text_viewer_content) - else: - with imscope.child("tv_scroll", -1, -1, True): - if self.text_viewer_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) - imgui.text_unformatted(self.text_viewer_content) - if self.text_viewer_wrap: imgui.pop_text_wrap_pos() - imgui.end() - # Sync text and language - - #region: Inject File Modal - if getattr(self, "show_inject_modal", False): - imgui.open_popup("Inject File") - self.show_inject_modal = False - - if imgui.begin_popup_modal("Inject File", None, imgui.WindowFlags_.always_auto_resize)[0]: - files = self.project.get('files', {}).get('paths', []) - imgui.text("Select File to Inject:") - imgui.begin_child("inject_file_list", imgui.ImVec2(0, 200), True) - for f_path in files: - is_selected = (self._inject_file_path == f_path) - if imgui.selectable(f_path, is_selected)[0]: - self._inject_file_path = f_path - self.controller._update_inject_preview() - imgui.end_child() - imgui.separator() - if imgui.radio_button("Skeleton", self._inject_mode == "skeleton"): - self._inject_mode = "skeleton" - self.controller._update_inject_preview() - imgui.same_line() - if imgui.radio_button("Full", self._inject_mode == "full"): - self._inject_mode = "full" - self.controller._update_inject_preview() - imgui.separator() - imgui.text("Preview:") - imgui.begin_child("inject_preview_area", imgui.ImVec2(600, 300), True) - imgui.text_unformatted(self._inject_preview) - imgui.end_child() - imgui.separator() - if imgui.button("Inject", imgui.ImVec2(120, 0)): - formatted = f"## File: {self._inject_file_path}\n```python\n{self._inject_preview}\n```\n" - with self._disc_entries_lock: - self.disc_entries.append({ - "role": "Context", - "content": formatted, - "collapsed": True, - "ts": project_manager.now_ts() - }) - self._scroll_disc_to_bottom = True - imgui.close_current_popup() - imgui.same_line() - if imgui.button("Cancel", imgui.ImVec2(120, 0)): - imgui.close_current_popup() - imgui.end_popup() - #endregion: Inject File Modal - - self._render_ast_inspector_modal() - return - - def _render_base_prompt_diff_modal(self) -> None: - if not getattr(self.controller, "_show_base_prompt_diff_modal", False): - return - imgui.open_popup("Base Prompt Diff") - if imgui.begin_popup_modal("Base Prompt Diff", True, imgui.WindowFlags_.always_auto_resize)[0]: - imgui.text_colored(C_IN, "Difference between Default and Custom Base System Prompt") - imgui.separator() - - default_lines = ai_client._SYSTEM_PROMPT.splitlines(keepends=True) - custom_lines = self.ui_base_system_prompt.splitlines(keepends=True) - diff = list(difflib.unified_diff(default_lines, custom_lines, fromfile='Default', tofile='Custom')) - - if not diff: - imgui.text("No differences found.") - else: - imgui.begin_child("base_prompt_diff_scroll", imgui.ImVec2(800, 500), True) - for line in diff: - if line.startswith("+++") or line.startswith("---") or line.startswith("@@"): imgui.text_colored(vec4(77, 178, 255), line.rstrip()) - elif line.startswith("+"): imgui.text_colored(vec4(51, 230, 51), line.rstrip()) - elif line.startswith("-"): imgui.text_colored(vec4(230, 51, 51), line.rstrip()) - else: imgui.text(line.rstrip()) - imgui.end_child() - - imgui.separator() - if imgui.button("Close", imgui.ImVec2(120, 0)): - self.controller._show_base_prompt_diff_modal = False - imgui.close_current_popup() - imgui.end_popup() - - def _close_vscode_diff(self) -> None: - if hasattr(self, '_vscode_diff_process') and self._vscode_diff_process: - try: - self._vscode_diff_process.terminate() - except Exception: - pass - self._vscode_diff_process = None - - def _render_patch_modal(self) -> None: - if not self._show_patch_modal: - return - imgui.open_popup("Apply Patch?") - with imscope.popup_modal("Apply Patch?", True, imgui.WindowFlags_.always_auto_resize) as (opened, _): - if opened: - from src import shaders - p_min = imgui.get_window_pos() - p_max = imgui.ImVec2(p_min.x + imgui.get_window_size().x, p_min.y + imgui.get_window_size().y) - shaders.draw_soft_shadow(imgui.get_background_draw_list(), p_min, p_max, imgui.ImVec4(0, 0, 0, 0.6), 25.0, 6.0) - - imgui.text_colored(vec4(255, 230, 77), "Tier 4 QA Generated a Patch") - imgui.separator() - if self._pending_patch_files: - imgui.text("Files to modify:") - for f in self._pending_patch_files: - imgui.text(f" - {f}") - imgui.separator() - if self._patch_error_message: - imgui.text_colored(vec4(255, 77, 77), f"Error: {self._patch_error_message}") - imgui.separator() - imgui.text("Diff Preview:") - imgui.begin_child("patch_diff_scroll", imgui.ImVec2(-1, 280), True) - if self._pending_patch_text: - diff_lines = self._pending_patch_text.split("\n") - for line in diff_lines: - if line.startswith("+++") or line.startswith("---") or line.startswith("@@"): - imgui.text_colored(vec4(77, 178, 255), line) - elif line.startswith("+"): - imgui.text_colored(vec4(51, 230, 51), line) - elif line.startswith("-"): - imgui.text_colored(vec4(230, 51, 51), line) - else: - imgui.text(line) - imgui.end_child() - imgui.separator() - if imgui.button("Open in External Editor"): - self._open_patch_in_external_editor() - imgui.same_line() - if imgui.button("Apply Patch"): - self._apply_pending_patch() - self._close_vscode_diff() - imgui.same_line() - if imgui.button("Reject"): - self._close_vscode_diff() - self._show_patch_modal = False - self._pending_patch_text = None - self._pending_patch_files = [] - self._patch_error_message = None - imgui.close_current_popup() - -#endregion: Misc Tools - -#region: Sanity Tests - - def _render_markdown_test(self) -> None: - imgui.text("Markdown Test Panel") - imgui.separator() - md = """ -# Header 1 -## Header 2 -### Header 3 -This is **bold** text and *italic* text. -And ***bold italic*** text. - -* List item 1 -* List item 2 - * Sub-item - -[Link to Google](https://google.com) - -```python -def hello(): - print("Markdown works!") -``` -""" - markdown_helper.render(md) - -#endregion: Sanity Tests - - def _render_save_preset_modal(self) -> None: - if not self._show_save_preset_modal: return - imgui.open_popup("Save Layout Preset") - with imscope.popup_modal("Save Layout Preset", True, imgui.WindowFlags_.always_auto_resize) as (opened, _): - if opened: - imgui.text("Preset Name:") - _, self._new_preset_name = imgui.input_text("##preset_name", self._new_preset_name) - if imgui.button("Save", imgui.ImVec2(120, 0)): - if self._new_preset_name.strip(): - ini_data = imgui.save_ini_settings_to_memory() - self.layout_presets[self._new_preset_name.strip()] = { - "ini": ini_data, - "multi_viewport": self.ui_multi_viewport - } - self.config["layout_presets"] = self.layout_presets - models.save_config(self.config) - self._show_save_preset_modal = False - self._new_preset_name = "" - imgui.close_current_popup() - imgui.same_line() - if imgui.button("Cancel", imgui.ImVec2(120, 0)): - self._show_save_preset_modal = False - imgui.close_current_popup() - - def _set_external_editor_default(self, editor_name: str) -> None: - from src import models - if "tools" not in self.config: self.config["tools"] = {} - if "default_editor" not in self.config["tools"]: self.config["tools"]["default_editor"] = {} - self.config["tools"]["default_editor"]["default_editor"] = editor_name - models.save_config(self.config) - self.ai_status = f"Default editor set to: {editor_name}" - - - render_path_field("Logs Directory", "ui_logs_dir", "logs_dir", "Directory where session JSON-L logs and artifacts are stored.") - render_path_field("Scripts Directory", "ui_scripts_dir", "scripts_dir", "Directory for AI-generated PowerShell scripts.") - - imgui.separator() - if imgui.button("Apply", imgui.ImVec2(120, 0)): - self._save_paths() - imgui.same_line() - if imgui.button("Reset", imgui.ImVec2(120, 0)): - self.init_state() - self.ai_status = "paths reset to defaults" - - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_paths_panel") - - def _apply_pending_patch(self) -> None: - if not self._pending_patch_text: - self._patch_error_message = "No patch to apply" - return - try: - from src.diff_viewer import apply_patch_to_file - base_dir = str(self.controller.current_project_dir) if hasattr(self.controller, 'current_project_dir') else "." - success, msg = apply_patch_to_file(self._pending_patch_text, base_dir) - if success: - self._show_patch_modal = False - self._pending_patch_text = None - self._pending_patch_files = [] - self._patch_error_message = None - imgui.close_current_popup() - else: - self._patch_error_message = msg - except Exception as e: - self._patch_error_message = str(e) - - def _open_patch_in_external_editor(self) -> None: - self._external_editor_clicked = True - try: - from src.external_editor import get_default_launcher, create_temp_modified_file - import os - if not self._pending_patch_files: - self._patch_error_message = "No files to edit" - return - launcher = get_default_launcher() - editor = launcher.config.get_default() - if not editor: - self._patch_error_message = "No external editor configured" - return - original_path = self._pending_patch_files[0] - if not os.path.exists(original_path): - self._patch_error_message = f"Original file not found: {original_path}" - return - temp_path = create_temp_modified_file(self._pending_patch_text) - result = launcher.launch_diff(None, original_path, temp_path) - if result is None: - self._patch_error_message = "Failed to launch external editor" - else: - self._patch_error_message = None - self._vscode_diff_process = result - except Exception as e: - self._patch_error_message = str(e) - def request_patch_from_tier4(self, error: str, file_context: str) -> None: try: from src import ai_client @@ -4112,771 +5185,6 @@ def hello(): except Exception as e: self._patch_error_message = str(e) - def _render_log_management(self) -> None: - """ - [C: tests/test_log_management_ui.py:test_render_log_management_logic] - """ - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_log_management") - with imscope.window("Log Management", self.show_windows["Log Management"]) as (exp, opened): - self.show_windows["Log Management"] = bool(opened) - if exp: - if self._log_registry is None: self._log_registry = log_registry.LogRegistry(str(paths.get_logs_dir() / "log_registry.toml")) - else: - if imgui.button("Refresh Registry"): self._log_registry = log_registry.LogRegistry(str(paths.get_logs_dir() / "log_registry.toml")) - imgui.same_line() - if imgui.button("Load Log"): self.cb_load_prior_log() - imgui.same_line() - if imgui.button("Force Prune Logs"): self.controller.event_queue.put("gui_task", {"action": "click", "item": "btn_prune_logs"}) - - registry = self._log_registry - sessions = registry.data - if imgui.begin_table("sessions_table", 7, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable): - imgui.table_setup_column("Session ID") - imgui.table_setup_column("Start Time") - imgui.table_setup_column("Star") - imgui.table_setup_column("Reason") - imgui.table_setup_column("Size (KB)") - imgui.table_setup_column("Msgs") - imgui.table_setup_column("Actions") - imgui.table_headers_row() - for session_id, s_data in sessions.items(): - imgui.table_next_row() - imgui.table_next_column() - imgui.text(session_id) - imgui.table_next_column() - imgui.text(s_data.get("start_time", "")) - imgui.table_next_column() - whitelisted = s_data.get("whitelisted", False) - if whitelisted: - imgui.text_colored(vec4(255, 215, 0), "YES") - else: - imgui.text("NO") - metadata = s_data.get("metadata") or {} - imgui.table_next_column() - imgui.text(metadata.get("reason", "")) - imgui.table_next_column() - imgui.text(str(metadata.get("size_kb", ""))) - imgui.table_next_column() - imgui.text(str(metadata.get("message_count", ""))) - imgui.table_next_column() - if imgui.button(f"Load##{session_id}"): - self.cb_load_prior_log(s_data.get("path")) - imgui.same_line() - if whitelisted: - if imgui.button(f"Unstar##{session_id}"): - registry.update_session_metadata( - session_id, - message_count=int(metadata.get("message_count") or 0), - errors=int(metadata.get("errors") or 0), - size_kb=int(metadata.get("size_kb") or 0), - whitelisted=False, - reason=str(metadata.get("reason") or "") - ) - else: - if imgui.button(f"Star##{session_id}"): - registry.update_session_metadata( - session_id, - message_count=int(metadata.get("message_count") or 0), - errors=int(metadata.get("errors") or 0), - size_kb=int(metadata.get("size_kb") or 0), - whitelisted=True, - reason="Manually whitelisted" - ) - imgui.end_table() - - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_log_management") - - def _update_context_file_stats(self) -> tuple[int, int]: - if not hasattr(self, '_file_stats_cache'): - self._file_stats_cache = {} - if not hasattr(self, '_file_stats_queue'): - self._file_stats_queue = [] - if not hasattr(self, '_file_stats_worker_active'): - self._file_stats_worker_active = False - - total_lines = 0 - total_ast = 0 - - missing_keys = [] - for f in self.context_files: - f_path = f.path if hasattr(f, "path") else str(f) - mtime = os.path.getmtime(f_path) if os.path.exists(f_path) else 0 - cache_key = f"{f_path}_{mtime}" - if cache_key not in self._file_stats_cache: - missing_keys.append((f_path, cache_key)) - else: - stats = self._file_stats_cache[cache_key] - total_lines += stats.get("lines", 0) - total_ast += stats.get("ast_elements", 0) - - if missing_keys and not self._file_stats_worker_active: - def _stats_worker(): - self._file_stats_worker_active = True - try: - for path, key in missing_keys[:10]: - self._file_stats_cache[key] = aggregate.compute_file_stats(path) - finally: - self._file_stats_worker_active = False - - threading.Thread(target=_stats_worker, daemon=True).start() - - return total_lines, total_ast - - def _render_context_batch_actions(self, total_lines: int, total_ast: int) -> None: - imgui.text("Batch:") - for mode in ["full", "summary", "skeleton", "outline", "masked", "none"]: - if imgui.button(f"{mode.capitalize()}##batch"): - 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.view_mode = mode - imgui.same_line() - if imgui.button("Sel All##selall"): - 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: - 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 = [] - 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.context_files = new_files - self.ui_selected_context_files.clear() - imgui.same_line() - imgui.text(f" | Total: {len(self.context_files)} files, {total_lines} lines, {total_ast} AST elements") - - def _render_context_files_table(self) -> None: - imgui.dummy(imgui.ImVec2(0, 4)) - grouped_files = aggregate.group_files_by_dir(self.context_files) - - with imscope.table("ctx_comp_table", 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders) as active: - if active: - imgui.table_setup_column("File", imgui.TableColumnFlags_.width_stretch) - imgui.table_setup_column("Flags", imgui.TableColumnFlags_.width_fixed, 200) - imgui.table_headers_row() - - file_indices = {id(f): idx for idx, f in enumerate(self.context_files)} - - for dir_name, g_files in grouped_files.items(): - imgui.table_next_row() - imgui.table_set_column_index(0) - with imscope.tree_node_ex(f"{dir_name}##dir_{dir_name}", imgui.TreeNodeFlags_.default_open) as is_open: - imgui.table_set_column_index(1) - if is_open: - for f_item in g_files: - i = file_indices[id(f_item)] - imgui.table_next_row() - imgui.table_set_column_index(0) - - f_path = f_item.path if hasattr(f_item, "path") else str(f_item) - is_sel = f_path in self.ui_selected_context_files - changed_sel, is_sel = imgui.checkbox(f"##sel{i}", is_sel) - if changed_sel: - if imgui.get_io().key_shift and self._last_selected_context_index != -1: - 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.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) - else: - self.ui_selected_context_files.discard(item_path) - else: - if is_sel: - self.ui_selected_context_files.add(f_path) - else: - self.ui_selected_context_files.discard(f_path) - self._last_selected_context_index = i - imgui.same_line() - - mtime = os.path.getmtime(f_path) if os.path.exists(f_path) else 0 - cache_key = f"{f_path}_{mtime}" - stats = self._file_stats_cache.get(cache_key, {"lines": 0, "ast_elements": 0}) - f_name = os.path.basename(f_path) - imgui.text(f"{f_name} (L: {stats.get('lines', 0)}, AST: {stats.get('ast_elements', 0)})") - - if f_path.lower().endswith(('.c', '.cpp', '.h', '.hpp', '.cxx', '.cc')): - imgui.same_line() - if imgui.button(f"[Inspect]##{i}"): - self.ui_inspecting_ast_file = f_item - self._show_ast_inspector = True - - imgui.same_line() - if imgui.button(f"[Slices]##{i}"): - self.ui_editing_slices_file = f_item - f_path = f_item.path if hasattr(f_item, "path") else str(f_item) - self.text_viewer_title = f"Slices: {f_path}" - try: - self.text_viewer_content = mcp_client.read_file(f_path) - except Exception as e: - self.text_viewer_content = f"Error reading file: {e}" - self.text_viewer_type = 'cpp' if f_path.endswith(('.cpp', '.hpp', '.h')) else 'python' if f_path.endswith('.py') else 'text' - self.show_text_viewer = True - - imgui.table_set_column_index(1) - if not hasattr(f_item, "view_mode"): - f_item.view_mode = "summary" - view_modes = ["full", "summary", "skeleton", "outline", "masked", "none"] - try: - current_idx = view_modes.index(f_item.view_mode) - except ValueError: - current_idx = 1 - f_item.view_mode = "summary" - imgui.set_next_item_width(120) - changed_vm, new_idx = imgui.combo(f"##vm{i}", current_idx, view_modes) - if changed_vm: - f_item.view_mode = view_modes[new_idx] - - imgui.same_line() - if imgui.button(f"[Save]##vpsave{i}"): - imgui.open_popup(f"save_vp_popup{i}") - - if imgui.begin_popup(f"save_vp_popup{i}"): - imgui.text("Preset Name:") - changed_pname, self.ui_new_vp_name = imgui.input_text(f"##pname{i}", self.ui_new_vp_name) - if imgui.button("OK"): - if self.ui_new_vp_name.strip(): - self.controller._cb_save_view_preset(self.ui_new_vp_name.strip(), f_item) - self.ui_new_vp_name = "" - imgui.close_current_popup() - imgui.end_popup() - - imgui.same_line() - if imgui.button(f"[Load]##vpload{i}"): - imgui.open_popup(f"load_vp_popup{i}") - - if imgui.begin_popup(f"load_vp_popup{i}"): - vp_names = sorted([vp.name for vp in self.controller.view_presets]) - if not vp_names: - imgui.text("No presets saved.") - for vp_name in vp_names: - if imgui.selectable(vp_name): - self.controller._cb_apply_view_preset(vp_name, f_item) - imgui.close_current_popup() - imgui.end_popup() - if hasattr(f_item, "custom_slices") and f_item.custom_slices: - imgui.same_line() - imgui.text_colored(imgui.ImVec4(1.0, 0.5, 0.0, 1.0), "[Slices Active]") - - def _render_context_presets(self) -> None: - imgui.text("Presets") - presets = self.controller.project.get('context_presets', {}) - preset_names = [""] + sorted(presets.keys()) - active = getattr(self, "ui_active_context_preset", "") - if active not in preset_names: - active = "" - try: - idx = preset_names.index(active) - except ValueError: - idx = 0 - ch, new_idx = imgui.combo("##ctx_preset", idx, preset_names) - if ch: - self.ui_active_context_preset = preset_names[new_idx] - if preset_names[new_idx]: - self.load_context_preset(preset_names[new_idx]) - imgui.same_line() - changed, new_name = imgui.input_text("##new_preset", getattr(self, "ui_new_context_preset_name", "")) - if changed: - self.ui_new_context_preset_name = new_name - imgui.same_line() - if imgui.button("Save##ctx"): - if getattr(self, "ui_new_context_preset_name", "").strip(): - self.save_context_preset(self.ui_new_context_preset_name.strip()) - self.ui_new_context_preset_name = "" - imgui.same_line() - if imgui.button("Delete##ctx"): - if getattr(self, "ui_active_context_preset", ""): - self.delete_context_preset(self.ui_active_context_preset) - self.ui_active_context_preset = "" - - def _render_snapshot_tab(self) -> None: - if imgui.begin_tab_bar("snapshot_tabs"): - if imgui.begin_tab_item("Aggregate MD")[0]: - display_md = self.last_aggregate_markdown - if self.ui_focus_agent: - tier_usage = self.mma_tier_usage.get(self.ui_focus_agent) - if tier_usage: - persona_name = tier_usage.get("persona") - if persona_name: - persona = self.controller.personas.get(persona_name) - if persona and persona.context_preset: - cp_name = persona.context_preset - if cp_name in self._focus_md_cache: - display_md = self._focus_md_cache[cp_name] - else: - flat = src.project_manager.flat_config(self.controller.project, self.active_discussion) - cp = self.controller.project.get('context_presets', {}).get(cp_name) - if cp: - flat["files"]["paths"] = cp.get("files", []) - flat["screenshots"]["paths"] = cp.get("screenshots", []) - full_md, _, _ = src.aggregate.run(flat) - self._focus_md_cache[cp_name] = full_md - display_md = full_md - if imgui.button("Copy"): - imgui.set_clipboard_text(display_md) - imgui.begin_child("last_agg_md", imgui.ImVec2(0, 0), True) - markdown_helper.render(display_md, context_id="snapshot_agg") - imgui.end_child() - imgui.end_tab_item() - if imgui.begin_tab_item("System Prompt")[0]: - if imgui.button("Copy"): - imgui.set_clipboard_text(self.last_resolved_system_prompt) - imgui.begin_child("last_sys_prompt", imgui.ImVec2(0, 0), True) - markdown_helper.render(self.last_resolved_system_prompt, context_id="snapshot_sys") - imgui.end_child() - imgui.end_tab_item() - imgui.end_tab_bar() - - def _render_screenshots_panel(self, height_override: float = 0) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_screenshots_panel") - imgui.text("Paths"); imgui.same_line(); imgui.text("| Base Dir:"); imgui.same_line(); - imgui.set_next_item_width(-100) - ch, self.ui_shots_base_dir = imgui.input_text("##s_base", self.ui_shots_base_dir) - imgui.same_line() - if imgui.button("Browse##sb"): - r = hide_tk_root(); d = filedialog.askdirectory(); r.destroy() - if d: self.ui_shots_base_dir = d - imgui.separator() - # Calculate content-based height: use override if provided, else content-based - if height_override > 0: shot_h = height_override - else: - shot_count = max(len(self.screenshots), 1) - shot_h = min(shot_count * 28 + 40, 200) - # BEGIN s_paths child window - imgui.begin_child("s_paths", imgui.ImVec2(0, shot_h), True) - for i, s in enumerate(self.screenshots): - if imgui.button(f"x##s{i}"): - self.screenshots.pop(i) - break - imgui.same_line(); imgui.text(s) - imgui.end_child() - if imgui.button("Add Screenshot(s)"): - r = hide_tk_root() - paths = filedialog.askopenfilenames( - title="Select Screenshots", - filetypes=[("Images", "*.png *.jpg *.jpeg *.gif *.bmp *.webp"), ("All", "*.*")], - ) - r.destroy() - for p in paths: - if p not in self.screenshots: self.screenshots.append(p) - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_screenshots_panel") - - def _render_synthesis_panel(self) -> None: - """ - Renders a panel for synthesizing multiple discussion takes. - [C: tests/test_gui_synthesis.py:test_render_synthesis_panel] - """ - imgui.text("Select takes to synthesize:") - discussions = self.project.get('discussion', {}).get('discussions', {}) - if not hasattr(self, 'ui_synthesis_selected_takes'): - self.ui_synthesis_selected_takes = {name: False for name in discussions} - if not hasattr(self, 'ui_synthesis_prompt'): - self.ui_synthesis_prompt = "" - for name in discussions: - _, self.ui_synthesis_selected_takes[name] = imgui.checkbox(name, self.ui_synthesis_selected_takes.get(name, False)) - imgui.spacing() - imgui.text("Synthesis Prompt:") - _, self.ui_synthesis_prompt = imgui.input_text_multiline("##synthesis_prompt", self.ui_synthesis_prompt, imgui.ImVec2(-1, 100)) - if imgui.button("Generate Synthesis"): - selected = [name for name, sel in self.ui_synthesis_selected_takes.items() if sel] - if len(selected) > 1: - from src import synthesis_formatter - discussions_dict = self.project.get('discussion', {}).get('discussions', {}) - takes_dict = {name: discussions_dict.get(name, {}).get('history', []) for name in selected} - diff_text = synthesis_formatter.format_takes_diff(takes_dict) - prompt = f"{self.ui_synthesis_prompt}\n\nHere are the variations:\n{diff_text}" - - new_name = "synthesis_take" - counter = 1 - while new_name in discussions_dict: - new_name = f"synthesis_take_{counter}" - counter += 1 - - self._create_discussion(new_name) - with self._disc_entries_lock: self.disc_entries.append({"role": "User", "content": prompt, "collapsed": False, "ts": project_manager.now_ts()}) - self._handle_generate_send() - - def _render_persona_selector_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_persona_selector_panel") - imgui.text("Persona") - if not hasattr(self, 'ui_active_persona'): - self.ui_active_persona = "" - personas = getattr(self.controller, 'personas', {}) - if imgui.begin_combo("##persona", self.ui_active_persona or "None"): - if imgui.selectable("None", not self.ui_active_persona)[0]: - self.ui_active_persona = "" - for pname in sorted(personas.keys()): - if not pname: - continue - if imgui.selectable(pname, pname == self.ui_active_persona)[0]: - self.ui_active_persona = pname - if pname in personas: - persona = personas[pname] - self._editing_persona_name = persona.name - self._editing_persona_system_prompt = persona.system_prompt or "" - self._editing_persona_tool_preset_id = persona.tool_preset or "" - self._editing_persona_bias_profile_id = persona.bias_profile or "" - self._editing_persona_context_preset_id = getattr(persona, 'context_preset', '') or "" - self._editing_persona_aggregation_strategy = getattr(persona, 'aggregation_strategy', '') or "" - import copy - self._editing_persona_preferred_models_list = copy.deepcopy(persona.preferred_models) if persona.preferred_models else [] - self._editing_persona_is_new = False - - # Apply persona to current state immediately - if persona.preferred_models and len(persona.preferred_models) > 0: - first_model = persona.preferred_models[0] - if first_model.get("provider"): - self.current_provider = first_model.get("provider") - if first_model.get("model"): - self.current_model = first_model.get("model") - if first_model.get("temperature") is not None: - ai_client.temperature = first_model.get("temperature") - self.temperature = first_model.get("temperature") - if first_model.get("max_output_tokens"): - ai_client.max_output_tokens = first_model.get("max_output_tokens") - self.max_tokens = first_model.get("max_output_tokens") - if first_model.get("history_trunc_limit"): - self.history_trunc_limit = first_model.get("history_trunc_limit") - - if persona.system_prompt: - self.ui_project_system_prompt = persona.system_prompt - if persona.tool_preset: - self.ui_active_tool_preset = persona.tool_preset - ai_client.set_tool_preset(persona.tool_preset) - if persona.bias_profile: - self.ui_active_bias_profile = persona.bias_profile - ai_client.set_bias_profile(persona.bias_profile) - if getattr(persona, 'context_preset', None): - self.ui_active_context_preset = persona.context_preset - self.load_context_preset(persona.context_preset) - imgui.end_combo() - imgui.same_line() - if imgui.button("Manage Personas"): - self.show_persona_editor_window = True - if self.ui_active_persona and self.ui_active_persona in personas: - persona = personas[self.ui_active_persona] - self._editing_persona_name = persona.name - self._editing_persona_system_prompt = persona.system_prompt or "" - self._editing_persona_tool_preset_id = persona.tool_preset or "" - self._editing_persona_bias_profile_id = persona.bias_profile or "" - self._editing_persona_context_preset_id = getattr(persona, 'context_preset', '') or "" - self._editing_persona_aggregation_strategy = getattr(persona, 'aggregation_strategy', '') or "" - import copy - self._editing_persona_preferred_models_list = copy.deepcopy(persona.preferred_models) if persona.preferred_models else [] - self._editing_persona_scope = self.controller.persona_manager.get_persona_scope(persona.name) - self._editing_persona_is_new = False - else: - self._editing_persona_name = "" - self._editing_persona_system_prompt = "" - self._editing_persona_tool_preset_id = "" - self._editing_persona_bias_profile_id = "" - self._editing_persona_context_preset_id = "" - self._editing_persona_aggregation_strategy = "" - self._editing_persona_preferred_models_list = [{ - "provider": self.current_provider, - "model": self.current_model, - "temperature": getattr(self, "temperature", 0.7), - "max_output_tokens": getattr(self, "max_tokens", 4096), - "history_trunc_limit": getattr(self, "history_trunc_limit", 900000) - }] - self._editing_persona_scope = "project" - self._editing_persona_is_new = True - imgui.separator() - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_persona_selector_panel") - - def _render_response_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_response_panel") - if self._trigger_blink: - self._trigger_blink = False - self._is_blinking = True - self._blink_start_time = time.time() - try: - imgui.set_window_focus("Response") # type: ignore[call-arg] - except: - pass - is_blinking = False - blink_color = vec4(0, 0, 0, 0) - if self._is_blinking: - elapsed = time.time() - self._blink_start_time - if elapsed > 1.5: - self._is_blinking = False - else: - is_blinking = True - val = math.sin(elapsed * 8 * math.pi) - alpha = 50/255 if val > 0 else 0 - blink_color = vec4(0, 255, 0, alpha) - - with imscope.style_color(imgui.Col_.frame_bg, blink_color) if is_blinking else nullcontext(): - with imscope.style_color(imgui.Col_.child_bg, blink_color) if is_blinking else nullcontext(): - with imscope.child("response_scroll_area", imgui.ImVec2(0, -40), True): - with theme.ai_text_style(): - segments, parsed_response = thinking_parser.parse_thinking_trace(self.ai_response) - if segments: - self._render_thinking_trace([{"content": s.content, "marker": s.marker} for s in segments], 9999) - markdown_helper.render(parsed_response, context_id="response") - - imgui.separator() - if imgui.button("-> History"): - if self.ai_response: - segments, response = thinking_parser.parse_thinking_trace(self.ai_response) - entry = {"role": "AI", "content": response, "collapsed": True, "ts": project_manager.now_ts()} - if segments: - entry["thinking_segments"] = [{"content": s.content, "marker": s.marker} for s in segments] - self.disc_entries.append(entry) - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_response_panel") - - def _render_external_tools_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_external_tools_panel") - if imgui.button("Refresh External MCPs"): - self.event_queue.put("refresh_external_mcps", None) - - imgui.separator() - - # Server status indicators - manager = mcp_client.get_external_mcp_manager() - statuses = manager.get_servers_status() - if statuses: - imgui.text("Servers:") - for sname, status in statuses.items(): - imgui.same_line() - # Green for running, Yellow for starting, Red for error, Gray for idle - col = (0.5, 0.5, 0.5, 1.0) - if status == 'running': - col = (0.0, 1.0, 0.0, 1.0) - elif status == 'starting': - col = (1.0, 1.0, 0.0, 1.0) - elif status == 'error': - col = (1.0, 0.0, 0.0, 1.0) - imgui.color_button(f"##status_{sname}", col) - imgui.same_line() - imgui.text(sname) - imgui.separator() - - tools = manager.get_all_tools() - if not tools: - imgui.text_disabled("No external tools found.") - else: - if imgui.begin_table("external_tools_table", 3, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable): - imgui.table_setup_column("Name") - imgui.table_setup_column("Server") - imgui.table_setup_column("Description") - imgui.table_headers_row() - - for tname, tinfo in tools.items(): - imgui.table_next_row() - imgui.table_next_column() - imgui.text(tname) - imgui.table_next_column() - imgui.text(tinfo.get('server', 'unknown')) - imgui.table_next_column() - imgui.text(tinfo.get('description', '')) - imgui.end_table() - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_external_tools_panel") - - def _render_comms_history_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_comms_history_panel") - st_col = vec4(200, 220, 160) - if theme.is_nerv_active(): - st_col = vec4(80, 255, 80) # DATA_GREEN for status in NERV - imgui.text_colored(st_col, f"Status: {self.ai_status}") - imgui.same_line() - if imgui.button("Clear##comms"): - ai_client.clear_comms_log() - self._comms_log.clear() - self._comms_log_dirty = True - if self.is_viewing_prior_session: - imgui.same_line() - if imgui.button("Exit Prior Session"): - self.controller.cb_exit_prior_session() - self._comms_log_dirty = True - imgui.separator() - - imgui.text_colored(C_OUT, "OUT"); imgui.same_line() - imgui.text_colored(C_REQ, "request"); imgui.same_line() - imgui.text_colored(C_TC, "tool_call"); imgui.same_line() - imgui.text(" "); imgui.same_line() - imgui.text_colored(C_IN, "IN"); imgui.same_line() - imgui.text_colored(C_RES, "response"); imgui.same_line() - imgui.text_colored(C_TR, "tool_result") - imgui.separator() - - # Use tinted background for prior session - if self.is_viewing_prior_session: imgui.push_style_color(imgui.Col_.child_bg, vec4(40, 30, 20)) - - imgui.begin_child("comms_scroll", imgui.ImVec2(0, 0), False, imgui.WindowFlags_.horizontal_scrollbar) - - log_to_render = self._comms_log_cache - - clipper = imgui.ListClipper() - clipper.begin(len(log_to_render)) - while clipper.step(): - for i in range(clipper.display_start, clipper.display_end): - entry = log_to_render[i] - imgui.push_id(f"comms_entry_{i}") - - i_display = i + 1 - ts = entry.get("ts", "00:00:00") - direction = entry.get("direction", "??") - kind = entry.get("kind", entry.get("type", "??")) - provider = entry.get("provider", "?") - model = entry.get("model", "?") - tier = entry.get("source_tier", "main") - payload = entry.get("payload", {}) - if not payload and kind not in ("request", "response", "tool_call", "tool_result"): - payload = entry # legacy - - # Row 1: #Idx TS DIR KIND Provider/Model [Tier] - imgui.text_colored(C_LBL, f"#{i_display}"); imgui.same_line() - imgui.text_colored(vec4(160, 160, 160), ts) - - latency = entry.get("latency") or entry.get("metadata", {}).get("latency") - if latency: - imgui.same_line() - imgui.text_colored(C_SUB, f" ({latency:.2f}s)") - - ticket_id = entry.get("mma_ticket_id") - if ticket_id: - imgui.same_line() - imgui.text_colored(vec4(255, 120, 120), f"[{ticket_id}]") - imgui.same_line() - d_col = DIR_COLORS.get(direction, C_VAL) - imgui.text_colored(d_col, direction); imgui.same_line() - k_col = KIND_COLORS.get(kind, C_VAL) - imgui.text_colored(k_col, kind); imgui.same_line() - imgui.text_colored(C_LBL, f"{provider}/{model}"); imgui.same_line() - imgui.text_colored(C_SUB, f"[{tier}]") - - # Optimized content rendering using _render_heavy_text logic - idx_str = str(i) - if kind == "request": - usage = payload.get("usage", {}) - if usage: - inp = usage.get("input_tokens", 0) - imgui.text_colored(C_LBL, f" tokens in:{inp}") - self._render_heavy_text("message", payload.get("message", ""), idx_str) - if payload.get("system"): - self._render_heavy_text("system", payload.get("system", ""), idx_str) - elif kind == "response": - r = payload.get("round", 0) - sr = payload.get("stop_reason", "STOP") - usage = payload.get("usage", {}) - usage_str = "" - if usage: - inp = usage.get("input_tokens", 0) - out = usage.get("output_tokens", 0) - cache = usage.get("cache_read_input_tokens", 0) - usage_str = f" in:{inp} out:{out}" - if cache: usage_str += f" cache:{cache}" - imgui.text_colored(C_LBL, f"round: {r} stop_reason: {sr}{usage_str}") - - text_content = payload.get("text", "") - segments, parsed_response = thinking_parser.parse_thinking_trace(text_content) - if segments: self._render_thinking_trace([{"content": s.content, "marker": s.marker} for s in segments], i, is_standalone=not bool(parsed_response.strip())) - if parsed_response: self._render_heavy_text("text", parsed_response, idx_str) - - tcs = payload.get("tool_calls", []) - if tcs: self._render_heavy_text("tool_calls", json.dumps(tcs, indent=1), idx_str) - - elif kind == "tool_call": self._render_heavy_text(payload.get("name", "call"), payload.get("script") or json.dumps(payload.get("args", {}), indent=1), idx_str) - elif kind == "tool_result": self._render_heavy_text(payload.get("name", "result"), payload.get("output", ""), idx_str) - else: self._render_heavy_text("data", str(payload), idx_str) - - imgui.separator() - imgui.pop_id() - - if self._scroll_comms_to_bottom: - imgui.set_scroll_here_y(1.0) - self._scroll_comms_to_bottom = False - - imgui.end_child() - if self.is_viewing_prior_session: - imgui.pop_style_color() - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_comms_history_panel") - - def _render_tool_calls_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_tool_calls_panel") - imgui.text("Tool call history") - imgui.same_line() - if imgui.button("Clear##tc"): - self._tool_log.clear() - self._tool_log_dirty = True - imgui.separator() - - log_to_render = self._tool_log_cache - flags = imgui.TableFlags_.resizable | imgui.TableFlags_.hideable | imgui.TableFlags_.borders_inner_v | imgui.TableFlags_.row_bg | imgui.TableFlags_.scroll_y - - if imgui.begin_table("tool_calls_table", 4, flags, imgui.ImVec2(0, 0)): - imgui.table_setup_column("#", imgui.TableColumnFlags_.width_fixed, 40) - imgui.table_setup_column("Tier", imgui.TableColumnFlags_.width_fixed, 60) - imgui.table_setup_column("Script", imgui.TableColumnFlags_.width_stretch) - imgui.table_setup_column("Result", imgui.TableColumnFlags_.width_fixed, 100) - - imgui.table_headers_row() - - clipper = imgui.ListClipper() - clipper.begin(len(log_to_render)) - while clipper.step(): - for i in range(clipper.display_start, clipper.display_end): - entry = log_to_render[i] - imgui.table_next_row() - - imgui.table_next_column() - imgui.text_colored(C_LBL, f"#{i+1}") - - imgui.table_next_column() - imgui.text_colored(C_SUB, f"[{entry.get('source_tier', 'main')}]") - - imgui.table_next_column() - script = entry.get("script", "") - res = entry.get("result", "") - # Use a clear, formatted combined view for the detail window - combined = f"**COMMAND:**\n```powershell\n{script}\n```\n\n---\n**OUTPUT:**\n```text\n{res}\n```" - - script_preview = script.replace("\n", " ")[:150] - if len(script) > 150: script_preview += "..." - self._render_selectable_label(f'tc_script_{i}', script_preview, width=-1) - if imgui.is_item_clicked(): - self.text_viewer_title = f"Tool Call #{i+1} Details" - self.text_viewer_content = combined - self.text_viewer_type = 'markdown' - self.show_text_viewer = True - - imgui.table_next_column() - res_preview = res.replace("\n", " ")[:30] - if len(res) > 30: res_preview += "..." - self._render_selectable_label(f'tc_res_{i}', res_preview, width=-1) - if imgui.is_item_clicked(): - self.text_viewer_title = f"Tool Call #{i+1} Details" - self.text_viewer_content = combined - self.text_viewer_type = 'markdown' - self.show_text_viewer = True - - imgui.end_table() - - if self._scroll_tool_calls_to_bottom: - imgui.set_scroll_here_y(1.0) - self._scroll_tool_calls_to_bottom = False - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_tool_calls_panel") - def bulk_execute(self) -> None: """ [C: tests/test_ticket_queue.py:TestBulkOperations.test_bulk_execute] @@ -4949,313 +5257,7 @@ def hello(): changed = True self._push_mma_state_update() - def cb_load_prior_log(self, path: Optional[str] = None) -> None: - if path is None: - root = hide_tk_root() - path = filedialog.askdirectory(title='Select Session Directory', initialdir=str(paths.get_logs_dir())) - root.destroy() - if path: - self.controller.cb_load_prior_log(path) - - def _save_paths(self): - """ - [C: tests/test_gui_paths.py:test_save_paths] - """ - self.config["paths"] = { - "logs_dir": self.ui_logs_dir, - "scripts_dir": self.ui_scripts_dir - } - cfg_path = paths.get_config_path() - if cfg_path.exists(): - shutil.copy(cfg_path, str(cfg_path) + ".bak") - models.save_config(self.config) - paths.reset_resolved() - self.init_state() - self.ai_status = 'paths applied and session reset' - - def _render_external_editor_panel(self) -> None: - from src.external_editor import get_default_launcher - imgui.text("External Editor for Diff Viewing") - imgui.separator() - try: - launcher = get_default_launcher() - editors = launcher.config.editors - default_name = launcher.config.default_editor - if not editors: - imgui.text_colored(C_REQ, " No editors configured") - imgui.text("") - imgui.text("Add editors in config.toml:") - imgui.text(" [tools.text_editors.vscode]") - imgui.text(' path = "C:\\\\path\\\\to\\\\code.exe"') - imgui.text(' diff_args = ["--diff"]') - imgui.text("") - imgui.text(" [tools.text_editors.notepadpp]") - imgui.text(' path = "C:\\\\path\\\\to\\\\notepad++.exe"') - imgui.text(' diff_args = ["-multiInst", "-nosession"]') - imgui.text("") - imgui.text("Then set default in [tools.default_editor]") - else: - imgui.text("Default Editor:") - editor_names = sorted(list(editors.keys())) - if default_name and default_name in editor_names: - current_idx = editor_names.index(default_name) - else: - current_idx = 0 - changed, new_idx = imgui.combo("##editor_combo", current_idx, editor_names) - if changed: - self._set_external_editor_default(editor_names[new_idx]) - imgui.text("") - imgui.text("Configured Editors:") - imgui.separator() - for name in editor_names: - editor = editors.get(name) - if not editor: - continue - is_default = name == default_name - marker = " (default)" if is_default else "" - if is_default: - imgui.text_colored(C_IN, f" {name}{marker}") - else: - imgui.text(f" {name}{marker}") - imgui.text(f" {editor.path}") - if editor.diff_args: - imgui.textDisabled(f" diff: {editor.diff_args}") - imgui.text("") - imgui.text("Config: config.toml [tools.text_editors]") - imgui.text("Override: manual_slop.toml default_editor") - except Exception as e: - imgui.text_colored(C_TC, f"Error: {str(e)}") - - def _render_theme_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_theme_panel") - exp, opened = imgui.begin("Theme", self.show_windows["Theme"]) - self.show_windows["Theme"] = bool(opened) - if exp: - imgui.text("Palette") - cp = theme.get_current_palette() - if imgui.begin_combo("##pal", cp): - for p in theme.get_palette_names(): - if imgui.selectable(p, p == cp)[0]: - theme.apply(p) - self._flush_to_config() - models.save_config(self.config) - imgui.end_combo() - - imgui.separator() - ch1, self.ui_separate_message_panel = imgui.checkbox("Separate Message Panel", self.ui_separate_message_panel) - ch2, self.ui_separate_response_panel = imgui.checkbox("Separate Response Panel", self.ui_separate_response_panel) - ch3, self.ui_separate_tool_calls_panel = imgui.checkbox("Separate Tool Calls Panel", self.ui_separate_tool_calls_panel) - if ch1: self.show_windows["Message"] = self.ui_separate_message_panel - if ch2: self.show_windows["Response"] = self.ui_separate_response_panel - if ch3: self.show_windows["Tool Calls"] = self.ui_separate_tool_calls_panel - imgui.separator() - imgui.text("Font") - imgui.push_item_width(-150) - ch, path = imgui.input_text("##fontp", theme.get_current_font_path()) - imgui.pop_item_width() - if ch: theme._current_font_path = path - imgui.same_line() - if imgui.button("Browse##font"): - r = hide_tk_root() - p = filedialog.askopenfilename(filetypes=[("Fonts", "*.ttf *.otf"), ("All", "*.*")]) - r.destroy() - if p: theme._current_font_path = p - imgui.text("Size (px)") - imgui.same_line() - imgui.push_item_width(100) - ch, size = imgui.input_float("##fonts", theme.get_current_font_size(), 1.0, 1.0, "%.0f") - if ch: theme._current_font_size = size - imgui.pop_item_width() - imgui.same_line() - if imgui.button("Apply Font (Requires Restart)"): - self._flush_to_config() - models.save_config(self.config) - self.ai_status = "Font settings saved. Restart required." - imgui.separator() - imgui.text("UI Scale (DPI)") - ch, scale = imgui.slider_float("##scale", theme.get_current_scale(), 0.5, 3.0, "%.2f") - if ch: - theme.set_scale(scale) - self._flush_to_config() - models.save_config(self.config) - - imgui.text("Panel Transparency") - ch_t, trans = imgui.slider_float("##trans", theme.get_transparency(), 0.1, 1.0, "%.2f") - if ch_t: - theme.set_transparency(trans) - self._flush_to_config() - models.save_config(self.config) - - imgui.text("Panel Item Transparency") - ch_ct, ctrans = imgui.slider_float("##ctrans", theme.get_child_transparency(), 0.1, 1.0, "%.2f") - if ch_ct: - theme.set_child_transparency(ctrans) - bg = bg_shader.get_bg() - ch_bg, bg.enabled = imgui.checkbox("Animated Background Shader", bg.enabled) - if ch_bg: - gui_cfg = self.config.setdefault("gui", {}) - gui_cfg["bg_shader_enabled"] = bg.enabled - self._flush_to_config() - models.save_config(self.config) - - ch_crt, self.ui_crt_filter = imgui.checkbox("CRT Filter", self.ui_crt_filter) - if ch_crt: - gui_cfg = self.config.setdefault("gui", {}) - gui_cfg["crt_filter_enabled"] = self.ui_crt_filter - self._flush_to_config() - models.save_config(self.config) - self._flush_to_config() - models.save_config(self.config) - imgui.end() - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_theme_panel") - - if not imgui.collapsing_header("Discussions", imgui.TreeNodeFlags_.default_open): return - names = self._get_discussion_names(); grouped = {} - for name in names: - base = name.split("_take_")[0]; grouped.setdefault(base, []).append(name) - active_base = self.active_discussion.split("_take_")[0] - if active_base not in grouped: active_base = names[0] if names else "" - base_names = sorted(grouped.keys()) - if imgui.begin_combo("##disc_sel", active_base): - for bname in base_names: - is_selected = (bname == active_base) - if imgui.selectable(bname, is_selected)[0]: - target = bname if bname in names else grouped[bname][0] - if target != self.active_discussion: self._switch_discussion(target) - if is_selected: imgui.set_item_default_focus() - imgui.end_combo() - active_base = self.active_discussion.split("_take_")[0]; current_takes = grouped.get(active_base, []) - if imgui.begin_tab_bar("discussion_takes_tabs"): - for take_name in current_takes: - label = "Original" if take_name == active_base else take_name.replace(f"{active_base}_", "").replace("_", " ").title() - flags = imgui.TabItemFlags_.set_selected if take_name == self.active_discussion else 0 - with imscope.tab_item(f"{label}###{take_name}", flags) as (exp, _): - if exp and take_name != self.active_discussion: self._switch_discussion(take_name) - with imscope.tab_item("Synthesis###Synthesis") as (exp, _): - if exp: self._render_synthesis_panel() - imgui.end_tab_bar() - if "_take_" in self.active_discussion: - if imgui.button("Promote Take"): - base_name = self.active_discussion.split("_take_")[0]; new_name = f"{base_name}_promoted"; counter = 1 - while new_name in names: new_name = f"{base_name}_promoted_{counter}"; counter += 1 - project_manager.promote_take(self.project, self.active_discussion, new_name); self._switch_discussion(new_name) - imgui.same_line() - if self.active_track: - imgui.same_line(); ch, self._track_discussion_active = imgui.checkbox("Track Discussion", self._track_discussion_active) - if ch: - if self._track_discussion_active: - self._flush_disc_entries_to_project() - history_strings = project_manager.load_track_history(self.active_track.id, self.active_project_root) - with self._disc_entries_lock: self.disc_entries = models.parse_history_entries(history_strings, self.disc_roles) - self.ai_status = f"track discussion: {self.active_track.id}" - else: self._flush_disc_entries_to_project(); self._switch_discussion(self.active_discussion); self.ai_status = "track discussion disabled" - self._render_discussion_metadata() - - def _load_fonts(self) -> None: - # Set hello_imgui assets folder to the actual absolute path - assets_dir = Path(__file__).parent.parent / "assets" - if assets_dir.exists(): - hello_imgui.set_assets_folder(str(assets_dir.absolute())) - - # Improved font rendering with oversampling - config = imgui.ImFontConfig() - config.oversample_h = 3 - config.oversample_v = 3 - - font_path, font_size = theme.get_font_loading_params() - - if font_path: - p = Path(font_path) - if p.is_absolute(): - try: - if p.is_relative_to(assets_dir): - font_path = str(p.relative_to(assets_dir)).replace("\\", "/") - except (ValueError, AttributeError): - pass # Fallback to original font_path if relative_to fails or on old Python - - # Just try loading it directly; hello_imgui will look in the assets folder - try: - self.main_font = hello_imgui.load_font_ttf_with_font_awesome_icons(font_path, font_size, config) - except Exception as e: - print(f"Failed to load main font {font_path}: {e}") - self.main_font = None - else: - self.main_font = None - - try: - params = hello_imgui.FontLoadingParams(font_config=config) - self.mono_font = hello_imgui.load_font("fonts/MapleMono-Regular.ttf", font_size, params) - except Exception as e: - print(f"Failed to load mono font: {e}") - self.mono_font = None - - def _post_init(self) -> None: - theme.apply_current() - - def run(self) -> None: - """ - Initializes the ImGui runner and starts the main application loop. - [C: simulation/sim_base.py:run_sim, src/mcp_client.py:get_git_diff, src/project_manager.py:get_git_commit, src/project_manager.py:get_git_log, src/rag_engine.py:RAGEngine._search_mcp, src/shell_runner.py:run_powershell, tests/conftest.py:kill_process_tree, tests/conftest.py:live_gui, tests/test_conductor_abort_event.py:test_conductor_abort_event_populated, tests/test_conductor_engine_v2.py:test_conductor_engine_dynamic_parsing_and_execution, tests/test_conductor_engine_v2.py:test_conductor_engine_run_executes_tickets_in_order, tests/test_extended_sims.py:test_ai_settings_sim_live, tests/test_extended_sims.py:test_context_sim_live, tests/test_extended_sims.py:test_execution_sim_live, tests/test_extended_sims.py:test_tools_sim_live, tests/test_external_editor_gui.py:get_vscode_processes, tests/test_external_editor_gui.py:test_vscode_launches_with_diff_view, tests/test_gui_custom_window.py:test_app_window_is_borderless, tests/test_headless_simulation.py:module, tests/test_headless_verification.py:test_headless_verification_error_and_qa_interceptor, tests/test_headless_verification.py:test_headless_verification_full_run, tests/test_mock_gemini_cli.py:run_mock, tests/test_orchestration_logic.py:test_conductor_engine_run, tests/test_parallel_execution.py:test_conductor_engine_pool_integration, tests/test_sim_ai_settings.py:test_ai_settings_simulation_run, tests/test_sim_context.py:test_context_simulation_run, tests/test_sim_execution.py:test_execution_simulation_run, tests/test_sim_tools.py:test_tools_simulation_run] - """ - if "--headless" in sys.argv: - print("Headless mode active") - self._fetch_models(self.current_provider) - import uvicorn - headless_cfg = self.config.get("headless", {}) - port = headless_cfg.get("port", 8000) - api = self.create_api() - uvicorn.run(api, host="0.0.0.0", port=port) - else: - theme.load_from_config(self.config) - self.runner_params = hello_imgui.RunnerParams() - self.runner_params.app_window_params.window_title = "manual slop" - - if sys.platform == "win32": - self.runner_params.app_window_params.borderless = True - self.runner_params.app_window_params.borderless_closable = False - self.runner_params.app_window_params.borderless_movable = False - self.runner_params.app_window_params.borderless_resizable = True - - self.runner_params.app_window_params.window_geometry.size = (1680, 1200) - self.runner_params.imgui_window_params.enable_viewports = getattr(self, "ui_multi_viewport", False) - self.runner_params.imgui_window_params.remember_theme = True - self.runner_params.imgui_window_params.tweaked_theme = theme.get_tweaked_theme() - self.runner_params.imgui_window_params.default_imgui_window_type = hello_imgui.DefaultImGuiWindowType.provide_full_screen_dock_space - - # Enforce DPI Awareness and User Scale - user_scale = theme.get_current_scale() - self.runner_params.dpi_aware_params.dpi_window_size_factor = user_scale - - # Detect Monitor Refresh Rate for capping (Win32 only) - fps_cap = 60.0 - if sys.platform == "win32": - try: - # Use PowerShell to get max refresh rate across all controllers - cmd = "powershell -NoProfile -Command \"Get-CimInstance -ClassName Win32_VideoController | Select-Object -ExpandProperty CurrentRefreshRate\"" - out = subprocess.check_output(cmd, shell=True).decode().splitlines() - rates = [float(r.strip()) for r in out if r.strip().isdigit()] - if rates: fps_cap = max(rates) - except Exception: pass - - # Enable idling with monitor refresh rate to effectively cap FPS - self.runner_params.fps_idling.enable_idling = True - self.runner_params.fps_idling.fps_idle = fps_cap - - self.runner_params.imgui_window_params.show_menu_bar = True - self.runner_params.imgui_window_params.show_menu_view_themes = True - self.runner_params.ini_folder_type = hello_imgui.IniFolderType.current_folder - self.runner_params.ini_filename = "manualslop_layout.ini" - self.runner_params.callbacks.show_gui = self._gui_func - self.runner_params.callbacks.show_menus = self._show_menus - self.runner_params.callbacks.load_additional_fonts = self._load_fonts - self.runner_params.callbacks.setup_imgui_style = theme.apply_current - self.runner_params.callbacks.post_init = self._post_init - self._fetch_models(self.current_provider) - md_options = markdown_helper.get_renderer().options - immapp.run(self.runner_params, add_ons_params=immapp.AddOnsParams(with_markdown_options=md_options)) - # On exit - self.shutdown() - session_logger.close_session() +#endregion: MMA def main() -> None: app = App()