diff --git a/scripts/restore_regions_final.py b/scripts/restore_regions_final.py index 11ec1d5f..8f54cf52 100644 --- a/scripts/restore_regions_final.py +++ b/scripts/restore_regions_final.py @@ -1,55 +1,56 @@ +import ast import sys import re def restore(): file_path = "src/gui_2.py" with open(file_path, "r", encoding="utf-8") as f: - lines = f.read().splitlines() + content = f.read() - # Regions and their member functions (module-level) + # Comprehensive region mapping for ALL render functions region_map = { - "Diagnostics & Analytics": ["render_usage_analytics_panel", "render_diagnostics_panel"], + "Main Interface": ["render_main_interface", "render_custom_title_bar"], + "Diagnostics & Analytics": ["render_usage_analytics_panel", "render_diagnostics_panel", "render_cache_panel", "render_tool_analytics_panel", "render_token_budget_panel"], "Logging": ["render_log_management"], - "Project Management": ["render_project_settings_hub"], - "AI Settings": ["render_ai_settings_hub", "render_preset_manager_content", "render_preset_manager_window", "render_tool_preset_manager_content", "render_tool_preset_manager_window", "render_persona_editor_window"], - "Context Management": ["render_files_and_media", "render_files_panel", "render_screenshots_panel", "render_context_batch_actions", "render_add_context_files_modal"], - "Discussions": ["render_discussion_hub", "render_discussion_entry", "render_discussion_entry_read_mode"], - "Operations Monitor": ["render_operations_hub", "render_message_panel", "render_response_panel", "render_tool_calls_panel", "render_external_tools_panel"], - "Misc Tools": ["render_text_viewer", "render_heavy_text", "render_thinking_trace", "render_selectable_label", "render_window_if_open"], - "MMA": ["render_mma_dashboard", "render_mma_modals", "render_mma_track_summary", "render_mma_epic_planner", "render_mma_conductor_setup", "render_mma_track_browser", "render_mma_global_controls", "render_mma_usage_section", "render_mma_ticket_editor", "render_mma_agent_streams", "render_tier_stream_panel", "render_track_proposal_modal"] + "Project Management": ["render_project_settings_hub", "render_projects_panel", "render_paths_panel", "render_path_field"], + "AI Settings": ["render_ai_settings_hub", "render_rag_panel", "render_system_prompts_panel", "render_agent_tools_panel", "render_provider_panel", "render_persona_selector_panel", "render_base_prompt_diff_modal", "render_save_preset_modal", "render_preset_manager_content", "render_preset_manager_window", "render_tool_preset_manager_content", "render_tool_preset_manager_window", "render_persona_editor_window"], + "Context Management": ["render_files_and_media", "render_files_panel", "render_screenshots_panel", "render_context_batch_actions", "render_add_context_files_modal", "render_context_composition_panel", "render_ast_inspector_modal", "render_save_workspace_profile_modal", "render_context_presets_panel", "render_context_screenshots", "render_context_files_table", "render_context_presets", "render_snapshot_tab"], + "Discussions": ["render_discussion_hub", "render_discussion_entry", "render_discussion_entry_read_mode", "render_history_window", "render_session_insights_panel", "render_prior_session_view", "render_thinking_indicator", "render_synthesis_panel", "render_comms_history_panel", "render_takes_panel", "render_discussion_entries", "render_discussion_entry_controls", "render_discussion_metadata", "render_discussion_panel", "render_discussion_roles", "render_discussion_selector", "render_discussion_tab"], + "Operations Monitor": ["render_operations_hub", "render_message_panel", "render_response_panel", "render_tool_calls_panel", "render_external_tools_panel", "render_text_viewer_window", "render_patch_modal", "render_external_editor_panel", "render_approve_script_modal"], + "Misc Tools": ["render_theme_panel", "render_shader_live_editor", "render_markdown_test", "render_error_tint", "render_text_viewer", "render_heavy_text", "render_thinking_trace", "render_selectable_label", "render_window_if_open"], + "MMA": ["render_mma_dashboard", "render_mma_modals", "render_mma_track_summary", "render_mma_epic_planner", "render_mma_conductor_setup", "render_mma_track_browser", "render_mma_global_controls", "render_mma_usage_section", "render_mma_ticket_editor", "render_mma_agent_streams", "render_tier_stream_panel", "render_track_proposal_modal", "render_ticket_queue", "render_task_dag_panel", "render_beads_tab", "render_mma_focus_selector"] } - # 1. Find where extracted functions start - start_idx = -1 - for i, line in enumerate(lines): - if line.startswith("def render_") and not "render_error_tint" in line: - start_idx = i - break + # Parse to get exact line ranges + lines = content.splitlines() + tree = ast.parse(content) - if start_idx == -1: return - - prefix = lines[:start_idx] - func_lines = lines[start_idx:] - - # 2. Extract each function block func_blocks = {} - i = 0 - while i < len(func_lines): - if func_lines[i].startswith("def "): - m = re.match(r"def (\w+)\(", func_lines[i]) - if m: - f_name = m.group(1) - f_start = i - f_end = len(func_lines) - for j in range(i + 1, len(func_lines)): - if func_lines[j].startswith("def "): - f_end = j - break - func_blocks[f_name] = "\n".join(func_lines[f_start:f_end]) - i = f_end - 1 - i += 1 + + for node in tree.body: + if isinstance(node, ast.FunctionDef): + f_name = node.name + s_idx = node.lineno - 1 + if node.decorator_list: s_idx = node.decorator_list[0].lineno - 1 + e_idx = node.end_lineno + func_blocks[f_name] = "\n".join(lines[s_idx:e_idx]) - # 3. Re-assemble with regions + # Find the start of module-level render functions + first_render_idx = len(lines) + for node in tree.body: + if isinstance(node, ast.FunctionDef) and node.name.startswith("render_"): + first_render_idx = min(first_render_idx, node.lineno - 1) + if node.decorator_list: first_render_idx = min(first_render_idx, node.decorator_list[0].lineno - 1) + + # Get prefix and strip existing region tags + prefix_raw = lines[:first_render_idx] + prefix = [] + for line in prefix_raw: + if line.strip().startswith("#region:") or line.strip().startswith("#endregion:"): + continue + prefix.append(line) + + # Re-assemble with regions assembled = [] assigned = set() for r_name, funcs in region_map.items(): @@ -61,15 +62,17 @@ def restore(): if group: assembled.append(f"#region: {r_name}\n\n" + "\n\n".join(group) + f"\n\n#endregion: {r_name}") - # 4. Append unassigned (like render_error_tint) + # Append unassigned render functions for f_name, block in func_blocks.items(): - if f_name not in assigned: + if f_name.startswith("render_") and f_name not in assigned: assembled.append(block) - final_content = "\n".join(prefix) + "\n\n" + "\n\n".join(assembled) + "\n" + final_content = "\n".join(prefix).strip() + "\n\n" + "\n\n".join(assembled) + "\n" + final_content = re.sub(r"\n\n\n+", "\n\n", final_content) + with open(file_path, "w", encoding="utf-8") as f: f.write(final_content) - print("Regions restored successfully.") + print("Comprehensive regions restored successfully.") if __name__ == "__main__": restore() diff --git a/src/gui_2.py b/src/gui_2.py index ff1fc01a..8e601d43 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -838,7 +838,6 @@ class App: traceback.print_exc(file=sys.stderr) sys.stderr.flush() -#region: Logging def cb_load_prior_log(self, path: Optional[str] = None) -> None: if path is None: root = hide_tk_root() @@ -846,9 +845,7 @@ class App: root.destroy() if path: self.controller.cb_load_prior_log(path) -#endregion: Logging -#region: Project Management def _set_external_editor_default(self, editor_name: str) -> None: from src import models if "tools" not in self.config: self.config["tools"] = {} @@ -883,9 +880,6 @@ class App: paths.reset_resolved() self.init_state() self.ai_status = 'paths applied and session reset' -#endregion: Project Management - -#region: Context Management def _populate_auto_slices(self, f_item: models.FileItem) -> None: """ @@ -951,9 +945,6 @@ class App: threading.Thread(target=_stats_worker, daemon=True).start() return total_lines, total_ast -#endregion: Context Management - -#region: Misc Tools def _close_vscode_diff(self) -> None: if hasattr(self, '_vscode_diff_process') and self._vscode_diff_process: @@ -1007,9 +998,6 @@ class App: except Exception as e: self._patch_error_message = str(e) -#endregion: Misc Tools - -#region: MMA def _reorder_ticket(self, src_idx: int, dst_idx: int) -> None: """ @@ -1121,7 +1109,6 @@ class App: changed = True self._push_mma_state_update() -#endregion: MMA def main() -> None: app = App() @@ -1130,6 +1117,7 @@ def main() -> None: if __name__ == "__main__": main() +#region: Main Interface def render_main_interface(app: App) -> None: render_error_tint(app) @@ -1227,123 +1215,7 @@ def render_custom_title_bar(app: App) -> None: # Controls are now embedded in _show_menus. pass -def render_history_window(app: App) -> None: - if not app.show_windows.get('Undo/Redo History', False): - return - def iterate_history(history: typing.List[typing.Dict[str, typing.Any]]) -> None: - for i, entry in enumerate(reversed(history)): - actual_idx = len(history) - 1 - i - desc = entry.get("description", "UI Change") - ts = entry.get("timestamp", 0.0) - ts_str = datetime.datetime.fromtimestamp(ts).strftime("%H:%M:%S") - label = f"[{ts_str}] {desc}##{actual_idx}" - _, selected = imgui.selectable(label, False) - if selected: app._handle_jump_to_history(actual_idx) - with imscope.window("Undo/Redo History", app.show_windows['Undo/Redo History']) as (exp, opened): - app.show_windows['Undo/Redo History'] = bool(opened) - if exp: - if imgui.button("Undo") and app.history.can_undo: app._handle_undo() - imgui.same_line() - if imgui.button("Redo") and app.history.can_redo: app._handle_redo() - imgui.separator() - with imscope.child("history_list", 0, 0, True): - history = app.history.get_history() - if not history: imgui.text("No history available.") - else: iterate_history() - -def render_theme_panel(app: App) -> None: - if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_theme_panel") - exp, opened = imgui.begin("Theme", app.show_windows["Theme"]) - app.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) - app._flush_to_config() - models.save_config(app.config) - imgui.end_combo() - - imgui.separator() - ch1, app.ui_separate_message_panel = imgui.checkbox("Separate Message Panel", app.ui_separate_message_panel) - ch2, app.ui_separate_response_panel = imgui.checkbox("Separate Response Panel", app.ui_separate_response_panel) - ch3, app.ui_separate_tool_calls_panel = imgui.checkbox("Separate Tool Calls Panel", app.ui_separate_tool_calls_panel) - if ch1: app.show_windows["Message"] = app.ui_separate_message_panel - if ch2: app.show_windows["Response"] = app.ui_separate_response_panel - if ch3: app.show_windows["Tool Calls"] = app.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)"): - app._flush_to_config() - models.save_config(app.config) - app.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) - app._flush_to_config() - models.save_config(app.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) - app._flush_to_config() - models.save_config(app.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 = app.config.setdefault("gui", {}) - gui_cfg["bg_shader_enabled"] = bg.enabled - app._flush_to_config() - models.save_config(app.config) - - ch_crt, app.ui_crt_filter = imgui.checkbox("CRT Filter", app.ui_crt_filter) - if ch_crt: - gui_cfg = app.config.setdefault("gui", {}) - gui_cfg["crt_filter_enabled"] = app.ui_crt_filter - app._flush_to_config() - models.save_config(app.config) - - imgui.end() - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_theme_panel") - -def render_shader_live_editor(app: App) -> None: - """ - [C: tests/test_shader_live_editor.py:test_shader_live_editor_renders] - """ - if app.show_windows.get('Shader Editor', False): - with imscope.window('Shader Editor', app.show_windows['Shader Editor']) as (exp, opened): - app.show_windows['Shader Editor'] = bool(opened) - if exp: - changed_crt, app.shader_uniforms['crt'] = imgui.slider_float('CRT Curvature', app.shader_uniforms['crt'], 0.0, 2.0) - changed_scan, app.shader_uniforms['scanline'] = imgui.slider_float('Scanline Intensity', app.shader_uniforms['scanline'], 0.0, 1.0) - changed_bloom, app.shader_uniforms['bloom'] = imgui.slider_float('Bloom Threshold', app.shader_uniforms['bloom'], 0.0, 1.0) +#endregion: Main Interface #region: Diagnostics & Analytics @@ -1732,6 +1604,458 @@ def render_project_settings_hub(app: App) -> None: with imscope.tab_item('Paths') as (exp, _): if exp: render_paths_panel(app) +def render_projects_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_projects_panel") + proj_name = app.project.get("project", {}).get("name", Path(app.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(app.ui_project_execution_mode) if app.ui_project_execution_mode in modes else 0 + ch, new_idx = imgui.combo("##exec_mode", current_idx, modes) + if ch: app.ui_project_execution_mode = modes[new_idx] + imgui.separator() + imgui.text("Git Directory") + ch, app.ui_project_git_dir = imgui.input_text("##git_dir", app.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: app.ui_project_git_dir = d + imgui.separator() + imgui.text("Output Dir") + ch, app.ui_output_dir = imgui.input_text("##out_dir", app.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: app.ui_output_dir = d + imgui.separator() + imgui.text("Conductor Directory") + ch, app.ui_project_conductor_dir = imgui.input_text("##cond_dir", app.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: app.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(app.project_paths): + is_active = (pp == app.active_project_path) + if imgui.button(f"x##p{i}"): + removed = app.project_paths.pop(i) + if removed == app.active_project_path and app.project_paths: app._switch_project(app.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}"): app._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 app.project_paths: + app.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 app.project_paths: app.project_paths.append(p) + app._switch_project(p) + imgui.same_line() + if imgui.button("Save All"): + app._flush_to_project() + app._flush_to_config() + models.save_config(app.config) + app.ai_status = "config saved" + ch, app.ui_word_wrap = imgui.checkbox("Word-Wrap (Read-only panels)", app.ui_word_wrap) + ch, app.ui_auto_scroll_comms = imgui.checkbox("Auto-scroll Comms History", app.ui_auto_scroll_comms) + ch, app.ui_auto_scroll_tool_calls = imgui.checkbox("Auto-scroll Tool History", app.ui_auto_scroll_tool_calls) + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_projects_panel") + +def render_paths_panel(app: App) -> None: + if app.perf_profiling_enabled: app.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(app, attr) + changed, new_val = imgui.input_text(f"##{key}", val) + if imgui.is_item_hovered(): imgui.set_tooltip(tooltip) + if changed: setattr(app, 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(app, attr, d) + +#endregion: Project Management + +#region: AI Settings + +def render_ai_settings_hub(app: App) -> None: + render_persona_selector_panel(app) + if imgui.collapsing_header("Provider & Model"): render_provider_panel(app) + if imgui.collapsing_header("System Prompts"): render_system_prompts_panel(app) + if imgui.collapsing_header("RAG Settings"): render_rag_panel(app) + render_agent_tools_panel(app) + +def render_rag_panel(app: App) -> None: + conf = app.controller.rag_config + if not conf: return + ch, conf.enabled = imgui.checkbox("Enable RAG", conf.enabled) + + imgui.text("Vector Store Provider") + providers = ['chroma', 'qdrant', 'mock'] + try: + idx = providers.index(conf.vector_store.provider) + except (ValueError, AttributeError): + idx = 0 + ch2, next_idx = imgui.combo("##rag_provider", idx, providers) + if ch2: + conf.vector_store.provider = providers[next_idx] + + imgui.text("Embedding Provider") + emb_providers = ['gemini', 'local'] + try: + idx_e = emb_providers.index(conf.embedding_provider) + except (ValueError, AttributeError): + idx_e = 0 + ch3, next_idx_e = imgui.combo("##rag_emb_provider", idx_e, emb_providers) + if ch3: + conf.embedding_provider = emb_providers[next_idx_e] + + imgui.text("Chunk Size") + imgui.set_next_item_width(150) + ch4, conf.chunk_size = imgui.input_int("##rag_chunk_size", conf.chunk_size) + imgui.text("Chunk Overlap") + imgui.set_next_item_width(150) + ch5, conf.chunk_overlap = imgui.input_int("##rag_chunk_overlap", conf.chunk_overlap) + + imgui.separator() + imgui.text(f"Status: {app.controller.rag_status}") + + if imgui.button("Rebuild Index"): app.controller.event_queue.put('click', 'btn_rebuild_rag_index') + +def render_system_prompts_panel(app: App) -> None: + imgui.text("Global System Prompt (all projects)") + preset_names = sorted(app.controller.presets.keys()) + current_global = app.controller.ui_global_preset_name or "Select Preset..." + imgui.set_next_item_width(200) + if imgui.begin_combo("##global_preset", current_global): + for name in preset_names: + is_sel = (name == current_global) + if imgui.selectable(name, is_sel)[0]: app.controller._apply_preset(name, "global") + if is_sel: imgui.set_item_default_focus() + imgui.end_combo() + imgui.same_line(0, 8) + if imgui.button("Manage Presets##global"): app.show_preset_manager_window = True + imgui.set_item_tooltip("Open preset management modal") + ch, app.ui_global_system_prompt = imgui.input_text_multiline("##gsp", app.ui_global_system_prompt, imgui.ImVec2(-1, 100)) + imgui.separator() + _, app.ui_use_default_base_prompt = imgui.checkbox("Use Default Base System Prompt", app.ui_use_default_base_prompt) + imgui.same_line() + if imgui.button("Reset to Default##btn_reset_base_prompt"): app.controller._cb_reset_base_prompt() + imgui.same_line() + if imgui.button("Show Diff##btn_show_base_prompt_diff"): app.controller._cb_show_base_prompt_diff() + imgui.set_item_tooltip("Compare current base prompt with the default.") + + imgui.same_line() + imgui.text_disabled("(?)") + imgui.set_item_tooltip("The Base System Prompt contains foundational instructions for the AI, including its role as a coding assistant and safety guidelines. You can override it here if needed.") + + header_flags = imgui.TreeNodeFlags_.default_open if not app.ui_use_default_base_prompt else 0 + if imgui.collapsing_header("Base System Prompt (foundational instructions)", header_flags): + if app.ui_use_default_base_prompt: + imgui.begin_disabled() + imgui.input_text_multiline("##base_prompt_def", ai_client._SYSTEM_PROMPT, imgui.ImVec2(-1, 100), imgui.InputTextFlags_.read_only) + imgui.end_disabled() + imgui.text_disabled(f"Characters: {len(ai_client._SYSTEM_PROMPT)}") + else: + ch, app.ui_base_system_prompt = imgui.input_text_multiline("##base_prompt", app.ui_base_system_prompt, imgui.ImVec2(-1, 150)) + imgui.text_disabled(f"Characters: {len(app.ui_base_system_prompt)}") + imgui.separator() + imgui.text("Project System Prompt") + current_project = app.controller.ui_project_preset_name or "Select Preset..." + imgui.set_next_item_width(200) + if imgui.begin_combo("##project_preset", current_project): + for name in preset_names: + is_sel = (name == current_project) + if imgui.selectable(name, is_sel)[0]: app.controller._apply_preset(name, "project") + if is_sel: imgui.set_item_default_focus() + imgui.end_combo() + imgui.same_line(0, 8) + if imgui.button("Manage Presets##project"): app.show_preset_manager_window = True + imgui.set_item_tooltip("Open preset management modal") + ch, app.ui_project_system_prompt = imgui.input_text_multiline("##psp", app.ui_project_system_prompt, imgui.ImVec2(-1, 100)) + +def render_agent_tools_panel(app: App) -> None: + if imgui.collapsing_header("Active Tool Presets & Biases", imgui.TreeNodeFlags_.default_open): + imgui.text("Tool Preset") + presets = app.controller.tool_presets + preset_names = [""] + sorted(list(presets.keys())) + + active = getattr(app, "ui_active_tool_preset", "") + if active is None: active = "" + try: + idx = preset_names.index(active) + except ValueError: + idx = 0 + + ch, new_idx = imgui.combo("##tool_preset_select", idx, preset_names) + if ch: + app.ui_active_tool_preset = preset_names[new_idx] + + imgui.same_line() + if imgui.button("Manage Presets##tools"): app.show_tool_preset_manager_window = True + if imgui.is_item_hovered(): imgui.set_tooltip("Configure tool availability and default modes.") + + imgui.dummy(imgui.ImVec2(0, 4)) + imgui.text("Bias Profile") + if imgui.begin_combo("##bias", getattr(app, 'ui_active_bias_profile', "") or "None"): + if imgui.selectable("None", not getattr(app, 'ui_active_bias_profile', ""))[0]: + app.ui_active_bias_profile = "" + ai_client.set_bias_profile(None) + for bname in sorted(app.controller.bias_profiles.keys()): + if not bname: continue + if imgui.selectable(bname, bname == getattr(app, 'ui_active_bias_profile', ""))[0]: + app.ui_active_bias_profile = bname + ai_client.set_bias_profile(bname) + imgui.end_combo() + + imgui.dummy(imgui.ImVec2(0, 8)) + cat_options = ["All"] + sorted(list(models.DEFAULT_TOOL_CATEGORIES.keys())) + try: + f_idx = cat_options.index(app.ui_tool_filter_category) + except ValueError: + f_idx = 0 + imgui.set_next_item_width(200) + ch_cat, next_f_idx = imgui.combo("Filter Category##agent", f_idx, cat_options) + if ch_cat: app.ui_tool_filter_category = cat_options[next_f_idx] + + imgui.dummy(imgui.ImVec2(0, 8)) + active_name = app.ui_active_tool_preset + if active_name and active_name in presets: + preset = presets[active_name] + for cat_name, tools in preset.categories.items(): + if app.ui_tool_filter_category != "All" and app.ui_tool_filter_category != cat_name: continue + if imgui.tree_node(cat_name): + for tool in tools: + if tool.weight >= 5: imgui.text_colored(vec4(255, 100, 100), "[HIGH]"); imgui.same_line() + elif tool.weight == 4: imgui.text_colored(vec4(255, 255, 100), "[PREF]"); imgui.same_line() + elif tool.weight == 2: imgui.text_colored(vec4(255, 150, 50), "[REJECT]"); imgui.same_line() + elif tool.weight <= 1: imgui.text_colored(vec4(180, 180, 180), "[LOW]"); imgui.same_line() + + imgui.text(tool.name); imgui.same_line(180) + + mode = tool.approval + if imgui.radio_button(f"Auto##{cat_name}_{tool.name}", mode == "auto"): tool.approval = "auto" + imgui.same_line() + if imgui.radio_button(f"Ask##{cat_name}_{tool.name}", mode == "ask"): tool.approval = "ask" + imgui.tree_pop() + +def render_provider_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_provider_panel") + imgui.text("Provider") + if imgui.begin_combo("##prov", app.current_provider): + for p in models.PROVIDERS: + if imgui.selectable(p, p == app.current_provider)[0]: + app.current_provider = p + imgui.end_combo() + imgui.separator() + imgui.text("Model") + if imgui.begin_list_box("##models", imgui.ImVec2(-1, 120)): + for m in app.available_models: + if imgui.selectable(m, m == app.current_model)[0]: + app.current_model = m + imgui.end_list_box() + imgui.separator() + imgui.text("Parameters") + # Temperature + imgui.push_id("temp") + imgui.set_next_item_width(imgui.get_content_region_avail().x * 0.6) + _, app.temperature = imgui.slider_float("##slider", app.temperature, 0.0, 2.0, "%.2f") + imgui.same_line() + imgui.set_next_item_width(-1) + _, app.temperature = imgui.input_float("Temp", app.temperature, 0.0, 0.0, "%.2f") + imgui.pop_id() + + # Top-P + imgui.push_id("top_p") + imgui.set_next_item_width(imgui.get_content_region_avail().x * 0.6) + _, app.top_p = imgui.slider_float("##slider", app.top_p, 0.0, 1.0, "%.2f") + imgui.same_line() + imgui.set_next_item_width(-1) + _, app.top_p = imgui.input_float("Top-P", app.top_p, 0.0, 0.0, "%.2f") + imgui.pop_id() + + # Max Tokens + imgui.push_id("max_tokens") + imgui.set_next_item_width(imgui.get_content_region_avail().x * 0.6) + _, app.max_tokens = imgui.slider_int("##slider", app.max_tokens, 1, 32768) + imgui.same_line() + imgui.set_next_item_width(-1) + _, app.max_tokens = imgui.input_int("MaxTok", app.max_tokens) + imgui.pop_id() + + ch, app.history_trunc_limit = imgui.input_int("History Truncation Limit", app.history_trunc_limit, 1024) + + if app.current_provider == "gemini_cli": + imgui.separator() + imgui.text("Gemini CLI") + sid = "None" + if hasattr(ai_client, "_gemini_cli_adapter") and ai_client._gemini_cli_adapter: sid = ai_client._gemini_cli_adapter.session_id or "None" + imgui.text("Session ID:"); imgui.same_line(); render_selectable_label(app, "gemini_cli_sid", sid, width=200) + if imgui.button("Reset CLI Session"): ai_client.reset_session() + imgui.text("Binary Path") + ch, app.ui_gemini_cli_path = imgui.input_text("##gcli_path", app.ui_gemini_cli_path) + imgui.same_line() + if imgui.button("Browse##gcli"): + r = hide_tk_root() + p = filedialog.askopenfilename(title="Select gemini CLI binary") + r.destroy() + if p: app.ui_gemini_cli_path = p + if ch: + if hasattr(ai_client, "_gemini_cli_adapter") and ai_client._gemini_cli_adapter: + ai_client._gemini_cli_adapter.binary_path = app.ui_gemini_cli_path + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_provider_panel") + +def render_persona_selector_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_persona_selector_panel") + imgui.text("Persona") + if not hasattr(app, 'ui_active_persona'): app.ui_active_persona = "" + personas = getattr(app.controller, 'personas', {}) + if imgui.begin_combo("##persona", app.ui_active_persona or "None"): + if imgui.selectable("None", not app.ui_active_persona)[0]: app.ui_active_persona = "" + for pname in sorted(personas.keys()): + if not pname: continue + if imgui.selectable(pname, pname == app.ui_active_persona)[0]: + app.ui_active_persona = pname + if pname in personas: + persona = personas[pname] + app._editing_persona_name = persona.name + app._editing_persona_system_prompt = persona.system_prompt or "" + app._editing_persona_tool_preset_id = persona.tool_preset or "" + app._editing_persona_bias_profile_id = persona.bias_profile or "" + app._editing_persona_context_preset_id = getattr(persona, 'context_preset', '') or "" + app._editing_persona_aggregation_strategy = getattr(persona, 'aggregation_strategy', '') or "" + app._editing_persona_preferred_models_list = copy.deepcopy(persona.preferred_models) if persona.preferred_models else [] + app._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"): + app.current_provider = first_model.get("provider") + if first_model.get("model"): + app.current_model = first_model.get("model") + if first_model.get("temperature") is not None: + ai_client.temperature = first_model.get("temperature") + app.temperature = first_model.get("temperature") + if first_model.get("max_output_tokens"): + ai_client.max_output_tokens = first_model.get("max_output_tokens") + app.max_tokens = first_model.get("max_output_tokens") + if first_model.get("history_trunc_limit"): + app.history_trunc_limit = first_model.get("history_trunc_limit") + + if persona.system_prompt: app.ui_project_system_prompt = persona.system_prompt + if persona.tool_preset: + app.ui_active_tool_preset = persona.tool_preset + ai_client.set_tool_preset(persona.tool_preset) + if persona.bias_profile: + app.ui_active_bias_profile = persona.bias_profile + ai_client.set_bias_profile(persona.bias_profile) + if getattr(persona, 'context_preset', None): + app.ui_active_context_preset = persona.context_preset + app.load_context_preset(persona.context_preset) + imgui.end_combo() + imgui.same_line() + if imgui.button("Manage Personas"): + app.show_persona_editor_window = True + if app.ui_active_persona and app.ui_active_persona in personas: + persona = personas[app.ui_active_persona] + app._editing_persona_name = persona.name + app._editing_persona_system_prompt = persona.system_prompt or "" + app._editing_persona_tool_preset_id = persona.tool_preset or "" + app._editing_persona_bias_profile_id = persona.bias_profile or "" + app._editing_persona_context_preset_id = getattr(persona, 'context_preset', '') or "" + app._editing_persona_aggregation_strategy = getattr(persona, 'aggregation_strategy', '') or "" + app._editing_persona_preferred_models_list = copy.deepcopy(persona.preferred_models) if persona.preferred_models else [] + app._editing_persona_scope = app.controller.persona_manager.get_persona_scope(persona.name) + app._editing_persona_is_new = False + else: + app._editing_persona_name = "" + app._editing_persona_system_prompt = "" + app._editing_persona_tool_preset_id = "" + app._editing_persona_bias_profile_id = "" + app._editing_persona_context_preset_id = "" + app._editing_persona_aggregation_strategy = "" + app._editing_persona_preferred_models_list = [{ + "provider": app.current_provider, + "model": app.current_model, + "temperature": getattr(app, "temperature", 0.7), + "max_output_tokens": getattr(app, "max_tokens", 4096), + "history_trunc_limit": getattr(app, "history_trunc_limit", 900000) + }] + app._editing_persona_scope = "project" + app._editing_persona_is_new = True + imgui.separator() + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_persona_selector_panel") + +def render_base_prompt_diff_modal(app: App) -> None: + if not getattr(app.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 = app.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)): + app.controller._show_base_prompt_diff_modal = False + imgui.close_current_popup() + imgui.end_popup() + def render_save_preset_modal(app: App) -> None: if not app._show_save_preset_modal: return imgui.open_popup("Save Layout Preset") @@ -1756,17 +2080,6 @@ def render_save_preset_modal(app: App) -> None: app._show_save_preset_modal = False imgui.close_current_popup() -#endregion: Project Management - -#region: AI Settings - -def render_ai_settings_hub(app: App) -> None: - render_persona_selector_panel(app) - if imgui.collapsing_header("Provider & Model"): render_provider_panel(app) - if imgui.collapsing_header("System Prompts"): render_system_prompts_panel(app) - if imgui.collapsing_header("RAG Settings"): render_rag_panel(app) - render_agent_tools_panel(app) - def render_preset_manager_content(app: App, is_embedded: bool = False) -> None: avail = imgui.get_content_region_avail() if not hasattr(app, "_prompt_md_preview"): app._prompt_md_preview = False @@ -2532,6 +2845,380 @@ def render_add_context_files_modal(app: App) -> None: imgui.close_current_popup() imgui.end_popup() +def render_context_composition_panel(app: App) -> None: + """ + [C: tests/test_auto_slices.py:test_add_all_triggers_auto_slices, tests/test_gui_fast_render.py:test_render_context_composition_panel_fast] + """ + if imgui.collapsing_header("Context Composition##panel"): + total_lines, total_ast = app._update_context_file_stats() + render_context_batch_actions(app, total_lines, total_ast) + render_context_files_table(app) + + imgui.separator() + if imgui.collapsing_header("Screenshots"): + render_context_screenshots(app) + imgui.separator() + render_context_presets(app) + +def render_ast_inspector_modal(app: App) -> None: + """ + [C: tests/test_ast_inspector_extended.py:test_ast_inspector_line_range_parsing] + """ + if app._show_ast_inspector: + imgui.open_popup('AST Inspector') + app._show_ast_inspector = False + + #region: AST Inspector + expanded, opened = imgui.begin_popup_modal('AST Inspector', True, imgui.WindowFlags_.always_auto_resize) + if opened: + if expanded: + if app.ui_inspecting_ast_file is None: + imgui.close_current_popup() + else: + f_item = app.ui_inspecting_ast_file + f_path = f_item.path if hasattr(f_item, "path") else str(f_item) + + if f_path != app._cached_ast_file_path: + outline = "" + try: + if f_path.lower().endswith('.py'): outline = mcp_client.py_get_code_outline(f_path) + elif f_path.lower().endswith(('.c', '.h')): outline = mcp_client.ts_c_get_code_outline(f_path) + else: outline = mcp_client.ts_cpp_get_code_outline(f_path) + except Exception as e: + outline = f"Error fetching outline: {e}" + + app._cached_ast_nodes = [] + import re + pattern = re.compile(r'^(\s*)\[(.*?)\] (.*?) \(Lines (\d+)-(\d+)\)') + stack = [] # (indent, name) + for line in outline.splitlines(): + m = pattern.match(line) + if m: + indent_str, kind, name, start_ln, end_ln = m.groups() + indent = len(indent_str) + while stack and stack[-1][0] >= indent: stack.pop() + stack.append((indent, name)) + full_path = '::'.join([s[1] for s in stack]) + app._cached_ast_nodes.append({ + 'indent': indent, + 'kind': kind, + 'name': name, + 'full_path': full_path, + 'start_line': int(start_ln), + 'end_line': int(end_ln) + }) + try: + content = mcp_client.read_file(f_path) + app._cached_ast_file_lines = content.splitlines() + except Exception: + app._cached_ast_file_lines = ["Error loading file content."] + app._cached_ast_file_path = f_path + + imgui.text(f"Inspecting AST: {f_path}") + imgui.separator() + + #region: ast_dual_pane + if imgui.begin_table('ast_dual_pane', 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v): + imgui.table_next_column() + + #region: LEFT COLUMN (Tree) --- + imgui.begin_child("ast_tree_scroll", imgui.ImVec2(0, 600), True) + if True: + if not app._cached_ast_nodes: imgui.text("No AST nodes found or error fetching outline.") + else: + for node in app._cached_ast_nodes: + indent = node['indent'] + kind = node['kind'] + name = node['name'] + full_path = node['full_path'] + + imgui.dummy(imgui.ImVec2(indent * 10, 0)) + imgui.same_line() + imgui.text(f"[{kind}] {name}") + imgui.same_line(imgui.get_window_width() - 200) + + current_mode = f_item.ast_mask.get(full_path, 'hide') + + imgui.push_id(full_path) + if imgui.radio_button("Def", current_mode == 'def'): f_item.ast_mask[full_path] = 'def' + imgui.same_line() + if imgui.radio_button("Sig", current_mode == 'sig'): f_item.ast_mask[full_path] = 'sig' + imgui.same_line() + if imgui.radio_button("Hide", current_mode == 'hide'): f_item.ast_mask[full_path] = 'hide' + imgui.pop_id() + imgui.end_child() + #endregion: LEFT COLUMN (Tree) + + imgui.table_next_column() + + #region: RIGHT COLUMN (Content) --- + imgui.begin_child("ast_content_scroll", imgui.ImVec2(0, 600), True) + if True: + if not hasattr(app, '_cached_ast_file_lines') or not app._cached_ast_file_lines: + imgui.text("No file content loaded.") + else: + draw_list = imgui.get_window_draw_list() + for i, line_text in enumerate(app._cached_ast_file_lines): + line_num = i + 1 + + # Prioritize the most specific node (deepest indent) that covers the line + deepest_node = None + for node in app._cached_ast_nodes: + if node['start_line'] <= line_num <= node['end_line']: + if deepest_node is None or node['indent'] > deepest_node['indent']: deepest_node = node + + mode = 'hide' + if deepest_node: mode = f_item.ast_mask.get(deepest_node['full_path'], 'hide') + + pos = imgui.get_cursor_screen_pos() + line_height = imgui.get_text_line_height() + + if mode == 'def': + # Green, alpha 0.2 + draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(0, 255, 0, 0.2))) + elif mode == 'sig': + # Blue, alpha 0.2 + draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(0, 0, 255, 0.2))) + + imgui.text(f"{line_num:4} | {line_text}") + imgui.end_child() + #endregion: RIGHT COLUMN (Content) --- + imgui.end_table() + #endregion: ast_dual_pane + + imgui.separator() + + if imgui.button("Close", imgui.ImVec2(120, 0)): + app.ui_inspecting_ast_file = None + imgui.close_current_popup() + + imgui.end_popup() + + #endregion: AST Inspector + + if not opened: app.ui_inspecting_ast_file = None + +def render_save_workspace_profile_modal(app: App) -> None: + if app._show_save_workspace_profile_modal: + imgui.open_popup("Save Workspace Profile") + + if imgui.begin_popup_modal("Save Workspace Profile", True, imgui.WindowFlags_.always_auto_resize)[0]: + imgui.text("Name:") + _, app._new_workspace_profile_name = imgui.input_text("##profile_name", app._new_workspace_profile_name) + + imgui.text("Scope:") + if imgui.radio_button("Project", app._new_workspace_profile_scope == "project"): app._new_workspace_profile_scope = "project" + imgui.same_line() + if imgui.radio_button("Global", app._new_workspace_profile_scope == "global"): app._new_workspace_profile_scope = "global" + + imgui.separator() + if imgui.button("Save", (120, 0)): + if app._new_workspace_profile_name.strip(): + app.controller._cb_save_workspace_profile(app._new_workspace_profile_name, app._new_workspace_profile_scope) + app._show_save_workspace_profile_modal = False + imgui.close_current_popup() + + imgui.same_line() + if imgui.button("Cancel", (120, 0)): + app._show_save_workspace_profile_modal = False + imgui.close_current_popup() + + imgui.end_popup() + +def render_context_presets_panel(app: App) -> None: + imgui.text_colored(C_IN, "Context Presets") + imgui.separator() + changed, new_name = imgui.input_text("Preset Name##new_ctx", app.ui_new_context_preset_name) + if changed: app.ui_new_context_preset_name = new_name + imgui.same_line() + if imgui.button("Save Current"): + if app.ui_new_context_preset_name.strip(): + app.save_context_preset(app.ui_new_context_preset_name.strip()) + + imgui.separator() + presets = app.controller.project.get('context_presets', {}) + for name in sorted(presets.keys()): + preset = presets[name] + n_files = len(preset.get('files', [])) + n_shots = len(preset.get('screenshots', [])) + imgui.text(f"{name} ({n_files} files, {n_shots} shots)") + imgui.same_line() + if imgui.button(f"Load##{name}"): app.load_context_preset(name) + imgui.same_line() + if imgui.button(f"Delete##{name}"): app.delete_context_preset(name) + +def render_context_screenshots(app: App) -> None: + for i, s in enumerate(app.screenshots): imgui.text(s) + +def render_context_files_table(app: App) -> None: + imgui.dummy(imgui.ImVec2(0, 4)) + grouped_files = aggregate.group_files_by_dir(app.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(app.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 app.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 app._last_selected_context_index != -1: + start = min(app._last_selected_context_index, i) + end = max(app._last_selected_context_index, i) + for idx in range(start, end + 1): + item = app.context_files[idx] + item_path = item.path if hasattr(item, "path") else str(item) + if is_sel: app.ui_selected_context_files.add(item_path) + else: app.ui_selected_context_files.discard(item_path) + else: + if is_sel: app.ui_selected_context_files.add(f_path) + else: app.ui_selected_context_files.discard(f_path) + app._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 = app._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}"): + app.ui_inspecting_ast_file = f_item + app._show_ast_inspector = True + + imgui.same_line() + if imgui.button(f"[Slices]##{i}"): + app.ui_editing_slices_file = f_item + f_path = f_item.path if hasattr(f_item, "path") else str(f_item) + app.text_viewer_title = f"Slices: {f_path}" + try: + app.text_viewer_content = mcp_client.read_file(f_path) + except Exception as e: + app.text_viewer_content = f"Error reading file: {e}" + app.text_viewer_type = 'cpp' if f_path.endswith(('.cpp', '.hpp', '.h')) else 'python' if f_path.endswith('.py') else 'text' + app.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, app.ui_new_vp_name = imgui.input_text(f"##pname{i}", app.ui_new_vp_name) + if imgui.button("OK"): + if app.ui_new_vp_name.strip(): + app.controller._cb_save_view_preset(app.ui_new_vp_name.strip(), f_item) + app.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 app.controller.view_presets]) + if not vp_names: imgui.text("No presets saved.") + for vp_name in vp_names: + if imgui.selectable(vp_name): + app.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(app: App) -> None: + imgui.text("Presets") + presets = app.controller.project.get('context_presets', {}) + preset_names = [""] + sorted(presets.keys()) + active = getattr(app, "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: + app.ui_active_context_preset = preset_names[new_idx] + if preset_names[new_idx]: app.load_context_preset(preset_names[new_idx]) + imgui.same_line() + changed, new_name = imgui.input_text("##new_preset", getattr(app, "ui_new_context_preset_name", "")) + if changed: app.ui_new_context_preset_name = new_name + imgui.same_line() + if imgui.button("Save##ctx"): + if getattr(app, "ui_new_context_preset_name", "").strip(): + app.save_context_preset(app.ui_new_context_preset_name.strip()) + app.ui_new_context_preset_name = "" + imgui.same_line() + if imgui.button("Delete##ctx"): + if getattr(app, "ui_active_context_preset", ""): + app.delete_context_preset(app.ui_active_context_preset) + app.ui_active_context_preset = "" + +def render_snapshot_tab(app: App) -> None: + if imgui.begin_tab_bar("snapshot_tabs"): + if imgui.begin_tab_item("Aggregate MD")[0]: + display_md = app.last_aggregate_markdown + if app.ui_focus_agent: + tier_usage = app.mma_tier_usage.get(app.ui_focus_agent) + if tier_usage: + persona_name = tier_usage.get("persona") + if persona_name: + persona = app.controller.personas.get(persona_name) + if persona and persona.context_preset: + cp_name = persona.context_preset + if cp_name in app._focus_md_cache: + display_md = app._focus_md_cache[cp_name] + else: + flat = src.project_manager.flat_config(app.controller.project, app.active_discussion) + cp = app.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) + app._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(app.last_resolved_system_prompt) + imgui.begin_child("last_sys_prompt", imgui.ImVec2(0, 0), True) + markdown_helper.render(app.last_resolved_system_prompt, context_id="snapshot_sys") + imgui.end_child() + imgui.end_tab_item() + imgui.end_tab_bar() + #endregion: Context Management #region: Discussions @@ -2639,6 +3326,284 @@ def render_discussion_entry_read_mode(app: App, entry: dict, index: int) -> None markdown_helper.render(after, context_id=f'disc_{index}_a') if app.ui_word_wrap: imgui.pop_text_wrap_pos() +def render_history_window(app: App) -> None: + if not app.show_windows.get('Undo/Redo History', False): + return + def iterate_history(history: typing.List[typing.Dict[str, typing.Any]]) -> None: + for i, entry in enumerate(reversed(history)): + actual_idx = len(history) - 1 - i + desc = entry.get("description", "UI Change") + ts = entry.get("timestamp", 0.0) + ts_str = datetime.datetime.fromtimestamp(ts).strftime("%H:%M:%S") + label = f"[{ts_str}] {desc}##{actual_idx}" + _, selected = imgui.selectable(label, False) + if selected: app._handle_jump_to_history(actual_idx) + with imscope.window("Undo/Redo History", app.show_windows['Undo/Redo History']) as (exp, opened): + app.show_windows['Undo/Redo History'] = bool(opened) + if exp: + if imgui.button("Undo") and app.history.can_undo: app._handle_undo() + imgui.same_line() + if imgui.button("Redo") and app.history.can_redo: app._handle_redo() + imgui.separator() + with imscope.child("history_list", 0, 0, True): + history = app.history.get_history() + if not history: imgui.text("No history available.") + else: iterate_history() + +def render_session_insights_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_session_insights_panel") + imgui.text_colored(C_LBL, 'Session Insights') + imgui.separator() + insights = app.controller.get_session_insights() + imgui.text(f"Total Tokens: {insights.get('total_tokens', 0):,}") + imgui.text(f"API Calls: {insights.get('call_count', 0)}") + imgui.text(f"Burn Rate: {insights.get('burn_rate', 0):.0f} tokens/min") + imgui.text(f"Session Cost: ${insights.get('session_cost', 0):.4f}") + completed = insights.get('completed_tickets', 0) + efficiency = insights.get('efficiency', 0) + imgui.text(f"Completed: {completed}") + imgui.text(f"Tokens/Ticket: {efficiency:.0f}" if efficiency > 0 else "Tokens/Ticket: N/A") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_session_insights_panel") + +def render_prior_session_view(app: App) -> None: + with imscope.style_color(imgui.Col_.child_bg, vec4(50, 40, 20)): + if imgui.button("Exit Prior Session"): app.controller.cb_exit_prior_session(); app._comms_log_dirty = True + imgui.separator() + with imscope.child("prior_scroll"): + clipper = imgui.ListClipper(); clipper.begin(len(app.prior_disc_entries)) + while clipper.step(): + for idx in range(clipper.display_start, clipper.display_end): + entry = app.prior_disc_entries[idx]; + with imscope.id(f"prior_disc_{idx}"): + collapsed = entry.get("collapsed", False) + if imgui.button("+" if collapsed else "-"): entry["collapsed"] = not collapsed + imgui.same_line(); role, ts = entry.get("role", "??"), entry.get("ts", "") + imgui.text_colored(C_LBL, f"[{role}]") + if ts: imgui.same_line(); imgui.text_colored(vec4(160, 160, 160), str(ts)) + content = entry.get("content", "") + if collapsed: + imgui.same_line(); preview = content.replace("\n", " ")[:80] + if len(content) > 80: preview += "..." + imgui.text_colored(vec4(180, 180, 180), preview) + else: + with theme.ai_text_style(): + markdown_helper.render(content, context_id=f'prior_disc_{idx}') + imgui.separator() + +def render_thinking_indicator(app: App) -> None: + is_thinking = app.ai_status in ['sending...', 'streaming...', 'running powershell...'] + if is_thinking: + val = math.sin(time.time() * 10 * math.pi) + alpha = 1.0 if val > 0 else 0.0 + c = vec4(255, 50, 50, alpha) if theme.is_nerv_active() else vec4(255, 100, 100, alpha) + imgui.text_colored(c, "THINKING..."); imgui.same_line() + +def render_synthesis_panel(app: App) -> 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 = app.project.get('discussion', {}).get('discussions', {}) + if not hasattr(app, 'ui_synthesis_selected_takes'): app.ui_synthesis_selected_takes = {name: False for name in discussions} + if not hasattr(app, 'ui_synthesis_prompt'): app.ui_synthesis_prompt = "" + for name in discussions: _, app.ui_synthesis_selected_takes[name] = imgui.checkbox(name, app.ui_synthesis_selected_takes.get(name, False)) + imgui.spacing() + imgui.text("Synthesis Prompt:") + _, app.ui_synthesis_prompt = imgui.input_text_multiline("##synthesis_prompt", app.ui_synthesis_prompt, imgui.ImVec2(-1, 100)) + if imgui.button("Generate Synthesis"): + selected = [name for name, sel in app.ui_synthesis_selected_takes.items() if sel] + if len(selected) > 1: + discussions_dict = app.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"{app.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 + + app._create_discussion(new_name) + with app._disc_entries_lock: app.disc_entries.append({"role": "User", "content": prompt, "collapsed": False, "ts": project_manager.now_ts()}) + app._handle_generate_send() + +def render_comms_history_panel(app: App) -> None: + if app.perf_profiling_enabled: app.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: {app.ai_status}") + imgui.same_line() + if imgui.button("Clear##comms"): + ai_client.clear_comms_log() + app._comms_log.clear() + app._comms_log_dirty = True + if app.is_viewing_prior_session: + imgui.same_line() + if imgui.button("Exit Prior Session"): + app.controller.cb_exit_prior_session() + app._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 app.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 = app._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}") + render_heavy_text(app, "message", payload.get("message", ""), idx_str) + if payload.get("system"): + render_heavy_text(app, "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: render_thinking_trace(app, [{"content": s.content, "marker": s.marker} for s in segments], i, is_standalone=not bool(parsed_response.strip())) + if parsed_response: render_heavy_text(app, "text", parsed_response, idx_str) + + tcs = payload.get("tool_calls", []) + if tcs: render_heavy_text(app, "tool_calls", json.dumps(tcs, indent=1), idx_str) + + elif kind == "tool_call": render_heavy_text(app, payload.get("name", "call"), payload.get("script") or json.dumps(payload.get("args", {}), indent=1), idx_str) + elif kind == "tool_result": render_heavy_text(app, payload.get("name", "result"), payload.get("output", ""), idx_str) + else: render_heavy_text(app, "data", str(payload), idx_str) + + imgui.separator() + imgui.pop_id() + + if app._scroll_comms_to_bottom: + imgui.set_scroll_here_y(1.0) + app._scroll_comms_to_bottom = False + + imgui.end_child() + if app.is_viewing_prior_session: imgui.pop_style_color() + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_comms_history_panel") + +def render_takes_panel(app: App) -> None: + imgui.text("Takes & Synthesis") + imgui.separator() + discussions = app.project.get('discussion', {}).get('discussions', {}) + if not hasattr(app, 'ui_synthesis_selected_takes'): + app.ui_synthesis_selected_takes = {name: False for name in discussions} + if not hasattr(app, 'ui_synthesis_prompt'): + app.ui_synthesis_prompt = "" + if imgui.begin_table("takes_table", 3, imgui.TableFlags_.resizable | imgui.TableFlags_.borders): + imgui.table_setup_column("Name", imgui.TableColumnFlags_.width_stretch) + imgui.table_setup_column("Entries", imgui.TableColumnFlags_.width_fixed, 80) + imgui.table_setup_column("Actions", imgui.TableColumnFlags_.width_fixed, 150) + imgui.table_headers_row() + for name, disc in list(discussions.items()): + imgui.table_next_row() + imgui.table_set_column_index(0) + is_active = name == app.active_discussion + if is_active: + imgui.text_colored(C_IN, name) + else: + imgui.text(name) + imgui.table_set_column_index(1) + history = disc.get('history', []) + imgui.text(f"{len(history)}") + imgui.table_set_column_index(2) + if imgui.button(f"Switch##{name}"): + app._switch_discussion(name) + imgui.same_line() + if name != "main" and imgui.button(f"Delete##{name}"): + del discussions[name] + imgui.end_table() + imgui.separator() + imgui.text("Synthesis") + imgui.text("Select takes to synthesize:") + for name in discussions: + _, app.ui_synthesis_selected_takes[name] = imgui.checkbox(name, app.ui_synthesis_selected_takes.get(name, False)) + imgui.spacing() + imgui.text("Synthesis Prompt:") + _, app.ui_synthesis_prompt = imgui.input_text_multiline("##synthesis_prompt", app.ui_synthesis_prompt, imgui.ImVec2(-1, 100)) + if imgui.button("Generate Synthesis"): + selected = [name for name, sel in app.ui_synthesis_selected_takes.items() if sel] + if len(selected) > 1: + from src import synthesis_formatter + takes_dict = {name: discussions.get(name, {}).get('history', []) for name in selected} + diff_text = synthesis_formatter.format_takes_diff(takes_dict) + prompt = f"{app.ui_synthesis_prompt}\n\nHere are the variations:\n{diff_text}" + new_name = "synthesis_take" + counter = 1 + while new_name in discussions: + new_name = f"synthesis_take_{counter}" + counter += 1 + app._create_discussion(new_name) + with app._disc_entries_lock: + app.disc_entries.append({"role": "user", "content": prompt, "collapsed": False, "ts": project_manager.now_ts()}) + app._handle_generate_send() + def render_discussion_entries(app: App) -> None: with imscope.child("disc_scroll"): display_entries = app.disc_entries @@ -2811,62 +3776,6 @@ def render_discussion_tab(app: App) -> None: else: imgui.text_disabled("Message & Response panels are detached.") -def render_takes_panel(app: App) -> None: - imgui.text("Takes & Synthesis") - imgui.separator() - discussions = app.project.get('discussion', {}).get('discussions', {}) - if not hasattr(app, 'ui_synthesis_selected_takes'): - app.ui_synthesis_selected_takes = {name: False for name in discussions} - if not hasattr(app, 'ui_synthesis_prompt'): - app.ui_synthesis_prompt = "" - if imgui.begin_table("takes_table", 3, imgui.TableFlags_.resizable | imgui.TableFlags_.borders): - imgui.table_setup_column("Name", imgui.TableColumnFlags_.width_stretch) - imgui.table_setup_column("Entries", imgui.TableColumnFlags_.width_fixed, 80) - imgui.table_setup_column("Actions", imgui.TableColumnFlags_.width_fixed, 150) - imgui.table_headers_row() - for name, disc in list(discussions.items()): - imgui.table_next_row() - imgui.table_set_column_index(0) - is_active = name == app.active_discussion - if is_active: - imgui.text_colored(C_IN, name) - else: - imgui.text(name) - imgui.table_set_column_index(1) - history = disc.get('history', []) - imgui.text(f"{len(history)}") - imgui.table_set_column_index(2) - if imgui.button(f"Switch##{name}"): - app._switch_discussion(name) - imgui.same_line() - if name != "main" and imgui.button(f"Delete##{name}"): - del discussions[name] - imgui.end_table() - imgui.separator() - imgui.text("Synthesis") - imgui.text("Select takes to synthesize:") - for name in discussions: - _, app.ui_synthesis_selected_takes[name] = imgui.checkbox(name, app.ui_synthesis_selected_takes.get(name, False)) - imgui.spacing() - imgui.text("Synthesis Prompt:") - _, app.ui_synthesis_prompt = imgui.input_text_multiline("##synthesis_prompt", app.ui_synthesis_prompt, imgui.ImVec2(-1, 100)) - if imgui.button("Generate Synthesis"): - selected = [name for name, sel in app.ui_synthesis_selected_takes.items() if sel] - if len(selected) > 1: - from src import synthesis_formatter - takes_dict = {name: discussions.get(name, {}).get('history', []) for name in selected} - diff_text = synthesis_formatter.format_takes_diff(takes_dict) - prompt = f"{app.ui_synthesis_prompt}\n\nHere are the variations:\n{diff_text}" - new_name = "synthesis_take" - counter = 1 - while new_name in discussions: - new_name = f"synthesis_take_{counter}" - counter += 1 - app._create_discussion(new_name) - with app._disc_entries_lock: - app.disc_entries.append({"role": "user", "content": prompt, "collapsed": False, "ts": project_manager.now_ts()}) - app._handle_generate_send() - #endregion: Discussions #region: Operations Monitor @@ -3099,10 +4008,396 @@ def render_external_tools_panel(app: App) -> None: imgui.end_table() if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_external_tools_panel") +def render_text_viewer_window(app: App) -> None: + """Renders the standalone text/code/markdown viewer window.""" + if not app.show_text_viewer: return + imgui.set_next_window_size(imgui.ImVec2(900, 700), imgui.Cond_.first_use_ever) + expanded, opened = imgui.begin(f"Text Viewer - {app.text_viewer_title}", app.show_text_viewer) + app.show_text_viewer = bool(opened) + if not opened: + app.ui_editing_slices_file = None + app._slice_sel_start = -1 + app._slice_sel_end = -1 + if expanded: + if app.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 app._slice_sel_start != -1 and app._slice_sel_end != -1: + s_line = min(app._slice_sel_start, app._slice_sel_end) + e_line = max(app._slice_sel_start, app._slice_sel_end) + from src.fuzzy_anchor import FuzzyAnchor + slice_data = FuzzyAnchor.create_slice(app.text_viewer_content, s_line, e_line) + slice_data['tag'] = ""; slice_data['comment'] = "" + app.ui_editing_slices_file.custom_slices.append(slice_data) + app._slice_sel_start = -1; app._slice_sel_end = -1 + imgui.same_line() + if imgui.button("Clear Selection"): app._slice_sel_start = -1; app._slice_sel_end = -1 + to_remove = -1 + for idx, slc in enumerate(app.ui_editing_slices_file.custom_slices): + imgui.push_id(f"slc_row_{idx}"); imgui.text(f"Slice {idx+1}: {slc['start_line']}-{slc['end_line']}"); imgui.same_line() + imgui.set_next_item_width(100); changed_tag, new_tag = imgui.input_text("Tag", slc.get('tag', '')) + if changed_tag: slc['tag'] = new_tag + imgui.same_line(); imgui.set_next_item_width(200); changed_comm, new_comm = imgui.input_text("Comment", slc.get('comment', '')) + if changed_comm: slc['comment'] = new_comm + imgui.same_line() + if imgui.button("Remove"): to_remove = idx + imgui.pop_id() + if to_remove != -1: app.ui_editing_slices_file.custom_slices.pop(to_remove) + imgui.separator() + if imgui.button("Copy"): imgui.set_clipboard_text(app.text_viewer_content) + imgui.same_line(); _, app.text_viewer_wrap = imgui.checkbox("Word Wrap", app.text_viewer_wrap) + imgui.separator() + renderer = markdown_helper.get_renderer(); tv_type = getattr(app, "text_viewer_type", "text") + if tv_type == 'markdown': + with imscope.child("tv_md_scroll", -1, -1, True): markdown_helper.render(app.text_viewer_content, context_id='text_viewer') + elif app.ui_editing_slices_file is not None: + with imscope.child("slice_editor_content", -1, -1, True): + lines = app.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 app.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 app._slice_sel_start != -1 and app._slice_sel_end != -1: + s, e = min(app._slice_sel_start, app._slice_sel_end), max(app._slice_sel_start, app._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(): app._slice_sel_start = line_num; app._slice_sel_end = line_num + if imgui.is_item_hovered() and imgui.is_mouse_down(0): app._slice_sel_end = line_num + elif tv_type in renderer._lang_map: + if app._text_viewer_editor is None: + app._text_viewer_editor = ced.TextEditor(); app._text_viewer_editor.set_read_only_enabled(True); app._text_viewer_editor.set_show_line_numbers_enabled(True) + try: + app._text_viewer_editor.set_text(app.text_viewer_content) + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_text_viewer_ced") + app._text_viewer_editor.render(f"##ced_{app.text_viewer_title}", imgui.ImVec2(-1, -1)) + if app.perf_profiling_enabled: app.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(app.text_viewer_content) + else: + with imscope.child("tv_scroll", -1, -1, True): + if app.text_viewer_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) + imgui.text_unformatted(app.text_viewer_content) + if app.text_viewer_wrap: imgui.pop_text_wrap_pos() + imgui.end() + # Sync text and language + + #region: Inject File Modal + if getattr(app, "show_inject_modal", False): + imgui.open_popup("Inject File") + app.show_inject_modal = False + + if imgui.begin_popup_modal("Inject File", None, imgui.WindowFlags_.always_auto_resize)[0]: + files = app.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 = (app._inject_file_path == f_path) + if imgui.selectable(f_path, is_selected)[0]: + app._inject_file_path = f_path + app.controller._update_inject_preview() + imgui.end_child() + imgui.separator() + if imgui.radio_button("Skeleton", app._inject_mode == "skeleton"): + app._inject_mode = "skeleton" + app.controller._update_inject_preview() + imgui.same_line() + if imgui.radio_button("Full", app._inject_mode == "full"): + app._inject_mode = "full" + app.controller._update_inject_preview() + imgui.separator() + imgui.text("Preview:") + imgui.begin_child("inject_preview_area", imgui.ImVec2(600, 300), True) + imgui.text_unformatted(app._inject_preview) + imgui.end_child() + imgui.separator() + if imgui.button("Inject", imgui.ImVec2(120, 0)): + formatted = f"## File: {app._inject_file_path}\n```python\n{app._inject_preview}\n```\n" + with app._disc_entries_lock: + app.disc_entries.append({ + "role": "Context", + "content": formatted, + "collapsed": True, + "ts": project_manager.now_ts() + }) + app._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 + + render_ast_inspector_modal(app) + return + +def render_patch_modal(app: App) -> None: + if not app._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: + 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 app._pending_patch_files: + imgui.text("Files to modify:") + for f in app._pending_patch_files: imgui.text(f" - {f}") + imgui.separator() + if app._patch_error_message: + imgui.text_colored(vec4(255, 77, 77), f"Error: {app._patch_error_message}") + imgui.separator() + imgui.text("Diff Preview:") + imgui.begin_child("patch_diff_scroll", imgui.ImVec2(-1, 280), True) + if app._pending_patch_text: + diff_lines = app._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"): app._open_patch_in_external_editor() + imgui.same_line() + if imgui.button("Apply Patch"): + app._apply_pending_patch() + app._close_vscode_diff() + imgui.same_line() + if imgui.button("Reject"): + app._close_vscode_diff() + app._show_patch_modal = False + app._pending_patch_text = None + app._pending_patch_files = [] + app._patch_error_message = None + imgui.close_current_popup() + +def render_external_editor_panel(app: App) -> 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: app._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(app: App) -> None: + """Renders the modal dialog for approving AI-generated PowerShell scripts.""" + with app._pending_dialog_lock: + dlg = app._pending_dialog + if dlg: + if not app._pending_dialog_open: + imgui.open_popup("Approve PowerShell Command") + app._pending_dialog_open = True + else: + app._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 + _, app.show_text_viewer = imgui.checkbox("Show Full Preview", app.show_text_viewer) + if app.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 app._pending_dialog_lock: app._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 app._pending_dialog_lock: app._pending_dialog = None + imgui.close_current_popup() + imgui.end_popup() + #endregion: Operations Monitor #region: Misc Tools +def render_theme_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_theme_panel") + exp, opened = imgui.begin("Theme", app.show_windows["Theme"]) + app.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) + app._flush_to_config() + models.save_config(app.config) + imgui.end_combo() + + imgui.separator() + ch1, app.ui_separate_message_panel = imgui.checkbox("Separate Message Panel", app.ui_separate_message_panel) + ch2, app.ui_separate_response_panel = imgui.checkbox("Separate Response Panel", app.ui_separate_response_panel) + ch3, app.ui_separate_tool_calls_panel = imgui.checkbox("Separate Tool Calls Panel", app.ui_separate_tool_calls_panel) + if ch1: app.show_windows["Message"] = app.ui_separate_message_panel + if ch2: app.show_windows["Response"] = app.ui_separate_response_panel + if ch3: app.show_windows["Tool Calls"] = app.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)"): + app._flush_to_config() + models.save_config(app.config) + app.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) + app._flush_to_config() + models.save_config(app.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) + app._flush_to_config() + models.save_config(app.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 = app.config.setdefault("gui", {}) + gui_cfg["bg_shader_enabled"] = bg.enabled + app._flush_to_config() + models.save_config(app.config) + + ch_crt, app.ui_crt_filter = imgui.checkbox("CRT Filter", app.ui_crt_filter) + if ch_crt: + gui_cfg = app.config.setdefault("gui", {}) + gui_cfg["crt_filter_enabled"] = app.ui_crt_filter + app._flush_to_config() + models.save_config(app.config) + + imgui.end() + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_theme_panel") + +def render_shader_live_editor(app: App) -> None: + """ + [C: tests/test_shader_live_editor.py:test_shader_live_editor_renders] + """ + if app.show_windows.get('Shader Editor', False): + with imscope.window('Shader Editor', app.show_windows['Shader Editor']) as (exp, opened): + app.show_windows['Shader Editor'] = bool(opened) + if exp: + changed_crt, app.shader_uniforms['crt'] = imgui.slider_float('CRT Curvature', app.shader_uniforms['crt'], 0.0, 2.0) + changed_scan, app.shader_uniforms['scanline'] = imgui.slider_float('Scanline Intensity', app.shader_uniforms['scanline'], 0.0, 1.0) + changed_bloom, app.shader_uniforms['bloom'] = imgui.slider_float('Bloom Threshold', app.shader_uniforms['bloom'], 0.0, 1.0) + +def render_markdown_test(app: App) -> 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) + +def render_error_tint(app: App) -> None: + """Renders a red tint overlay if hot reload failed.""" + if not HotReloader.is_error_state: return + draw_list = imgui.get_background_draw_list() + display_size = imgui.get_io().display_size + # Translucent red: (1.0, 0.0, 0.0, 0.2) + tint_col = imgui.get_color_u32(imgui.ImVec4(1.0, 0.0, 0.0, 0.2)) + draw_list.add_rect_filled(imgui.ImVec2(0, 0), display_size, tint_col) + if app.perf_profiling_enabled: + imgui.set_next_window_pos(imgui.ImVec2(10, 50)) + with imscope.window("Hot Reload Error", None, imgui.WindowFlags_.always_auto_resize | imgui.WindowFlags_.no_title_bar): + imgui.text_colored(imgui.ImVec4(1, 0, 0, 1), "HOT RELOAD ERROR") + imgui.text_wrapped(HotReloader.last_error or "Unknown error") + def render_text_viewer(app: App, label: str, content: str, text_type: str = 'text', force_open: bool = False) -> None: if imgui.button("[+]##" + str(id(content))) or force_open: app.text_viewer_type = text_type @@ -3596,1313 +4891,6 @@ def render_track_proposal_modal(app: App) -> None: imgui.close_current_popup() imgui.end_popup() -def render_mma_focus_selector(app: App) -> None: - """ - [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] - """ - imgui.text("Focus Agent:"); imgui.same_line() - focus_label = app.ui_focus_agent or "All" - if imgui.begin_combo("##focus_agent", focus_label, imgui.ComboFlags_.width_fit_preview): - if imgui.selectable("All", app.ui_focus_agent is None)[0]: app.ui_focus_agent = None - for tier in ["Tier 2", "Tier 3", "Tier 4"]: - if imgui.selectable(tier, app.ui_focus_agent == tier)[0]: app.ui_focus_agent = tier - imgui.end_combo() - imgui.same_line() - if app.ui_focus_agent and imgui.button("x##clear_focus"): app.ui_focus_agent = None - -#endregion: MMA - -def render_session_insights_panel(app: App) -> None: - if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_session_insights_panel") - imgui.text_colored(C_LBL, 'Session Insights') - imgui.separator() - insights = app.controller.get_session_insights() - imgui.text(f"Total Tokens: {insights.get('total_tokens', 0):,}") - imgui.text(f"API Calls: {insights.get('call_count', 0)}") - imgui.text(f"Burn Rate: {insights.get('burn_rate', 0):.0f} tokens/min") - imgui.text(f"Session Cost: ${insights.get('session_cost', 0):.4f}") - completed = insights.get('completed_tickets', 0) - efficiency = insights.get('efficiency', 0) - imgui.text(f"Completed: {completed}") - imgui.text(f"Tokens/Ticket: {efficiency:.0f}" if efficiency > 0 else "Tokens/Ticket: N/A") - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_session_insights_panel") - -def render_projects_panel(app: App) -> None: - if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_projects_panel") - proj_name = app.project.get("project", {}).get("name", Path(app.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(app.ui_project_execution_mode) if app.ui_project_execution_mode in modes else 0 - ch, new_idx = imgui.combo("##exec_mode", current_idx, modes) - if ch: app.ui_project_execution_mode = modes[new_idx] - imgui.separator() - imgui.text("Git Directory") - ch, app.ui_project_git_dir = imgui.input_text("##git_dir", app.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: app.ui_project_git_dir = d - imgui.separator() - imgui.text("Output Dir") - ch, app.ui_output_dir = imgui.input_text("##out_dir", app.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: app.ui_output_dir = d - imgui.separator() - imgui.text("Conductor Directory") - ch, app.ui_project_conductor_dir = imgui.input_text("##cond_dir", app.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: app.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(app.project_paths): - is_active = (pp == app.active_project_path) - if imgui.button(f"x##p{i}"): - removed = app.project_paths.pop(i) - if removed == app.active_project_path and app.project_paths: app._switch_project(app.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}"): app._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 app.project_paths: - app.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 app.project_paths: app.project_paths.append(p) - app._switch_project(p) - imgui.same_line() - if imgui.button("Save All"): - app._flush_to_project() - app._flush_to_config() - models.save_config(app.config) - app.ai_status = "config saved" - ch, app.ui_word_wrap = imgui.checkbox("Word-Wrap (Read-only panels)", app.ui_word_wrap) - ch, app.ui_auto_scroll_comms = imgui.checkbox("Auto-scroll Comms History", app.ui_auto_scroll_comms) - ch, app.ui_auto_scroll_tool_calls = imgui.checkbox("Auto-scroll Tool History", app.ui_auto_scroll_tool_calls) - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_projects_panel") - -def render_paths_panel(app: App) -> None: - if app.perf_profiling_enabled: app.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(app, attr) - changed, new_val = imgui.input_text(f"##{key}", val) - if imgui.is_item_hovered(): imgui.set_tooltip(tooltip) - if changed: setattr(app, 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(app, attr, d) - -def render_rag_panel(app: App) -> None: - conf = app.controller.rag_config - if not conf: return - ch, conf.enabled = imgui.checkbox("Enable RAG", conf.enabled) - - imgui.text("Vector Store Provider") - providers = ['chroma', 'qdrant', 'mock'] - try: - idx = providers.index(conf.vector_store.provider) - except (ValueError, AttributeError): - idx = 0 - ch2, next_idx = imgui.combo("##rag_provider", idx, providers) - if ch2: - conf.vector_store.provider = providers[next_idx] - - imgui.text("Embedding Provider") - emb_providers = ['gemini', 'local'] - try: - idx_e = emb_providers.index(conf.embedding_provider) - except (ValueError, AttributeError): - idx_e = 0 - ch3, next_idx_e = imgui.combo("##rag_emb_provider", idx_e, emb_providers) - if ch3: - conf.embedding_provider = emb_providers[next_idx_e] - - imgui.text("Chunk Size") - imgui.set_next_item_width(150) - ch4, conf.chunk_size = imgui.input_int("##rag_chunk_size", conf.chunk_size) - imgui.text("Chunk Overlap") - imgui.set_next_item_width(150) - ch5, conf.chunk_overlap = imgui.input_int("##rag_chunk_overlap", conf.chunk_overlap) - - imgui.separator() - imgui.text(f"Status: {app.controller.rag_status}") - - if imgui.button("Rebuild Index"): app.controller.event_queue.put('click', 'btn_rebuild_rag_index') - -def render_system_prompts_panel(app: App) -> None: - imgui.text("Global System Prompt (all projects)") - preset_names = sorted(app.controller.presets.keys()) - current_global = app.controller.ui_global_preset_name or "Select Preset..." - imgui.set_next_item_width(200) - if imgui.begin_combo("##global_preset", current_global): - for name in preset_names: - is_sel = (name == current_global) - if imgui.selectable(name, is_sel)[0]: app.controller._apply_preset(name, "global") - if is_sel: imgui.set_item_default_focus() - imgui.end_combo() - imgui.same_line(0, 8) - if imgui.button("Manage Presets##global"): app.show_preset_manager_window = True - imgui.set_item_tooltip("Open preset management modal") - ch, app.ui_global_system_prompt = imgui.input_text_multiline("##gsp", app.ui_global_system_prompt, imgui.ImVec2(-1, 100)) - imgui.separator() - _, app.ui_use_default_base_prompt = imgui.checkbox("Use Default Base System Prompt", app.ui_use_default_base_prompt) - imgui.same_line() - if imgui.button("Reset to Default##btn_reset_base_prompt"): app.controller._cb_reset_base_prompt() - imgui.same_line() - if imgui.button("Show Diff##btn_show_base_prompt_diff"): app.controller._cb_show_base_prompt_diff() - imgui.set_item_tooltip("Compare current base prompt with the default.") - - imgui.same_line() - imgui.text_disabled("(?)") - imgui.set_item_tooltip("The Base System Prompt contains foundational instructions for the AI, including its role as a coding assistant and safety guidelines. You can override it here if needed.") - - header_flags = imgui.TreeNodeFlags_.default_open if not app.ui_use_default_base_prompt else 0 - if imgui.collapsing_header("Base System Prompt (foundational instructions)", header_flags): - if app.ui_use_default_base_prompt: - imgui.begin_disabled() - imgui.input_text_multiline("##base_prompt_def", ai_client._SYSTEM_PROMPT, imgui.ImVec2(-1, 100), imgui.InputTextFlags_.read_only) - imgui.end_disabled() - imgui.text_disabled(f"Characters: {len(ai_client._SYSTEM_PROMPT)}") - else: - ch, app.ui_base_system_prompt = imgui.input_text_multiline("##base_prompt", app.ui_base_system_prompt, imgui.ImVec2(-1, 150)) - imgui.text_disabled(f"Characters: {len(app.ui_base_system_prompt)}") - imgui.separator() - imgui.text("Project System Prompt") - current_project = app.controller.ui_project_preset_name or "Select Preset..." - imgui.set_next_item_width(200) - if imgui.begin_combo("##project_preset", current_project): - for name in preset_names: - is_sel = (name == current_project) - if imgui.selectable(name, is_sel)[0]: app.controller._apply_preset(name, "project") - if is_sel: imgui.set_item_default_focus() - imgui.end_combo() - imgui.same_line(0, 8) - if imgui.button("Manage Presets##project"): app.show_preset_manager_window = True - imgui.set_item_tooltip("Open preset management modal") - ch, app.ui_project_system_prompt = imgui.input_text_multiline("##psp", app.ui_project_system_prompt, imgui.ImVec2(-1, 100)) - -def render_agent_tools_panel(app: App) -> None: - if imgui.collapsing_header("Active Tool Presets & Biases", imgui.TreeNodeFlags_.default_open): - imgui.text("Tool Preset") - presets = app.controller.tool_presets - preset_names = [""] + sorted(list(presets.keys())) - - active = getattr(app, "ui_active_tool_preset", "") - if active is None: active = "" - try: - idx = preset_names.index(active) - except ValueError: - idx = 0 - - ch, new_idx = imgui.combo("##tool_preset_select", idx, preset_names) - if ch: - app.ui_active_tool_preset = preset_names[new_idx] - - imgui.same_line() - if imgui.button("Manage Presets##tools"): app.show_tool_preset_manager_window = True - if imgui.is_item_hovered(): imgui.set_tooltip("Configure tool availability and default modes.") - - imgui.dummy(imgui.ImVec2(0, 4)) - imgui.text("Bias Profile") - if imgui.begin_combo("##bias", getattr(app, 'ui_active_bias_profile', "") or "None"): - if imgui.selectable("None", not getattr(app, 'ui_active_bias_profile', ""))[0]: - app.ui_active_bias_profile = "" - ai_client.set_bias_profile(None) - for bname in sorted(app.controller.bias_profiles.keys()): - if not bname: continue - if imgui.selectable(bname, bname == getattr(app, 'ui_active_bias_profile', ""))[0]: - app.ui_active_bias_profile = bname - ai_client.set_bias_profile(bname) - imgui.end_combo() - - imgui.dummy(imgui.ImVec2(0, 8)) - cat_options = ["All"] + sorted(list(models.DEFAULT_TOOL_CATEGORIES.keys())) - try: - f_idx = cat_options.index(app.ui_tool_filter_category) - except ValueError: - f_idx = 0 - imgui.set_next_item_width(200) - ch_cat, next_f_idx = imgui.combo("Filter Category##agent", f_idx, cat_options) - if ch_cat: app.ui_tool_filter_category = cat_options[next_f_idx] - - imgui.dummy(imgui.ImVec2(0, 8)) - active_name = app.ui_active_tool_preset - if active_name and active_name in presets: - preset = presets[active_name] - for cat_name, tools in preset.categories.items(): - if app.ui_tool_filter_category != "All" and app.ui_tool_filter_category != cat_name: continue - if imgui.tree_node(cat_name): - for tool in tools: - if tool.weight >= 5: imgui.text_colored(vec4(255, 100, 100), "[HIGH]"); imgui.same_line() - elif tool.weight == 4: imgui.text_colored(vec4(255, 255, 100), "[PREF]"); imgui.same_line() - elif tool.weight == 2: imgui.text_colored(vec4(255, 150, 50), "[REJECT]"); imgui.same_line() - elif tool.weight <= 1: imgui.text_colored(vec4(180, 180, 180), "[LOW]"); imgui.same_line() - - imgui.text(tool.name); imgui.same_line(180) - - mode = tool.approval - if imgui.radio_button(f"Auto##{cat_name}_{tool.name}", mode == "auto"): tool.approval = "auto" - imgui.same_line() - if imgui.radio_button(f"Ask##{cat_name}_{tool.name}", mode == "ask"): tool.approval = "ask" - imgui.tree_pop() - -def render_provider_panel(app: App) -> None: - if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_provider_panel") - imgui.text("Provider") - if imgui.begin_combo("##prov", app.current_provider): - for p in models.PROVIDERS: - if imgui.selectable(p, p == app.current_provider)[0]: - app.current_provider = p - imgui.end_combo() - imgui.separator() - imgui.text("Model") - if imgui.begin_list_box("##models", imgui.ImVec2(-1, 120)): - for m in app.available_models: - if imgui.selectable(m, m == app.current_model)[0]: - app.current_model = m - imgui.end_list_box() - imgui.separator() - imgui.text("Parameters") - # Temperature - imgui.push_id("temp") - imgui.set_next_item_width(imgui.get_content_region_avail().x * 0.6) - _, app.temperature = imgui.slider_float("##slider", app.temperature, 0.0, 2.0, "%.2f") - imgui.same_line() - imgui.set_next_item_width(-1) - _, app.temperature = imgui.input_float("Temp", app.temperature, 0.0, 0.0, "%.2f") - imgui.pop_id() - - # Top-P - imgui.push_id("top_p") - imgui.set_next_item_width(imgui.get_content_region_avail().x * 0.6) - _, app.top_p = imgui.slider_float("##slider", app.top_p, 0.0, 1.0, "%.2f") - imgui.same_line() - imgui.set_next_item_width(-1) - _, app.top_p = imgui.input_float("Top-P", app.top_p, 0.0, 0.0, "%.2f") - imgui.pop_id() - - # Max Tokens - imgui.push_id("max_tokens") - imgui.set_next_item_width(imgui.get_content_region_avail().x * 0.6) - _, app.max_tokens = imgui.slider_int("##slider", app.max_tokens, 1, 32768) - imgui.same_line() - imgui.set_next_item_width(-1) - _, app.max_tokens = imgui.input_int("MaxTok", app.max_tokens) - imgui.pop_id() - - ch, app.history_trunc_limit = imgui.input_int("History Truncation Limit", app.history_trunc_limit, 1024) - - if app.current_provider == "gemini_cli": - imgui.separator() - imgui.text("Gemini CLI") - sid = "None" - if hasattr(ai_client, "_gemini_cli_adapter") and ai_client._gemini_cli_adapter: sid = ai_client._gemini_cli_adapter.session_id or "None" - imgui.text("Session ID:"); imgui.same_line(); render_selectable_label(app, "gemini_cli_sid", sid, width=200) - if imgui.button("Reset CLI Session"): ai_client.reset_session() - imgui.text("Binary Path") - ch, app.ui_gemini_cli_path = imgui.input_text("##gcli_path", app.ui_gemini_cli_path) - imgui.same_line() - if imgui.button("Browse##gcli"): - r = hide_tk_root() - p = filedialog.askopenfilename(title="Select gemini CLI binary") - r.destroy() - if p: app.ui_gemini_cli_path = p - if ch: - if hasattr(ai_client, "_gemini_cli_adapter") and ai_client._gemini_cli_adapter: - ai_client._gemini_cli_adapter.binary_path = app.ui_gemini_cli_path - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_provider_panel") - -def render_persona_selector_panel(app: App) -> None: - if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_persona_selector_panel") - imgui.text("Persona") - if not hasattr(app, 'ui_active_persona'): app.ui_active_persona = "" - personas = getattr(app.controller, 'personas', {}) - if imgui.begin_combo("##persona", app.ui_active_persona or "None"): - if imgui.selectable("None", not app.ui_active_persona)[0]: app.ui_active_persona = "" - for pname in sorted(personas.keys()): - if not pname: continue - if imgui.selectable(pname, pname == app.ui_active_persona)[0]: - app.ui_active_persona = pname - if pname in personas: - persona = personas[pname] - app._editing_persona_name = persona.name - app._editing_persona_system_prompt = persona.system_prompt or "" - app._editing_persona_tool_preset_id = persona.tool_preset or "" - app._editing_persona_bias_profile_id = persona.bias_profile or "" - app._editing_persona_context_preset_id = getattr(persona, 'context_preset', '') or "" - app._editing_persona_aggregation_strategy = getattr(persona, 'aggregation_strategy', '') or "" - app._editing_persona_preferred_models_list = copy.deepcopy(persona.preferred_models) if persona.preferred_models else [] - app._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"): - app.current_provider = first_model.get("provider") - if first_model.get("model"): - app.current_model = first_model.get("model") - if first_model.get("temperature") is not None: - ai_client.temperature = first_model.get("temperature") - app.temperature = first_model.get("temperature") - if first_model.get("max_output_tokens"): - ai_client.max_output_tokens = first_model.get("max_output_tokens") - app.max_tokens = first_model.get("max_output_tokens") - if first_model.get("history_trunc_limit"): - app.history_trunc_limit = first_model.get("history_trunc_limit") - - if persona.system_prompt: app.ui_project_system_prompt = persona.system_prompt - if persona.tool_preset: - app.ui_active_tool_preset = persona.tool_preset - ai_client.set_tool_preset(persona.tool_preset) - if persona.bias_profile: - app.ui_active_bias_profile = persona.bias_profile - ai_client.set_bias_profile(persona.bias_profile) - if getattr(persona, 'context_preset', None): - app.ui_active_context_preset = persona.context_preset - app.load_context_preset(persona.context_preset) - imgui.end_combo() - imgui.same_line() - if imgui.button("Manage Personas"): - app.show_persona_editor_window = True - if app.ui_active_persona and app.ui_active_persona in personas: - persona = personas[app.ui_active_persona] - app._editing_persona_name = persona.name - app._editing_persona_system_prompt = persona.system_prompt or "" - app._editing_persona_tool_preset_id = persona.tool_preset or "" - app._editing_persona_bias_profile_id = persona.bias_profile or "" - app._editing_persona_context_preset_id = getattr(persona, 'context_preset', '') or "" - app._editing_persona_aggregation_strategy = getattr(persona, 'aggregation_strategy', '') or "" - app._editing_persona_preferred_models_list = copy.deepcopy(persona.preferred_models) if persona.preferred_models else [] - app._editing_persona_scope = app.controller.persona_manager.get_persona_scope(persona.name) - app._editing_persona_is_new = False - else: - app._editing_persona_name = "" - app._editing_persona_system_prompt = "" - app._editing_persona_tool_preset_id = "" - app._editing_persona_bias_profile_id = "" - app._editing_persona_context_preset_id = "" - app._editing_persona_aggregation_strategy = "" - app._editing_persona_preferred_models_list = [{ - "provider": app.current_provider, - "model": app.current_model, - "temperature": getattr(app, "temperature", 0.7), - "max_output_tokens": getattr(app, "max_tokens", 4096), - "history_trunc_limit": getattr(app, "history_trunc_limit", 900000) - }] - app._editing_persona_scope = "project" - app._editing_persona_is_new = True - imgui.separator() - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_persona_selector_panel") - -def render_context_composition_panel(app: App) -> None: - """ - [C: tests/test_auto_slices.py:test_add_all_triggers_auto_slices, tests/test_gui_fast_render.py:test_render_context_composition_panel_fast] - """ - if imgui.collapsing_header("Context Composition##panel"): - total_lines, total_ast = app._update_context_file_stats() - render_context_batch_actions(app, total_lines, total_ast) - render_context_files_table(app) - - imgui.separator() - if imgui.collapsing_header("Screenshots"): - render_context_screenshots(app) - imgui.separator() - render_context_presets(app) - -def render_ast_inspector_modal(app: App) -> None: - """ - [C: tests/test_ast_inspector_extended.py:test_ast_inspector_line_range_parsing] - """ - if app._show_ast_inspector: - imgui.open_popup('AST Inspector') - app._show_ast_inspector = False - - #region: AST Inspector - expanded, opened = imgui.begin_popup_modal('AST Inspector', True, imgui.WindowFlags_.always_auto_resize) - if opened: - if expanded: - if app.ui_inspecting_ast_file is None: - imgui.close_current_popup() - else: - f_item = app.ui_inspecting_ast_file - f_path = f_item.path if hasattr(f_item, "path") else str(f_item) - - if f_path != app._cached_ast_file_path: - outline = "" - try: - if f_path.lower().endswith('.py'): outline = mcp_client.py_get_code_outline(f_path) - elif f_path.lower().endswith(('.c', '.h')): outline = mcp_client.ts_c_get_code_outline(f_path) - else: outline = mcp_client.ts_cpp_get_code_outline(f_path) - except Exception as e: - outline = f"Error fetching outline: {e}" - - app._cached_ast_nodes = [] - import re - pattern = re.compile(r'^(\s*)\[(.*?)\] (.*?) \(Lines (\d+)-(\d+)\)') - stack = [] # (indent, name) - for line in outline.splitlines(): - m = pattern.match(line) - if m: - indent_str, kind, name, start_ln, end_ln = m.groups() - indent = len(indent_str) - while stack and stack[-1][0] >= indent: stack.pop() - stack.append((indent, name)) - full_path = '::'.join([s[1] for s in stack]) - app._cached_ast_nodes.append({ - 'indent': indent, - 'kind': kind, - 'name': name, - 'full_path': full_path, - 'start_line': int(start_ln), - 'end_line': int(end_ln) - }) - try: - content = mcp_client.read_file(f_path) - app._cached_ast_file_lines = content.splitlines() - except Exception: - app._cached_ast_file_lines = ["Error loading file content."] - app._cached_ast_file_path = f_path - - imgui.text(f"Inspecting AST: {f_path}") - imgui.separator() - - #region: ast_dual_pane - if imgui.begin_table('ast_dual_pane', 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v): - imgui.table_next_column() - - #region: LEFT COLUMN (Tree) --- - imgui.begin_child("ast_tree_scroll", imgui.ImVec2(0, 600), True) - if True: - if not app._cached_ast_nodes: imgui.text("No AST nodes found or error fetching outline.") - else: - for node in app._cached_ast_nodes: - indent = node['indent'] - kind = node['kind'] - name = node['name'] - full_path = node['full_path'] - - imgui.dummy(imgui.ImVec2(indent * 10, 0)) - imgui.same_line() - imgui.text(f"[{kind}] {name}") - imgui.same_line(imgui.get_window_width() - 200) - - current_mode = f_item.ast_mask.get(full_path, 'hide') - - imgui.push_id(full_path) - if imgui.radio_button("Def", current_mode == 'def'): f_item.ast_mask[full_path] = 'def' - imgui.same_line() - if imgui.radio_button("Sig", current_mode == 'sig'): f_item.ast_mask[full_path] = 'sig' - imgui.same_line() - if imgui.radio_button("Hide", current_mode == 'hide'): f_item.ast_mask[full_path] = 'hide' - imgui.pop_id() - imgui.end_child() - #endregion: LEFT COLUMN (Tree) - - imgui.table_next_column() - - #region: RIGHT COLUMN (Content) --- - imgui.begin_child("ast_content_scroll", imgui.ImVec2(0, 600), True) - if True: - if not hasattr(app, '_cached_ast_file_lines') or not app._cached_ast_file_lines: - imgui.text("No file content loaded.") - else: - draw_list = imgui.get_window_draw_list() - for i, line_text in enumerate(app._cached_ast_file_lines): - line_num = i + 1 - - # Prioritize the most specific node (deepest indent) that covers the line - deepest_node = None - for node in app._cached_ast_nodes: - if node['start_line'] <= line_num <= node['end_line']: - if deepest_node is None or node['indent'] > deepest_node['indent']: deepest_node = node - - mode = 'hide' - if deepest_node: mode = f_item.ast_mask.get(deepest_node['full_path'], 'hide') - - pos = imgui.get_cursor_screen_pos() - line_height = imgui.get_text_line_height() - - if mode == 'def': - # Green, alpha 0.2 - draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(0, 255, 0, 0.2))) - elif mode == 'sig': - # Blue, alpha 0.2 - draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(0, 0, 255, 0.2))) - - imgui.text(f"{line_num:4} | {line_text}") - imgui.end_child() - #endregion: RIGHT COLUMN (Content) --- - imgui.end_table() - #endregion: ast_dual_pane - - imgui.separator() - - if imgui.button("Close", imgui.ImVec2(120, 0)): - app.ui_inspecting_ast_file = None - imgui.close_current_popup() - - imgui.end_popup() - - #endregion: AST Inspector - - if not opened: app.ui_inspecting_ast_file = None - -def render_save_workspace_profile_modal(app: App) -> None: - if app._show_save_workspace_profile_modal: - imgui.open_popup("Save Workspace Profile") - - if imgui.begin_popup_modal("Save Workspace Profile", True, imgui.WindowFlags_.always_auto_resize)[0]: - imgui.text("Name:") - _, app._new_workspace_profile_name = imgui.input_text("##profile_name", app._new_workspace_profile_name) - - imgui.text("Scope:") - if imgui.radio_button("Project", app._new_workspace_profile_scope == "project"): app._new_workspace_profile_scope = "project" - imgui.same_line() - if imgui.radio_button("Global", app._new_workspace_profile_scope == "global"): app._new_workspace_profile_scope = "global" - - imgui.separator() - if imgui.button("Save", (120, 0)): - if app._new_workspace_profile_name.strip(): - app.controller._cb_save_workspace_profile(app._new_workspace_profile_name, app._new_workspace_profile_scope) - app._show_save_workspace_profile_modal = False - imgui.close_current_popup() - - imgui.same_line() - if imgui.button("Cancel", (120, 0)): - app._show_save_workspace_profile_modal = False - imgui.close_current_popup() - - imgui.end_popup() - -def render_context_presets_panel(app: App) -> None: - imgui.text_colored(C_IN, "Context Presets") - imgui.separator() - changed, new_name = imgui.input_text("Preset Name##new_ctx", app.ui_new_context_preset_name) - if changed: app.ui_new_context_preset_name = new_name - imgui.same_line() - if imgui.button("Save Current"): - if app.ui_new_context_preset_name.strip(): - app.save_context_preset(app.ui_new_context_preset_name.strip()) - - imgui.separator() - presets = app.controller.project.get('context_presets', {}) - for name in sorted(presets.keys()): - preset = presets[name] - n_files = len(preset.get('files', [])) - n_shots = len(preset.get('screenshots', [])) - imgui.text(f"{name} ({n_files} files, {n_shots} shots)") - imgui.same_line() - if imgui.button(f"Load##{name}"): app.load_context_preset(name) - imgui.same_line() - if imgui.button(f"Delete##{name}"): app.delete_context_preset(name) - -def render_context_screenshots(app: App) -> None: - for i, s in enumerate(app.screenshots): imgui.text(s) - -def render_context_files_table(app: App) -> None: - imgui.dummy(imgui.ImVec2(0, 4)) - grouped_files = aggregate.group_files_by_dir(app.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(app.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 app.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 app._last_selected_context_index != -1: - start = min(app._last_selected_context_index, i) - end = max(app._last_selected_context_index, i) - for idx in range(start, end + 1): - item = app.context_files[idx] - item_path = item.path if hasattr(item, "path") else str(item) - if is_sel: app.ui_selected_context_files.add(item_path) - else: app.ui_selected_context_files.discard(item_path) - else: - if is_sel: app.ui_selected_context_files.add(f_path) - else: app.ui_selected_context_files.discard(f_path) - app._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 = app._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}"): - app.ui_inspecting_ast_file = f_item - app._show_ast_inspector = True - - imgui.same_line() - if imgui.button(f"[Slices]##{i}"): - app.ui_editing_slices_file = f_item - f_path = f_item.path if hasattr(f_item, "path") else str(f_item) - app.text_viewer_title = f"Slices: {f_path}" - try: - app.text_viewer_content = mcp_client.read_file(f_path) - except Exception as e: - app.text_viewer_content = f"Error reading file: {e}" - app.text_viewer_type = 'cpp' if f_path.endswith(('.cpp', '.hpp', '.h')) else 'python' if f_path.endswith('.py') else 'text' - app.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, app.ui_new_vp_name = imgui.input_text(f"##pname{i}", app.ui_new_vp_name) - if imgui.button("OK"): - if app.ui_new_vp_name.strip(): - app.controller._cb_save_view_preset(app.ui_new_vp_name.strip(), f_item) - app.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 app.controller.view_presets]) - if not vp_names: imgui.text("No presets saved.") - for vp_name in vp_names: - if imgui.selectable(vp_name): - app.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(app: App) -> None: - imgui.text("Presets") - presets = app.controller.project.get('context_presets', {}) - preset_names = [""] + sorted(presets.keys()) - active = getattr(app, "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: - app.ui_active_context_preset = preset_names[new_idx] - if preset_names[new_idx]: app.load_context_preset(preset_names[new_idx]) - imgui.same_line() - changed, new_name = imgui.input_text("##new_preset", getattr(app, "ui_new_context_preset_name", "")) - if changed: app.ui_new_context_preset_name = new_name - imgui.same_line() - if imgui.button("Save##ctx"): - if getattr(app, "ui_new_context_preset_name", "").strip(): - app.save_context_preset(app.ui_new_context_preset_name.strip()) - app.ui_new_context_preset_name = "" - imgui.same_line() - if imgui.button("Delete##ctx"): - if getattr(app, "ui_active_context_preset", ""): - app.delete_context_preset(app.ui_active_context_preset) - app.ui_active_context_preset = "" - -def render_prior_session_view(app: App) -> None: - with imscope.style_color(imgui.Col_.child_bg, vec4(50, 40, 20)): - if imgui.button("Exit Prior Session"): app.controller.cb_exit_prior_session(); app._comms_log_dirty = True - imgui.separator() - with imscope.child("prior_scroll"): - clipper = imgui.ListClipper(); clipper.begin(len(app.prior_disc_entries)) - while clipper.step(): - for idx in range(clipper.display_start, clipper.display_end): - entry = app.prior_disc_entries[idx]; - with imscope.id(f"prior_disc_{idx}"): - collapsed = entry.get("collapsed", False) - if imgui.button("+" if collapsed else "-"): entry["collapsed"] = not collapsed - imgui.same_line(); role, ts = entry.get("role", "??"), entry.get("ts", "") - imgui.text_colored(C_LBL, f"[{role}]") - if ts: imgui.same_line(); imgui.text_colored(vec4(160, 160, 160), str(ts)) - content = entry.get("content", "") - if collapsed: - imgui.same_line(); preview = content.replace("\n", " ")[:80] - if len(content) > 80: preview += "..." - imgui.text_colored(vec4(180, 180, 180), preview) - else: - with theme.ai_text_style(): - markdown_helper.render(content, context_id=f'prior_disc_{idx}') - imgui.separator() - -def render_thinking_indicator(app: App) -> None: - is_thinking = app.ai_status in ['sending...', 'streaming...', 'running powershell...'] - if is_thinking: - val = math.sin(time.time() * 10 * math.pi) - alpha = 1.0 if val > 0 else 0.0 - c = vec4(255, 50, 50, alpha) if theme.is_nerv_active() else vec4(255, 100, 100, alpha) - imgui.text_colored(c, "THINKING..."); imgui.same_line() - -def render_synthesis_panel(app: App) -> 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 = app.project.get('discussion', {}).get('discussions', {}) - if not hasattr(app, 'ui_synthesis_selected_takes'): app.ui_synthesis_selected_takes = {name: False for name in discussions} - if not hasattr(app, 'ui_synthesis_prompt'): app.ui_synthesis_prompt = "" - for name in discussions: _, app.ui_synthesis_selected_takes[name] = imgui.checkbox(name, app.ui_synthesis_selected_takes.get(name, False)) - imgui.spacing() - imgui.text("Synthesis Prompt:") - _, app.ui_synthesis_prompt = imgui.input_text_multiline("##synthesis_prompt", app.ui_synthesis_prompt, imgui.ImVec2(-1, 100)) - if imgui.button("Generate Synthesis"): - selected = [name for name, sel in app.ui_synthesis_selected_takes.items() if sel] - if len(selected) > 1: - discussions_dict = app.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"{app.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 - - app._create_discussion(new_name) - with app._disc_entries_lock: app.disc_entries.append({"role": "User", "content": prompt, "collapsed": False, "ts": project_manager.now_ts()}) - app._handle_generate_send() - -def render_snapshot_tab(app: App) -> None: - if imgui.begin_tab_bar("snapshot_tabs"): - if imgui.begin_tab_item("Aggregate MD")[0]: - display_md = app.last_aggregate_markdown - if app.ui_focus_agent: - tier_usage = app.mma_tier_usage.get(app.ui_focus_agent) - if tier_usage: - persona_name = tier_usage.get("persona") - if persona_name: - persona = app.controller.personas.get(persona_name) - if persona and persona.context_preset: - cp_name = persona.context_preset - if cp_name in app._focus_md_cache: - display_md = app._focus_md_cache[cp_name] - else: - flat = src.project_manager.flat_config(app.controller.project, app.active_discussion) - cp = app.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) - app._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(app.last_resolved_system_prompt) - imgui.begin_child("last_sys_prompt", imgui.ImVec2(0, 0), True) - markdown_helper.render(app.last_resolved_system_prompt, context_id="snapshot_sys") - imgui.end_child() - imgui.end_tab_item() - imgui.end_tab_bar() - -def render_comms_history_panel(app: App) -> None: - if app.perf_profiling_enabled: app.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: {app.ai_status}") - imgui.same_line() - if imgui.button("Clear##comms"): - ai_client.clear_comms_log() - app._comms_log.clear() - app._comms_log_dirty = True - if app.is_viewing_prior_session: - imgui.same_line() - if imgui.button("Exit Prior Session"): - app.controller.cb_exit_prior_session() - app._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 app.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 = app._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}") - render_heavy_text(app, "message", payload.get("message", ""), idx_str) - if payload.get("system"): - render_heavy_text(app, "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: render_thinking_trace(app, [{"content": s.content, "marker": s.marker} for s in segments], i, is_standalone=not bool(parsed_response.strip())) - if parsed_response: render_heavy_text(app, "text", parsed_response, idx_str) - - tcs = payload.get("tool_calls", []) - if tcs: render_heavy_text(app, "tool_calls", json.dumps(tcs, indent=1), idx_str) - - elif kind == "tool_call": render_heavy_text(app, payload.get("name", "call"), payload.get("script") or json.dumps(payload.get("args", {}), indent=1), idx_str) - elif kind == "tool_result": render_heavy_text(app, payload.get("name", "result"), payload.get("output", ""), idx_str) - else: render_heavy_text(app, "data", str(payload), idx_str) - - imgui.separator() - imgui.pop_id() - - if app._scroll_comms_to_bottom: - imgui.set_scroll_here_y(1.0) - app._scroll_comms_to_bottom = False - - imgui.end_child() - if app.is_viewing_prior_session: imgui.pop_style_color() - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_comms_history_panel") - -def render_text_viewer_window(app: App) -> None: - """Renders the standalone text/code/markdown viewer window.""" - if not app.show_text_viewer: return - imgui.set_next_window_size(imgui.ImVec2(900, 700), imgui.Cond_.first_use_ever) - expanded, opened = imgui.begin(f"Text Viewer - {app.text_viewer_title}", app.show_text_viewer) - app.show_text_viewer = bool(opened) - if not opened: - app.ui_editing_slices_file = None - app._slice_sel_start = -1 - app._slice_sel_end = -1 - if expanded: - if app.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 app._slice_sel_start != -1 and app._slice_sel_end != -1: - s_line = min(app._slice_sel_start, app._slice_sel_end) - e_line = max(app._slice_sel_start, app._slice_sel_end) - from src.fuzzy_anchor import FuzzyAnchor - slice_data = FuzzyAnchor.create_slice(app.text_viewer_content, s_line, e_line) - slice_data['tag'] = ""; slice_data['comment'] = "" - app.ui_editing_slices_file.custom_slices.append(slice_data) - app._slice_sel_start = -1; app._slice_sel_end = -1 - imgui.same_line() - if imgui.button("Clear Selection"): app._slice_sel_start = -1; app._slice_sel_end = -1 - to_remove = -1 - for idx, slc in enumerate(app.ui_editing_slices_file.custom_slices): - imgui.push_id(f"slc_row_{idx}"); imgui.text(f"Slice {idx+1}: {slc['start_line']}-{slc['end_line']}"); imgui.same_line() - imgui.set_next_item_width(100); changed_tag, new_tag = imgui.input_text("Tag", slc.get('tag', '')) - if changed_tag: slc['tag'] = new_tag - imgui.same_line(); imgui.set_next_item_width(200); changed_comm, new_comm = imgui.input_text("Comment", slc.get('comment', '')) - if changed_comm: slc['comment'] = new_comm - imgui.same_line() - if imgui.button("Remove"): to_remove = idx - imgui.pop_id() - if to_remove != -1: app.ui_editing_slices_file.custom_slices.pop(to_remove) - imgui.separator() - if imgui.button("Copy"): imgui.set_clipboard_text(app.text_viewer_content) - imgui.same_line(); _, app.text_viewer_wrap = imgui.checkbox("Word Wrap", app.text_viewer_wrap) - imgui.separator() - renderer = markdown_helper.get_renderer(); tv_type = getattr(app, "text_viewer_type", "text") - if tv_type == 'markdown': - with imscope.child("tv_md_scroll", -1, -1, True): markdown_helper.render(app.text_viewer_content, context_id='text_viewer') - elif app.ui_editing_slices_file is not None: - with imscope.child("slice_editor_content", -1, -1, True): - lines = app.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 app.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 app._slice_sel_start != -1 and app._slice_sel_end != -1: - s, e = min(app._slice_sel_start, app._slice_sel_end), max(app._slice_sel_start, app._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(): app._slice_sel_start = line_num; app._slice_sel_end = line_num - if imgui.is_item_hovered() and imgui.is_mouse_down(0): app._slice_sel_end = line_num - elif tv_type in renderer._lang_map: - if app._text_viewer_editor is None: - app._text_viewer_editor = ced.TextEditor(); app._text_viewer_editor.set_read_only_enabled(True); app._text_viewer_editor.set_show_line_numbers_enabled(True) - try: - app._text_viewer_editor.set_text(app.text_viewer_content) - if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_text_viewer_ced") - app._text_viewer_editor.render(f"##ced_{app.text_viewer_title}", imgui.ImVec2(-1, -1)) - if app.perf_profiling_enabled: app.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(app.text_viewer_content) - else: - with imscope.child("tv_scroll", -1, -1, True): - if app.text_viewer_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) - imgui.text_unformatted(app.text_viewer_content) - if app.text_viewer_wrap: imgui.pop_text_wrap_pos() - imgui.end() - # Sync text and language - - #region: Inject File Modal - if getattr(app, "show_inject_modal", False): - imgui.open_popup("Inject File") - app.show_inject_modal = False - - if imgui.begin_popup_modal("Inject File", None, imgui.WindowFlags_.always_auto_resize)[0]: - files = app.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 = (app._inject_file_path == f_path) - if imgui.selectable(f_path, is_selected)[0]: - app._inject_file_path = f_path - app.controller._update_inject_preview() - imgui.end_child() - imgui.separator() - if imgui.radio_button("Skeleton", app._inject_mode == "skeleton"): - app._inject_mode = "skeleton" - app.controller._update_inject_preview() - imgui.same_line() - if imgui.radio_button("Full", app._inject_mode == "full"): - app._inject_mode = "full" - app.controller._update_inject_preview() - imgui.separator() - imgui.text("Preview:") - imgui.begin_child("inject_preview_area", imgui.ImVec2(600, 300), True) - imgui.text_unformatted(app._inject_preview) - imgui.end_child() - imgui.separator() - if imgui.button("Inject", imgui.ImVec2(120, 0)): - formatted = f"## File: {app._inject_file_path}\n```python\n{app._inject_preview}\n```\n" - with app._disc_entries_lock: - app.disc_entries.append({ - "role": "Context", - "content": formatted, - "collapsed": True, - "ts": project_manager.now_ts() - }) - app._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 - - render_ast_inspector_modal(app) - return - -def render_base_prompt_diff_modal(app: App) -> None: - if not getattr(app.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 = app.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)): - app.controller._show_base_prompt_diff_modal = False - imgui.close_current_popup() - imgui.end_popup() - -def render_patch_modal(app: App) -> None: - if not app._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: - 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 app._pending_patch_files: - imgui.text("Files to modify:") - for f in app._pending_patch_files: imgui.text(f" - {f}") - imgui.separator() - if app._patch_error_message: - imgui.text_colored(vec4(255, 77, 77), f"Error: {app._patch_error_message}") - imgui.separator() - imgui.text("Diff Preview:") - imgui.begin_child("patch_diff_scroll", imgui.ImVec2(-1, 280), True) - if app._pending_patch_text: - diff_lines = app._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"): app._open_patch_in_external_editor() - imgui.same_line() - if imgui.button("Apply Patch"): - app._apply_pending_patch() - app._close_vscode_diff() - imgui.same_line() - if imgui.button("Reject"): - app._close_vscode_diff() - app._show_patch_modal = False - app._pending_patch_text = None - app._pending_patch_files = [] - app._patch_error_message = None - imgui.close_current_popup() - -def render_external_editor_panel(app: App) -> 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: app._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(app: App) -> None: - """Renders the modal dialog for approving AI-generated PowerShell scripts.""" - with app._pending_dialog_lock: - dlg = app._pending_dialog - if dlg: - if not app._pending_dialog_open: - imgui.open_popup("Approve PowerShell Command") - app._pending_dialog_open = True - else: - app._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 - _, app.show_text_viewer = imgui.checkbox("Show Full Preview", app.show_text_viewer) - if app.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 app._pending_dialog_lock: app._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 app._pending_dialog_lock: app._pending_dialog = None - imgui.close_current_popup() - imgui.end_popup() - -def render_markdown_test(app: App) -> 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) - def render_ticket_queue(app: App) -> None: """ [C: tests/test_gui_kill_button.py:test_render_ticket_queue_table_columns] @@ -5202,17 +5190,18 @@ def render_beads_tab(app: App) -> None: except Exception as e: imgui.text_colored(imgui.ImVec4(1, 0, 0, 1), f"Error loading beads: {e}") -def render_error_tint(app: App) -> None: - """Renders a red tint overlay if hot reload failed.""" - if not HotReloader.is_error_state: return - draw_list = imgui.get_background_draw_list() - display_size = imgui.get_io().display_size - # Translucent red: (1.0, 0.0, 0.0, 0.2) - tint_col = imgui.get_color_u32(imgui.ImVec4(1.0, 0.0, 0.0, 0.2)) - draw_list.add_rect_filled(imgui.ImVec2(0, 0), display_size, tint_col) - if app.perf_profiling_enabled: - imgui.set_next_window_pos(imgui.ImVec2(10, 50)) - with imscope.window("Hot Reload Error", None, imgui.WindowFlags_.always_auto_resize | imgui.WindowFlags_.no_title_bar): - imgui.text_colored(imgui.ImVec4(1, 0, 0, 1), "HOT RELOAD ERROR") - imgui.text_wrapped(HotReloader.last_error or "Unknown error") +def render_mma_focus_selector(app: App) -> None: + """ + [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] + """ + imgui.text("Focus Agent:"); imgui.same_line() + focus_label = app.ui_focus_agent or "All" + if imgui.begin_combo("##focus_agent", focus_label, imgui.ComboFlags_.width_fit_preview): + if imgui.selectable("All", app.ui_focus_agent is None)[0]: app.ui_focus_agent = None + for tier in ["Tier 2", "Tier 3", "Tier 4"]: + if imgui.selectable(tier, app.ui_focus_agent == tier)[0]: app.ui_focus_agent = tier + imgui.end_combo() + imgui.same_line() + if app.ui_focus_agent and imgui.button("x##clear_focus"): app.ui_focus_agent = None +#endregion: MMA