From 3c2fde3c8361401a84591f4203926fa57cc6e203 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sat, 16 May 2026 04:46:51 -0400 Subject: [PATCH] feat(hot-reload): Final high-integrity refactor with restored code regions --- scripts/restore_regions_final.py | 75 + src/gui_2.py | 3535 ++++++++++++++++-------------- 2 files changed, 1905 insertions(+), 1705 deletions(-) create mode 100644 scripts/restore_regions_final.py diff --git a/scripts/restore_regions_final.py b/scripts/restore_regions_final.py new file mode 100644 index 00000000..11ec1d5f --- /dev/null +++ b/scripts/restore_regions_final.py @@ -0,0 +1,75 @@ +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() + + # Regions and their member functions (module-level) + region_map = { + "Diagnostics & Analytics": ["render_usage_analytics_panel", "render_diagnostics_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"] + } + + # 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 + + 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 + + # 3. Re-assemble with regions + assembled = [] + assigned = set() + for r_name, funcs in region_map.items(): + group = [] + for f in funcs: + if f in func_blocks: + group.append(func_blocks[f]) + assigned.add(f) + 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) + for f_name, block in func_blocks.items(): + if f_name not in assigned: + assembled.append(block) + + final_content = "\n".join(prefix) + "\n\n" + "\n\n".join(assembled) + "\n" + with open(file_path, "w", encoding="utf-8") as f: + f.write(final_content) + print("Regions restored successfully.") + +if __name__ == "__main__": + restore() diff --git a/src/gui_2.py b/src/gui_2.py index 97d4c4a6..bd5e2403 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -1241,336 +1241,8 @@ if __name__ == "__main__": main() -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 - app.show_text_viewer = True - app.text_viewer_title = label - app.text_viewer_content = content -def render_heavy_text(app: App, label: str, content: str, id_suffix: str = "") -> None: - imgui.text_colored(C_LBL, f"{label}:") - imgui.same_line() - if imgui.button("[+]##" + label + id_suffix): - app.show_text_viewer = True - app.text_viewer_type = 'markdown' if label in ('message', 'text', 'content', 'system') else 'json' if label in ('tool_calls', 'data') else 'powershell' if label == 'script' else 'text' - app.text_viewer_title = label - app.text_viewer_content = content - - if not content: - imgui.text_disabled("(empty)") - return - - is_md = label in ("message", "text", "content") - ctx_id = f"heavy_{label}_{id_suffix}" - - with theme.ai_text_style(): - if len(content) > COMMS_CLAMP_CHARS: - if is_md: - imgui.begin_child(f"heavy_text_child_{label}_{id_suffix}", imgui.ImVec2(0, 180), True, imgui.WindowFlags_.always_vertical_scrollbar) - markdown_helper.render(content, context_id=ctx_id) - imgui.end_child() - else: - imgui.input_text_multiline(f"##heavy_text_input_{label}_{id_suffix}", content, imgui.ImVec2(-1, 180), imgui.InputTextFlags_.read_only) - else: - if is_md: - markdown_helper.render(content, context_id=ctx_id) - else: - if app.ui_word_wrap: - imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) - imgui.text(content) - imgui.pop_text_wrap_pos() - else: - imgui.text(content) - -def render_thinking_trace(app: App, segments: list[dict], entry_index: int, is_standalone: bool = False) -> None: - if not segments: - return - with imscope.style_color(imgui.Col_.child_bg, vec4(40, 35, 25, 180)), \ - theme.ai_text_style(): - imgui.indent() - - show_content = True - if not is_standalone: - header_label = f"Monologue ({len(segments)} traces)###thinking_header_{entry_index}" - show_content = imgui.collapsing_header(header_label) - - if show_content: - h = 150 if is_standalone else 100 - with imscope.child(f"thinking_content_{entry_index}", 0, h, True): - for idx, seg in enumerate(segments): - content = seg.get("content", "") - marker = seg.get("marker", "thinking") - with imscope.id(f"think_{entry_index}_{idx}"): - imgui.text_colored(vec4(180, 150, 80), f"[{marker}]") - if app.ui_word_wrap: - with imscope.text_wrap(imgui.get_content_region_avail().x): - imgui.text(content) - else: - imgui.text(content) - imgui.separator() - - imgui.unindent() - -def render_selectable_label(app: App, label: str, value: str, width: float = 0.0, multiline: bool = False, height: float = 0.0, color: Optional[imgui.ImVec4] = None) -> None: - with imscope.id(label + str(hash(value))): - with imscope.style_color(imgui.Col_.frame_bg, vec4(0, 0, 0, 0)), \ - imscope.style_color(imgui.Col_.frame_bg_hovered, vec4(0, 0, 0, 0)), \ - imscope.style_color(imgui.Col_.frame_bg_active, vec4(0, 0, 0, 0)), \ - imscope.style_color(imgui.Col_.border, vec4(0, 0, 0, 0)): - with imscope.style_var(imgui.StyleVar_.frame_border_size, 0.0), \ - imscope.style_var(imgui.StyleVar_.frame_padding, imgui.ImVec2(0, 0)): - if color: - with imscope.style_color(imgui.Col_.text, color): - if multiline: - imgui.input_text_multiline("##" + label, value, imgui.ImVec2(width, height), imgui.InputTextFlags_.read_only) - else: - if width > 0: imgui.set_next_item_width(width) - imgui.input_text("##" + label, value, imgui.InputTextFlags_.read_only) - else: - if multiline: - imgui.input_text_multiline("##" + label, value, imgui.ImVec2(width, height), imgui.InputTextFlags_.read_only) - else: - if width > 0: imgui.set_next_item_width(width) - imgui.input_text("##" + label, value, imgui.InputTextFlags_.read_only) - -def render_save_preset_modal(app: App) -> None: - if not app._show_save_preset_modal: return - imgui.open_popup("Save Layout Preset") - with imscope.popup_modal("Save Layout Preset", True, imgui.WindowFlags_.always_auto_resize) as (opened, _): - if opened: - imgui.text("Preset Name:") - _, app._new_preset_name = imgui.input_text("##preset_name", app._new_preset_name) - if imgui.button("Save", imgui.ImVec2(120, 0)): - if app._new_preset_name.strip(): - ini_data = imgui.save_ini_settings_to_memory() - app.layout_presets[app._new_preset_name.strip()] = { - "ini": ini_data, - "multi_viewport": app.ui_multi_viewport - } - app.config["layout_presets"] = app.layout_presets - models.save_config(app.config) - app._show_save_preset_modal = False - app._new_preset_name = "" - imgui.close_current_popup() - imgui.same_line() - if imgui.button("Cancel", imgui.ImVec2(120, 0)): - app._show_save_preset_modal = False - imgui.close_current_popup() - -def render_main_interface(app: App) -> None: - render_error_tint(app) - app.perf_monitor.start_frame() - app._autofocus_response_tab = app.controller._autofocus_response_tab - - #region: Process GUI task queue - app._process_pending_gui_tasks() - app._process_pending_history_adds() - if app.controller._process_pending_tool_calls(): app._tool_log_dirty = True - #endregion: Process GUI task queue - - render_track_proposal_modal(app) - render_patch_modal(app) - render_base_prompt_diff_modal(app) - render_save_preset_modal(app) - render_save_workspace_profile_modal(app) - render_add_context_files_modal(app) - render_preset_manager_window(app) - render_tool_preset_manager_window(app) - render_persona_editor_window(app) - - # Auto-save (every 60s) - now = time.time() - if now - app._last_autosave >= app._autosave_interval: - app._last_autosave = now - try: - app._flush_to_project() - app._flush_to_config() - models.save_config(app.config) - except Exception: - pass # silent — don't disrupt the GUI loop - - # Sync pending comms - with app._pending_comms_lock: - if app._pending_comms: - if app.ui_auto_scroll_comms: app._scroll_comms_to_bottom = True - app._comms_log_dirty = True - for c in app._pending_comms: app._comms_log.append(c) - app._pending_comms.clear() - - if app.ui_focus_agent != app._last_ui_focus_agent: - app._comms_log_dirty = True - app._tool_log_dirty = True - app._last_ui_focus_agent = app.ui_focus_agent - - if app._comms_log_dirty: - if app.is_viewing_prior_session: app._comms_log_cache = app.prior_session_entries - else: - log_raw = list(app._comms_log) - if app.ui_focus_agent: app._comms_log_cache = [e for e in log_raw if e.get("source_tier", "").startswith(app.ui_focus_agent)] - else: app._comms_log_cache = log_raw - app._comms_log_dirty = False - - if app._tool_log_dirty: - if app.is_viewing_prior_session: app._tool_log_cache = app.prior_tool_calls - else: - log_raw = list(app._tool_log) - if app.ui_focus_agent: app._tool_log_cache = [e for e in log_raw if e.get("source_tier", "").startswith(app.ui_focus_agent)] - else: app._tool_log_cache = log_raw - app._tool_log_dirty = False - - app._render_window_if_open("Project Settings", app._render_project_settings_hub) - app._render_window_if_open("Files & Media", app._render_files_and_media) - app._render_window_if_open("AI Settings", app._render_ai_settings_hub) - app._render_window_if_open("Usage Analytics", app._render_usage_analytics_panel, app.ui_separate_usage_analytics) - app._render_window_if_open("MMA Dashboard", app._render_mma_dashboard) - app._render_window_if_open("Task DAG", app._render_task_dag_panel, app.ui_separate_task_dag) - - app._render_window_if_open("Tier 1: Strategy", lambda: render_tier_stream_panel(app, "Tier 1", "Tier 1"), app.ui_separate_tier1) - app._render_window_if_open("Tier 2: Tech Lead", lambda: render_tier_stream_panel(app, "Tier 2", "Tier 2 (Tech Lead)"), app.ui_separate_tier2) - app._render_window_if_open("Tier 3: Workers", lambda: render_tier_stream_panel(app, "Tier 3", None), app.ui_separate_tier3) - app._render_window_if_open("Tier 4: QA", lambda: render_tier_stream_panel(app, "Tier 4", "Tier 4 (QA)"), app.ui_separate_tier4) - - if app.show_windows.get("Theme", False): render_theme_panel(app) - - app._render_window_if_open("Discussion Hub", app._render_discussion_hub) - app._render_window_if_open("Operations Hub", app._render_operations_hub) - - app._render_window_if_open("Message", app._render_message_panel, app.ui_separate_message_panel) - app._render_window_if_open("Response", app._render_response_panel, app.ui_separate_response_panel) - app._render_window_if_open("Tool Calls", app._render_tool_calls_panel, app.ui_separate_tool_calls_panel) - app._render_window_if_open("External Tools", app._render_external_tools_panel, app.ui_separate_external_tools) - app._render_window_if_open("Log Management", app._render_log_management) - app._render_window_if_open("Diagnostics", app._render_diagnostics_panel) - - app.perf_monitor.end_frame() - - # Modals / Popups - render_approve_script_modal(app) - render_mma_modals(app) - -def render_custom_title_bar(app: App) -> None: - # Obsolete, removed since it renders behind the full screen dock space. - # 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) +#region: Diagnostics & Analytics def render_usage_analytics_panel(app: App) -> None: if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_usage_analytics_panel") @@ -1597,37 +1269,6 @@ def render_usage_analytics_panel(app: App) -> None: if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_usage_analytics_panel") -def render_cache_panel(app: App) -> None: - if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_cache_panel") - if app.current_provider != "gemini": - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_cache_panel") - return - imgui.text_colored(C_LBL, 'Cache Analytics') - stats = getattr(app.controller, '_cached_cache_stats', {}) - if not stats.get("cache_exists"): - imgui.text_disabled("No active cache") - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_cache_panel") - return - age_sec = stats.get("cache_age_seconds", 0) - ttl_remaining = stats.get("ttl_remaining", 0) - ttl_total = stats.get("ttl_seconds", 3600) - age_str = f"{age_sec/60:.0f}m {age_sec%60:.0f}s" - remaining_str = f"{ttl_remaining/60:.0f}m {ttl_remaining%60:.0f}s" - ttl_pct = (ttl_remaining / ttl_total * 100) if ttl_total > 0 else 0 - imgui.text(f"Age: {age_str}") - imgui.text(f"TTL: {remaining_str} ({ttl_pct:.0f}%)") - color = imgui.ImVec4(0.2, 0.8, 0.2, 1.0) - if ttl_pct < 20: color = imgui.ImVec4(1.0, 0.2, 0.2, 1.0) - elif ttl_pct < 50: color = imgui.ImVec4(1.0, 0.8, 0.0, 1.0) - imgui.push_style_color(imgui.Col_.plot_histogram, color) - imgui.progress_bar(ttl_pct / 100.0, imgui.ImVec2(-1, 0), f"{ttl_pct:.0f}%") - imgui.pop_style_color() - if imgui.button("Clear Cache"): - app.controller.clear_cache() - app._cache_cleared_timestamp = time.time() - if hasattr(app, '_cache_cleared_timestamp') and time.time() - app._cache_cleared_timestamp < 5: - imgui.text_colored(imgui.ImVec4(0.2, 1.0, 0.2, 1.0), "Cache cleared - will rebuild on next request") - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_cache_panel") def render_diagnostics_panel(app: App) -> None: if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_diagnostics_panel") @@ -1734,154 +1375,10 @@ def render_diagnostics_panel(app: App) -> None: if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_diagnostics_panel") -def render_tool_analytics_panel(app: App) -> None: - if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_tool_analytics_panel") - imgui.text_colored(C_LBL, 'Tool Usage') - imgui.separator() - now = time.time() - if not hasattr(app, '_tool_stats_cache_time') or now - app._tool_stats_cache_time > 1.0: - app._cached_tool_stats = getattr(app.controller, '_tool_stats', {}) - tool_stats = getattr(app.controller, '_cached_tool_stats', {}) - if not tool_stats: - imgui.text_disabled("No tool usage data") - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_tool_analytics_panel") - return - if imgui.begin_table("tool_stats", 4, imgui.TableFlags_.borders | imgui.TableFlags_.sortable): - imgui.table_setup_column("Tool") - imgui.table_setup_column("Count") - imgui.table_setup_column("Avg (ms)") - imgui.table_setup_column("Fail %") - imgui.table_headers_row() - sorted_tools = sorted(tool_stats.items(), key=lambda x: -x[1].get("count", 0)) - for tool_name, stats in sorted_tools: - count = stats.get("count", 0) - total_time = stats.get("total_time_ms", 0) - failures = stats.get("failures", 0) - avg_time = total_time / count if count > 0 else 0 - fail_pct = (failures / count * 100) if count > 0 else 0 - imgui.table_next_row() - imgui.table_set_column_index(0) - imgui.text(tool_name) - imgui.table_set_column_index(1) - imgui.text(str(count)) - imgui.table_set_column_index(2) - imgui.text(f"{avg_time:.0f}") - imgui.table_set_column_index(3) - if fail_pct > 0: imgui.text_colored(imgui.ImVec4(1.0, 0.2, 0.2, 1.0), f"{fail_pct:.0f}%") - else: imgui.text("0%") - imgui.end_table() - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_tool_analytics_panel") -def render_token_budget_panel(app: App) -> None: - if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_token_budget_panel") - imgui.text_colored(C_LBL, 'Prompt Utilization') - usage = app.session_usage - total = usage["input_tokens"] + usage["output_tokens"] - if total == 0 and usage.get("total_tokens", 0) > 0: total = usage["total_tokens"] - render_selectable_label(app, "session_telemetry_tokens", f"Tokens: {total:,} (In: {usage['input_tokens']:,} Out: {usage['output_tokens']:,})", width=-1, color=C_RES) - if usage.get("last_latency", 0.0) > 0: imgui.text_colored(C_LBL, f" Last Latency: {usage['last_latency']:.2f}s") - if usage["cache_read_input_tokens"]: imgui.text_colored(C_LBL, f" Cache Read: {usage['cache_read_input_tokens']:,} Creation: {usage['cache_creation_input_tokens']:,}") - if app._gemini_cache_text: imgui.text_colored(C_SUB, app._gemini_cache_text) - imgui.separator() +#endregion: Diagnostics & Analytics - if app._token_stats_dirty: - app._token_stats_dirty = False - # Offload to background thread via event queue - app.controller.event_queue.put("refresh_api_metrics", {"md_content": app._last_stable_md or ""}) - stats = app._token_stats - if not stats: - imgui.text_disabled("Token stats unavailable") - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_token_budget_panel") - return - pct = stats.get("utilization_pct", 0.0) - current = stats.get("estimated_prompt_tokens", stats.get("total_tokens", 0)) - limit = stats.get("max_prompt_tokens", 0) - headroom = stats.get("headroom_tokens", max(0, limit - current)) - if pct < 50.0: color = imgui.ImVec4(0.2, 0.8, 0.2, 1.0) - elif pct < 80.0: color = imgui.ImVec4(1.0, 0.8, 0.0, 1.0) - else: color = imgui.ImVec4(1.0, 0.2, 0.2, 1.0) - imgui.push_style_color(imgui.Col_.plot_histogram, color) - imgui.progress_bar(pct / 100.0, imgui.ImVec2(-1, 0), f"{pct:.1f}%") - imgui.pop_style_color() - imgui.text_disabled(f"{current:,} / {limit:,} tokens ({headroom:,} remaining)") - sys_tok = stats.get("system_tokens", 0) - tool_tok = stats.get("tools_tokens", 0) - hist_tok = stats.get("history_tokens", 0) - total_tok = sys_tok + tool_tok + hist_tok or 1 - if imgui.begin_table("token_breakdown", 3, imgui.TableFlags_.borders_inner_h | imgui.TableFlags_.sizing_fixed_fit): - imgui.table_setup_column("Component") - imgui.table_setup_column("Tokens") - imgui.table_setup_column("Pct") - imgui.table_headers_row() - for lbl, tok in [("System", sys_tok), ("Tools", tool_tok), ("History", hist_tok)]: - imgui.table_next_row() - imgui.table_set_column_index(0); imgui.text(lbl) - imgui.table_set_column_index(1); imgui.text(f"{tok:,}") - imgui.table_set_column_index(2); imgui.text(f"{tok / total_tok * 100:.0f}%") - imgui.end_table() - imgui.separator() - imgui.text("MMA Tier Costs") - if hasattr(app, 'mma_tier_usage') and app.mma_tier_usage: - if imgui.begin_table("tier_cost_breakdown", 4, imgui.TableFlags_.borders_inner_h | imgui.TableFlags_.sizing_fixed_fit): - imgui.table_setup_column("Tier") - imgui.table_setup_column("Model") - imgui.table_setup_column("Tokens") - imgui.table_setup_column("Est. Cost") - imgui.table_headers_row() - for tier, stats in app.mma_tier_usage.items(): - model = stats.get('model', 'unknown') - in_t = stats.get('input', 0) - out_t = stats.get('output', 0) - tokens = in_t + out_t - cost = cost_tracker.estimate_cost(model, in_t, out_t) - imgui.table_next_row() - imgui.table_set_column_index(0); render_selectable_label(app, f"tier_{tier}", tier, width=-1) - imgui.table_set_column_index(1); render_selectable_label(app, f"model_{tier}", model.split("-")[0], width=-1) - imgui.table_set_column_index(2); render_selectable_label(app, f"tokens_{tier}", f"{tokens:,}", width=-1) - imgui.table_set_column_index(3); render_selectable_label(app, f"cost_{tier}", f"${cost:.4f}", width=-1, color=imgui.ImVec4(0.2, 0.9, 0.2, 1)) - imgui.end_table() - tier_total = sum(cost_tracker.estimate_cost(stats.get('model', ''), stats.get('input', 0), stats.get('output', 0)) for stats in app.mma_tier_usage.values()) - render_selectable_label(app, "session_total_cost", f"Session Total: ${tier_total:.4f}", width=-1, color=imgui.ImVec4(0, 1, 0, 1)) - else: - imgui.text_disabled("No MMA tier usage data") - if stats.get("would_trim"): - imgui.text_colored(imgui.ImVec4(1.0, 0.3, 0.0, 1.0), "WARNING: Next call will trim history") - trimmable = stats.get("trimmable_turns", 0) - if trimmable: imgui.text_disabled(f"Trimmable turns: {trimmable}") - msgs = stats.get("messages") - if msgs: - shown = 0 - for msg in msgs: - if shown >= 3: break - if msg.get("trimmable"): - role = msg.get("role", "?") - toks = msg.get("tokens", 0) - imgui.text_disabled(f" [{role}] ~{toks:,} tokens") - shown += 1 - imgui.separator() - cache_stats = getattr(app.controller, '_cached_cache_stats', {}) - if cache_stats.get("cache_exists"): - age = cache_stats.get("cache_age_seconds", 0) - ttl = cache_stats.get("ttl_seconds", 3600) - imgui.text_colored(C_LBL, f"Cache Usage: ACTIVE | Age: {age:.0f}s / {ttl}s | Renews at: {ttl * 0.9:.0f}s") - else: - imgui.text_disabled("Cache Usage: INACTIVE") - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_token_budget_panel") - -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") +#region: Logging def render_log_management(app: App) -> None: """ @@ -1957,6 +1454,11 @@ def render_log_management(app: App) -> None: if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_log_management") + +#endregion: Logging + +#region: Project Management + def render_project_settings_hub(app: App) -> None: with imscope.tab_bar('context_hub_tabs'): with imscope.tab_item('Projects') as (exp, _): @@ -1964,159 +1466,10 @@ 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() +#endregion: Project Management -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_external_tools_panel(app: App) -> None: - if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_external_tools_panel") - if imgui.button("Refresh External MCPs"): app.event_queue.put("refresh_external_mcps", None) - - imgui.separator() - - # Server status indicators - manager = mcp_client.get_external_mcp_manager() - statuses = manager.get_servers_status() - if statuses: - imgui.text("Servers:") - for sname, status in statuses.items(): - imgui.same_line() - # Green for running, Yellow for starting, Red for error, Gray for idle - col = (0.5, 0.5, 0.5, 1.0) - if status == 'running': col = (0.0, 1.0, 0.0, 1.0) - elif status == 'starting': col = (1.0, 1.0, 0.0, 1.0) - elif status == 'error': col = (1.0, 0.0, 0.0, 1.0) - imgui.color_button(f"##status_{sname}", col) - imgui.same_line() - imgui.text(sname) - imgui.separator() - - tools = manager.get_all_tools() - if not tools: - imgui.text_disabled("No external tools found.") - else: - if imgui.begin_table("external_tools_table", 3, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable): - imgui.table_setup_column("Name") - imgui.table_setup_column("Server") - imgui.table_setup_column("Description") - imgui.table_headers_row() - - for tname, tinfo in tools.items(): - imgui.table_next_row() - imgui.table_next_column() - imgui.text(tname) - imgui.table_next_column() - imgui.text(tinfo.get('server', 'unknown')) - imgui.table_next_column() - imgui.text(tinfo.get('description', '')) - imgui.end_table() - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_external_tools_panel") +#region: AI Settings def render_ai_settings_hub(app: App) -> None: render_persona_selector_panel(app) @@ -2125,159 +1478,6 @@ def render_ai_settings_hub(app: App) -> None: 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_preset_manager_content(app: App, is_embedded: bool = False) -> None: avail = imgui.get_content_region_avail() @@ -2368,6 +1568,7 @@ def render_preset_manager_content(app: App, is_embedded: bool = False) -> None: if imgui.button("Close##p", imgui.ImVec2(100, 0)): app.show_preset_manager_window = False imgui.end_table() + def render_preset_manager_window(app: App, is_embedded: bool = False) -> None: if not app.show_preset_manager_window and not is_embedded: return if not is_embedded: @@ -2378,6 +1579,7 @@ def render_preset_manager_window(app: App, is_embedded: bool = False) -> None: else: render_preset_manager_content(app, is_embedded=is_embedded) + def render_tool_preset_manager_content(app: App, is_embedded: bool = False) -> None: avail = imgui.get_content_region_avail() if not hasattr(app, "_tool_split_v"): app._tool_split_v = 0.4 @@ -2559,6 +1761,7 @@ def render_tool_preset_manager_content(app: App, is_embedded: bool = False) -> N if imgui.button("Close##tp", imgui.ImVec2(100, 0)): app.show_tool_preset_manager_window = False imgui.end_table() + def render_tool_preset_manager_window(app: App, is_embedded: bool = False) -> None: if not app.show_tool_preset_manager_window and not is_embedded: return if not is_embedded: @@ -2569,6 +1772,7 @@ def render_tool_preset_manager_window(app: App, is_embedded: bool = False) -> No else: render_preset_manager_content(app, is_embedded=is_embedded) + def render_persona_editor_window(app: App, is_embedded: bool = False) -> None: if not app.show_persona_editor_window and not is_embedded: return if not is_embedded: @@ -2741,153 +1945,10 @@ def render_persona_editor_window(app: App, is_embedded: bool = False) -> None: if not is_embedded: imgui.end() -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() +#endregion: AI Settings - # 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") +#region: Context Management def render_files_and_media(app: App) -> None: """ @@ -2998,6 +2059,7 @@ def render_files_and_media(app: App) -> None: if p not in app.screenshots: app.screenshots.append(p) return + def render_files_panel(app: App, height_override: float = 0) -> None: if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_files_panel") imgui.text("Paths") @@ -3078,6 +2140,7 @@ def render_files_panel(app: App, height_override: float = 0) -> None: if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_files_panel") + def render_screenshots_panel(app: App, height_override: float = 0) -> None: if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_screenshots_panel") imgui.text("Paths"); imgui.same_line(); imgui.text("| Base Dir:"); imgui.same_line(); @@ -3112,6 +2175,1796 @@ def render_screenshots_panel(app: App, height_override: float = 0) -> None: if p not in app.screenshots: app.screenshots.append(p) if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_screenshots_panel") + +def render_context_batch_actions(app: App, total_lines: int, total_ast: int) -> None: + imgui.text("Batch:") + for mode in ["full", "summary", "skeleton", "outline", "masked", "none"]: + if imgui.button(f"{mode.capitalize()}##batch"): + for f in app.context_files: + f_path = f.path if hasattr(f, "path") else str(f) + if f_path in app.ui_selected_context_files: f.view_mode = mode + imgui.same_line() + if imgui.button("Sel All##selall"): + for f in app.context_files: + f_path = f.path if hasattr(f, "path") else str(f) + app.ui_selected_context_files.add(f_path) + imgui.same_line() + if imgui.button("Unsel All##unselall"): app.ui_selected_context_files.clear() + imgui.same_line() + if imgui.button("Add Files"): imgui.open_popup("Select Context Files") + imgui.same_line() + if imgui.button("Add All##addall"): + context_paths = {f.path if hasattr(f, "path") else str(f) for f in app.context_files} + for f in app.files: + f_path = f.path if hasattr(f, "path") else str(f) + if f_path not in context_paths: + f_copy = copy.deepcopy(f) + app.context_files.append(f_copy) + app._populate_auto_slices(f_copy) + imgui.same_line() + if imgui.button("Del##batch"): + new_files = [] + for f in app.context_files: + f_path = f.path if hasattr(f, "path") else str(f) + if f_path not in app.ui_selected_context_files: new_files.append(f) + app.context_files = new_files + app.ui_selected_context_files.clear() + imgui.same_line() + imgui.text(f" | Total: {len(app.context_files)} files, {total_lines} lines, {total_ast} AST elements") + + +def render_add_context_files_modal(app: App) -> None: + """ + [C: tests/test_auto_slices.py:test_add_selected_triggers_auto_slices] + """ + if imgui.begin_popup_modal("Select Context Files", None, imgui.WindowFlags_.always_auto_resize)[0]: + imgui.text("Select files from project to add to context:") + imgui.begin_child("ctx_picker_list", imgui.ImVec2(600, 300), True) + if True: + # Create a temporary selection set if not initialized + if not hasattr(app, '_ui_picker_selected'): app._ui_picker_selected = set() + for f in app.files: + fpath = f.path if hasattr(f, 'path') else str(f) + # Skip if already in context + if any((cf.path if hasattr(cf, 'path') else str(cf)) == fpath for cf in app.context_files): + continue + is_sel = fpath in app._ui_picker_selected + clicked, new_sel = imgui.checkbox(f"{fpath}##picker_{fpath}", is_sel) + if clicked: + if new_sel: + app._ui_picker_selected.add(fpath) + else: + app._ui_picker_selected.discard(fpath) + imgui.end_child() + imgui.separator() + + if imgui.button("Add Selected", imgui.ImVec2(120, 0)): + for fpath in app._ui_picker_selected: + f_item = models.FileItem(path=fpath) + app.context_files.append(f_item) + app._populate_auto_slices(f_item) + app._ui_picker_selected.clear() + imgui.close_current_popup() + + imgui.same_line() + if imgui.button("Cancel", imgui.ImVec2(120, 0)): + if hasattr(app, '_ui_picker_selected'): + app._ui_picker_selected.clear() + imgui.close_current_popup() + imgui.end_popup() + + +#endregion: Context Management + +#region: Discussions + +def render_discussion_hub(app: App) -> None: + with imscope.tab_bar("discussion_hub_tabs"): + with imscope.tab_item("Discussion") as (exp, opened): + if exp: render_discussion_tab(app) + with imscope.tab_item("Context Composition") as (exp, opened): + if exp: render_context_composition_panel(app) + with imscope.tab_item("Snapshot") as (exp, opened): + if exp: render_snapshot_tab(app) + with imscope.tab_item("Takes") as (exp, opened): + if exp: render_takes_panel(app) + return + + +def render_discussion_entry(app: App, entry: dict, index: int) -> None: + with imscope.id(f"disc_{index}"): + collapsed, read_mode = entry.get("collapsed", False), entry.get("read_mode", False) + if imgui.button("+" if collapsed else "-"): entry["collapsed"] = not collapsed + imgui.same_line(); render_text_viewer(app, f"Entry #{index+1}", entry["content"]); imgui.same_line(); imgui.set_next_item_width(120) + if imgui.begin_combo("##role", entry["role"]): + for r in app.disc_roles: + if imgui.selectable(r, r == entry["role"])[0]: entry["role"] = r + imgui.end_combo() + if not collapsed: + imgui.same_line() + if imgui.button("[Edit]" if read_mode else "[Read]"): entry["read_mode"] = not read_mode + ts_str = entry.get("ts", "") + if ts_str: + imgui.same_line(); imgui.text_colored(vec4(120, 120, 100), str(ts_str)); e_dt = project_manager.parse_ts(ts_str) + if e_dt: + e_unix, next_unix = e_dt.timestamp(), float('inf') + if index + 1 < len(app.disc_entries): + n_ts = app.disc_entries[index+1].get("ts", ""); n_dt = project_manager.parse_ts(n_ts) + if n_dt: next_unix = n_dt.timestamp() + injected = [f for f in app.files if hasattr(f, 'injected_at') and f.injected_at and e_unix <= f.injected_at < next_unix] + if injected: + imgui.same_line(); imgui.text_colored(vec4(100, 255, 100), f"[{len(injected)}+]") + if imgui.is_item_hovered(): imgui.set_tooltip("Files injected at this point:\n" + "\n".join([f.path for f in injected])) + if collapsed: + imgui.same_line() + if imgui.button("Ins"): app.disc_entries.insert(index, {"role": "User", "content": "", "collapsed": True, "ts": project_manager.now_ts()}) + imgui.same_line() + if imgui.button("Del"): app.disc_entries.pop(index); return + imgui.same_line() + if imgui.button("Branch"): app._branch_discussion(index) + imgui.same_line(); preview = entry["content"].replace("\n", " ")[:60] + if len(entry["content"]) > 60: preview += "..." + if not preview.strip() and entry.get("thinking_segments"): + preview = entry["thinking_segments"][0]["content"].replace("\n", " ")[:60] + if len(entry["thinking_segments"][0]["content"]) > 60: preview += "..." + imgui.text_colored(vec4(160, 160, 150), preview) + if not collapsed: + thinking_segments, has_content = entry.get("thinking_segments", []), bool(entry.get("content", "").strip()) + if thinking_segments: render_thinking_trace(app, thinking_segments, index, is_standalone=not has_content) + if read_mode: render_discussion_entry_read_mode(app, entry, index) + else: + if not (bool(thinking_segments) and not has_content): ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150)) + imgui.separator() + + +def render_discussion_entry_read_mode(app: App, entry: dict, index: int) -> None: + content = entry["content"] + if not content.strip(): return + if '## Retrieved Context' in content: + rag_match = re.search(r'## Retrieved Context\n\n([\s\S]*?)(?=\n\n#|\Z)', content) + if rag_match: + rag_section = rag_match.group(1) + if imgui.collapsing_header('Retrieved Context'): + chunks = re.finditer(r'### Chunk (\d+) \(Source: (.*?)\)\n([\s\S]*?)(?=\n### Chunk|\Z)', rag_section) + for chunk_match in chunks: + idx, path, chunk_content = chunk_match.group(1), chunk_match.group(2), chunk_match.group(3) + if imgui.collapsing_header(f'Chunk {idx}: {path}'): + if imgui.button(f'[Source]##rag_{index}_{idx}'): + res = mcp_client.read_file(path) + if res: app.text_viewer_title, app.text_viewer_content, app.text_viewer_type, app.show_text_viewer = path, res, (Path(path).suffix.lstrip('.') if Path(path).suffix else 'text'), True + imgui.text_unformatted(chunk_content) + content = content[:rag_match.start()] + content[rag_match.end():] + pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?") + matches, is_nerv = list(pattern.finditer(content)), theme.is_nerv_active() + if not matches: + with theme.ai_text_style(): + markdown_helper.render(content, context_id=f'disc_{index}') + else: + with imscope.child(f"read_content_{index}", size_y=150, flags=True): + if app.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) + last_idx = 0 + for m_idx, match in enumerate(matches): + before = content[last_idx:match.start()] + if before: + with theme.ai_text_style(): + markdown_helper.render(before, context_id=f'disc_{index}_b_{m_idx}') + header_text, path, code_block = match.group(0).split("\n")[0].strip(), match.group(2), match.group(4) + if imgui.collapsing_header(header_text): + if imgui.button(f"[Source]##{index}_{match.start()}"): + res = mcp_client.read_file(path) + if res: app.text_viewer_title, app.text_viewer_content, app.text_viewer_type, app.show_text_viewer = path, res, (Path(path).suffix.lstrip('.') if Path(path).suffix else 'text'), True + if code_block: + with theme.ai_text_style(): + markdown_helper.render(code_block, context_id=f'disc_{index}_c_{m_idx}') + last_idx = match.end() + after = content[last_idx:] + if after: + with theme.ai_text_style(): + markdown_helper.render(after, context_id=f'disc_{index}_a') + if app.ui_word_wrap: imgui.pop_text_wrap_pos() + + +#endregion: Discussions + +#region: Operations Monitor + +def render_operations_hub(app: App) -> None: + imgui.push_style_var(imgui.StyleVar_.item_spacing, imgui.ImVec2(10, 4)) + ch1, app.ui_separate_tool_calls_panel = imgui.checkbox("Pop Out Tool Calls", app.ui_separate_tool_calls_panel) + if ch1: app.show_windows["Tool Calls"] = app.ui_separate_tool_calls_panel + imgui.same_line() + ch2, app.ui_separate_usage_analytics = imgui.checkbox("Pop Out Usage Analytics", app.ui_separate_usage_analytics) + if ch2: app.show_windows["Usage Analytics"] = app.ui_separate_usage_analytics + imgui.same_line() + ch3, app.ui_separate_external_tools = imgui.checkbox('Pop Out External Tools', app.ui_separate_external_tools) + if ch3: app.show_windows['External Tools'] = app.ui_separate_external_tools + imgui.pop_style_var() + show_tc_tab, show_usage_tab = not app.ui_separate_tool_calls_panel, not app.ui_separate_usage_analytics + with imscope.tab_bar("ops_tabs"): + with imscope.tab_item("Comms History") as (exp, _): + if exp: render_comms_history_panel(app) + if show_tc_tab: + with imscope.tab_item("Tool Calls") as (exp, _): + if exp: render_tool_calls_panel(app) + if show_usage_tab: + with imscope.tab_item("Usage Analytics") as (exp, _): + if exp: render_usage_analytics_panel(app) + if not app.ui_separate_external_tools: + with imscope.tab_item("External Tools") as (exp, _): + if exp: + render_external_tools_panel(app) + imgui.separator(); imgui.text("") + try: render_external_editor_panel(app) + except Exception as e: imgui.text_colored(vec4(1, 0.3, 0.3, 1), f"Error: {str(e)}") + with imscope.tab_item("Workspace Layouts") as (exp, _): + if exp: + imgui.text("Experimental: Auto-switch layout by Tier") + ch, app.controller.ui_auto_switch_layout = imgui.checkbox("Enable Auto-Switch", app.controller.ui_auto_switch_layout) + if app.controller.ui_auto_switch_layout: + imgui.separator(); imgui.text("Tier Bindings (select profile for each tier)") + profiles = [""] + [p.name for p in app.controller.workspace_profiles.values()] + for t in ["Tier 1", "Tier 2", "Tier 3", "Tier 4"]: + curr = app.controller.ui_tier_layout_bindings.get(t, ""); idx = profiles.index(curr) if curr in profiles else 0 + ch_combo, new_idx = imgui.combo(t, idx, profiles) + if ch_combo: app.controller.ui_tier_layout_bindings[t] = profiles[new_idx] + + +def render_message_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_message_panel") +# LIVE indicator + is_live = app.ai_status in ["running powershell...", "fetching url...", "searching web...", "powershell done, awaiting AI..."] + if is_live: + val = math.sin(time.time() * 10 * math.pi) + alpha = 1.0 if val > 0 else 0.0 + c = imgui.ImVec4(0.39, 1.0, 0.39, alpha) + if theme.is_nerv_active(): c = vec4(80, 255, 80, alpha) # DATA_GREEN for LIVE in NERV + imgui.text_colored(c, "LIVE") + imgui.separator() + ch, app.ui_ai_input = imgui.input_text_multiline("##ai_in", app.ui_ai_input, imgui.ImVec2(-1, -40)) + # Keyboard shortcuts + io = imgui.get_io() + ctrl_l = io.key_ctrl and imgui.is_key_pressed(imgui.Key.l) + if ctrl_l: app.ui_ai_input = "" + imgui.separator() + is_busy = app.ai_status in ['sending...', 'streaming...'] + send_busy = False + with app._send_thread_lock: + if app.send_thread and app.send_thread.is_alive(): send_busy = True + if is_busy: send_busy = True + + imgui.begin_disabled(send_busy) + ctrl_enter = io.key_ctrl and imgui.is_key_pressed(imgui.Key.enter) + label = "Gen + Send (Busy)" if send_busy else "Gen + Send" + if (imgui.button(label) or ctrl_enter) and not send_busy: app._handle_generate_send() + imgui.end_disabled() + imgui.same_line() + if imgui.button("MD Only"): app._handle_md_only() + imgui.same_line() + if imgui.button("Inject File"): app.show_inject_modal = True + imgui.same_line() + if imgui.button("-> History"): + if app.ui_ai_input: app.disc_entries.append({"role": "User", "content": app.ui_ai_input, "collapsed": False, "ts": project_manager.now_ts()}) + imgui.same_line() + if imgui.button("Reset"): app._handle_reset_session() + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_message_panel") + + +def render_response_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_response_panel") + if app._trigger_blink: + app._trigger_blink = False + app._is_blinking = True + app._blink_start_time = time.time() + try: + imgui.set_window_focus("Response") # type: ignore[call-arg] + except: + pass + is_blinking = False + blink_color = vec4(0, 0, 0, 0) + if app._is_blinking: + elapsed = time.time() - app._blink_start_time + if elapsed > 1.5: + app._is_blinking = False + else: + is_blinking = True + val = math.sin(elapsed * 8 * math.pi) + alpha = 50/255 if val > 0 else 0 + blink_color = vec4(0, 255, 0, alpha) + + with imscope.style_color(imgui.Col_.frame_bg, blink_color) if is_blinking else nullcontext(): + with imscope.style_color(imgui.Col_.child_bg, blink_color) if is_blinking else nullcontext(): + with imscope.child("response_scroll_area", 0, -40, True): + with theme.ai_text_style(): + segments, parsed_response = thinking_parser.parse_thinking_trace(app.ai_response) + if segments: + render_thinking_trace(app, [{"content": s.content, "marker": s.marker} for s in segments], 9999) + markdown_helper.render(parsed_response, context_id="response") + + imgui.separator() + if imgui.button("-> History"): + if app.ai_response: + segments, response = thinking_parser.parse_thinking_trace(app.ai_response) + entry = {"role": "AI", "content": response, "collapsed": True, "ts": project_manager.now_ts()} + if segments: entry["thinking_segments"] = [{"content": s.content, "marker": s.marker} for s in segments] + app.disc_entries.append(entry) + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_response_panel") + + +def render_tool_calls_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_tool_calls_panel") + imgui.text("Tool call history") + imgui.same_line() + if imgui.button("Clear##tc"): + app._tool_log.clear() + app._tool_log_dirty = True + imgui.separator() + + log_to_render = app._tool_log_cache + flags = imgui.TableFlags_.resizable | imgui.TableFlags_.hideable | imgui.TableFlags_.borders_inner_v | imgui.TableFlags_.row_bg | imgui.TableFlags_.scroll_y + + if imgui.begin_table("tool_calls_table", 4, flags, imgui.ImVec2(0, 0)): + imgui.table_setup_column("#", imgui.TableColumnFlags_.width_fixed, 40) + imgui.table_setup_column("Tier", imgui.TableColumnFlags_.width_fixed, 60) + imgui.table_setup_column("Script", imgui.TableColumnFlags_.width_stretch) + imgui.table_setup_column("Result", imgui.TableColumnFlags_.width_fixed, 100) + + imgui.table_headers_row() + + clipper = imgui.ListClipper() + clipper.begin(len(log_to_render)) + while clipper.step(): + for i in range(clipper.display_start, clipper.display_end): + entry = log_to_render[i] + imgui.table_next_row() + + imgui.table_next_column() + imgui.text_colored(C_LBL, f"#{i+1}") + + imgui.table_next_column() + imgui.text_colored(C_SUB, f"[{entry.get('source_tier', 'main')}]") + + imgui.table_next_column() + script = entry.get("script", "") + res = entry.get("result", "") + # Use a clear, formatted combined view for the detail window + combined = f"**COMMAND:**\n```powershell\n{script}\n```\n\n---\n**OUTPUT:**\n```text\n{res}\n```" + + script_preview = script.replace("\n", " ")[:150] + if len(script) > 150: script_preview += "..." + render_selectable_label(app, f'tc_script_{i}', script_preview, width=-1) + if imgui.is_item_clicked(): + app.text_viewer_title = f"Tool Call #{i+1} Details" + app.text_viewer_content = combined + app.text_viewer_type = 'markdown' + app.show_text_viewer = True + + imgui.table_next_column() + res_preview = res.replace("\n", " ")[:30] + if len(res) > 30: res_preview += "..." + render_selectable_label(app, f'tc_res_{i}', res_preview, width=-1) + if imgui.is_item_clicked(): + app.text_viewer_title = f"Tool Call #{i+1} Details" + app.text_viewer_content = combined + app.text_viewer_type = 'markdown' + app.show_text_viewer = True + + imgui.end_table() + + if app._scroll_tool_calls_to_bottom: + imgui.set_scroll_here_y(1.0) + app._scroll_tool_calls_to_bottom = False + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_tool_calls_panel") + + +def render_external_tools_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_external_tools_panel") + if imgui.button("Refresh External MCPs"): app.event_queue.put("refresh_external_mcps", None) + + imgui.separator() + + # Server status indicators + manager = mcp_client.get_external_mcp_manager() + statuses = manager.get_servers_status() + if statuses: + imgui.text("Servers:") + for sname, status in statuses.items(): + imgui.same_line() + # Green for running, Yellow for starting, Red for error, Gray for idle + col = (0.5, 0.5, 0.5, 1.0) + if status == 'running': col = (0.0, 1.0, 0.0, 1.0) + elif status == 'starting': col = (1.0, 1.0, 0.0, 1.0) + elif status == 'error': col = (1.0, 0.0, 0.0, 1.0) + imgui.color_button(f"##status_{sname}", col) + imgui.same_line() + imgui.text(sname) + imgui.separator() + + tools = manager.get_all_tools() + if not tools: + imgui.text_disabled("No external tools found.") + else: + if imgui.begin_table("external_tools_table", 3, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable): + imgui.table_setup_column("Name") + imgui.table_setup_column("Server") + imgui.table_setup_column("Description") + imgui.table_headers_row() + + for tname, tinfo in tools.items(): + imgui.table_next_row() + imgui.table_next_column() + imgui.text(tname) + imgui.table_next_column() + imgui.text(tinfo.get('server', 'unknown')) + imgui.table_next_column() + imgui.text(tinfo.get('description', '')) + imgui.end_table() + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_external_tools_panel") + + +#endregion: Operations Monitor + +#region: Misc Tools + +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 + app.show_text_viewer = True + app.text_viewer_title = label + app.text_viewer_content = content + + +def render_heavy_text(app: App, label: str, content: str, id_suffix: str = "") -> None: + imgui.text_colored(C_LBL, f"{label}:") + imgui.same_line() + if imgui.button("[+]##" + label + id_suffix): + app.show_text_viewer = True + app.text_viewer_type = 'markdown' if label in ('message', 'text', 'content', 'system') else 'json' if label in ('tool_calls', 'data') else 'powershell' if label == 'script' else 'text' + app.text_viewer_title = label + app.text_viewer_content = content + + if not content: + imgui.text_disabled("(empty)") + return + + is_md = label in ("message", "text", "content") + ctx_id = f"heavy_{label}_{id_suffix}" + + with theme.ai_text_style(): + if len(content) > COMMS_CLAMP_CHARS: + if is_md: + imgui.begin_child(f"heavy_text_child_{label}_{id_suffix}", imgui.ImVec2(0, 180), True, imgui.WindowFlags_.always_vertical_scrollbar) + markdown_helper.render(content, context_id=ctx_id) + imgui.end_child() + else: + imgui.input_text_multiline(f"##heavy_text_input_{label}_{id_suffix}", content, imgui.ImVec2(-1, 180), imgui.InputTextFlags_.read_only) + else: + if is_md: + markdown_helper.render(content, context_id=ctx_id) + else: + if app.ui_word_wrap: + imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) + imgui.text(content) + imgui.pop_text_wrap_pos() + else: + imgui.text(content) + + +def render_thinking_trace(app: App, segments: list[dict], entry_index: int, is_standalone: bool = False) -> None: + if not segments: + return + with imscope.style_color(imgui.Col_.child_bg, vec4(40, 35, 25, 180)), \ + theme.ai_text_style(): + imgui.indent() + + show_content = True + if not is_standalone: + header_label = f"Monologue ({len(segments)} traces)###thinking_header_{entry_index}" + show_content = imgui.collapsing_header(header_label) + + if show_content: + h = 150 if is_standalone else 100 + with imscope.child(f"thinking_content_{entry_index}", 0, h, True): + for idx, seg in enumerate(segments): + content = seg.get("content", "") + marker = seg.get("marker", "thinking") + with imscope.id(f"think_{entry_index}_{idx}"): + imgui.text_colored(vec4(180, 150, 80), f"[{marker}]") + if app.ui_word_wrap: + with imscope.text_wrap(imgui.get_content_region_avail().x): + imgui.text(content) + else: + imgui.text(content) + imgui.separator() + + imgui.unindent() + + +def render_selectable_label(app: App, label: str, value: str, width: float = 0.0, multiline: bool = False, height: float = 0.0, color: Optional[imgui.ImVec4] = None) -> None: + with imscope.id(label + str(hash(value))): + with imscope.style_color(imgui.Col_.frame_bg, vec4(0, 0, 0, 0)), \ + imscope.style_color(imgui.Col_.frame_bg_hovered, vec4(0, 0, 0, 0)), \ + imscope.style_color(imgui.Col_.frame_bg_active, vec4(0, 0, 0, 0)), \ + imscope.style_color(imgui.Col_.border, vec4(0, 0, 0, 0)): + with imscope.style_var(imgui.StyleVar_.frame_border_size, 0.0), \ + imscope.style_var(imgui.StyleVar_.frame_padding, imgui.ImVec2(0, 0)): + if color: + with imscope.style_color(imgui.Col_.text, color): + if multiline: + imgui.input_text_multiline("##" + label, value, imgui.ImVec2(width, height), imgui.InputTextFlags_.read_only) + else: + if width > 0: imgui.set_next_item_width(width) + imgui.input_text("##" + label, value, imgui.InputTextFlags_.read_only) + else: + if multiline: + imgui.input_text_multiline("##" + label, value, imgui.ImVec2(width, height), imgui.InputTextFlags_.read_only) + else: + if width > 0: imgui.set_next_item_width(width) + imgui.input_text("##" + label, value, imgui.InputTextFlags_.read_only) + + +#endregion: Misc Tools + +#region: MMA + +def render_mma_dashboard(app: App) -> None: + """ + Main MMA dashboard interface. + [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_approval_badge_shown_when_ask_dialog_pending, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_approval_badge_shown_when_mma_approval_pending, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_approval_badge_shown_when_spawn_pending, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_no_approval_badge_when_idle] + """ + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_mma_dashboard") + render_mma_focus_selector(app) + imgui.separator() + if app.is_viewing_prior_session: + c = vec4(255, 152, 48) if theme.is_nerv_active() else vec4(255, 200, 100) + imgui.text_colored(c, "HISTORICAL VIEW - READ ONLY") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_mma_dashboard") + return + render_mma_track_summary(app) + imgui.separator() + render_mma_epic_planner(app) + imgui.separator() + if imgui.collapsing_header("Conductor Setup"): render_mma_conductor_setup(app) + imgui.separator() + render_mma_track_browser(app) + imgui.separator() + render_mma_global_controls(app) + imgui.separator() + render_mma_usage_section(app) + imgui.separator() + render_ticket_queue(app) + imgui.separator() + app._render_window_if_open("Task DAG", app._render_task_dag_panel, not app.ui_separate_task_dag) + if app.ui_selected_ticket_id: render_mma_ticket_editor(app) + imgui.separator() + render_mma_agent_streams(app) + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_mma_dashboard") + + +def render_mma_modals(app: App) -> None: + """Renders all MMA-specific approval and info modals.""" + is_nerv = theme.is_nerv_active() + # Tool Execution Approval + if app._pending_ask_dialog: + if not app._ask_dialog_open: + imgui.open_popup("Approve Tool Execution") + app._ask_dialog_open = True + else: + app._ask_dialog_open = False + if imgui.begin_popup_modal("Approve Tool Execution", None, imgui.WindowFlags_.always_auto_resize)[0]: + if not app._pending_ask_dialog or app._ask_tool_data is None: imgui.close_current_popup() + else: + tool_name = app._ask_tool_data.get("tool", "unknown"); tool_args = app._ask_tool_data.get("args", {}) + imgui.text("The AI wants to execute a tool:"); imgui.text_colored(vec4(200, 200, 100), f"Tool: {tool_name}"); imgui.separator() + imgui.text("Arguments:"); imgui.begin_child("ask_args_child", imgui.ImVec2(400, 200), True); imgui.text_unformatted(json.dumps(tool_args, indent=2)); imgui.end_child() + imgui.separator() + if imgui.button("Approve", imgui.ImVec2(120, 0)): app._handle_approve_ask(); imgui.close_current_popup() + imgui.same_line() + if imgui.button("Deny", imgui.ImVec2(120, 0)): app._handle_reject_ask(); imgui.close_current_popup() + imgui.end_popup() + # MMA Step Approval + if app._pending_mma_approvals: + if not app._mma_approval_open: + imgui.open_popup("MMA Step Approval") + app._mma_approval_open, app._mma_approval_edit_mode = True, False + app._mma_approval_payload = app._pending_mma_approvals[0].get("payload", "") + else: app._mma_approval_open = False + if imgui.begin_popup_modal("MMA Step Approval", None, imgui.WindowFlags_.always_auto_resize)[0]: + if not app._pending_mma_approvals: imgui.close_current_popup() + else: + ticket_id = app._pending_mma_approvals[0].get("ticket_id", "??") + imgui.text(f"Ticket {ticket_id} is waiting for tool execution approval."); imgui.separator() + if app._mma_approval_edit_mode: + imgui.text("Edit Raw Payload (Manual Memory Mutation):"); _, app._mma_approval_payload = imgui.input_text_multiline("##mma_payload", app._mma_approval_payload, imgui.ImVec2(600, 400)) + else: + imgui.text("Proposed Tool Call:"); imgui.begin_child("mma_preview", imgui.ImVec2(600, 300), True); imgui.text_unformatted(str(app._pending_mma_approvals[0].get("payload", ""))); imgui.end_child() + imgui.separator() + if imgui.button("Approve", imgui.ImVec2(120, 0)): app._handle_mma_respond(approved=True, payload=app._mma_approval_payload); imgui.close_current_popup() + imgui.same_line() + if imgui.button("Edit Payload" if not app._mma_approval_edit_mode else "Show Original", imgui.ImVec2(120, 0)): app._mma_approval_edit_mode = not app._mma_approval_edit_mode + imgui.same_line() + if imgui.button("Abort Ticket", imgui.ImVec2(120, 0)): app._handle_mma_respond(approved=False); imgui.close_current_popup() + imgui.end_popup() + # MMA Spawn Approval + if app._pending_mma_spawns: + if not app._mma_spawn_open: + imgui.open_popup("MMA Spawn Approval") + app._mma_spawn_open, app._mma_spawn_edit_mode = True, False + app._mma_spawn_prompt, app._mma_spawn_context = app._pending_mma_spawns[0].get("prompt", ""), app._pending_mma_spawns[0].get("context_md", "") + else: app._mma_spawn_open = False + if imgui.begin_popup_modal("MMA Spawn Approval", None, imgui.WindowFlags_.always_auto_resize)[0]: + if not app._pending_mma_spawns: imgui.close_current_popup() + else: + role, ticket_id = app._pending_mma_spawns[0].get("role", "??"), app._pending_mma_spawns[0].get("ticket_id", "??") + imgui.text(f"Spawning {role} for Ticket {ticket_id}"); imgui.separator() + if app._mma_spawn_edit_mode: + imgui.text("Edit Prompt:"); _, app._mma_spawn_prompt = imgui.input_text_multiline("##spawn_prompt", app._mma_spawn_prompt, imgui.ImVec2(800, 200)) + imgui.text("Edit Context MD:"); _, app._mma_spawn_context = imgui.input_text_multiline("##spawn_context", app._mma_spawn_context, imgui.ImVec2(800, 300)) + else: + imgui.text("Proposed Prompt:"); imgui.begin_child("spawn_prompt_preview", imgui.ImVec2(800, 150), True); imgui.text_unformatted(app._mma_spawn_prompt); imgui.end_child() + imgui.text("Proposed Context MD:"); imgui.begin_child("spawn_context_preview", imgui.ImVec2(800, 250), True); imgui.text_unformatted(app._mma_spawn_context); imgui.end_child() + imgui.separator() + if imgui.button("Approve", imgui.ImVec2(120, 0)): app._handle_mma_respond(approved=True, prompt=app._mma_spawn_prompt, context_md=app._mma_spawn_context); imgui.close_current_popup() + imgui.same_line() + if imgui.button("Edit Mode" if not app._mma_spawn_edit_mode else "Preview Mode", imgui.ImVec2(120, 0)): app._mma_spawn_edit_mode = not app._mma_spawn_edit_mode + imgui.same_line() + if imgui.button("Abort", imgui.ImVec2(120, 0)): app._handle_mma_respond(approved=False, abort=True); imgui.close_current_popup() + imgui.end_popup() + # Cycle Detection + if imgui.begin_popup_modal("Cycle Detected!", None, imgui.WindowFlags_.always_auto_resize)[0]: + imgui.text_colored(imgui.ImVec4(1, 0.3, 0.3, 1), "The dependency graph contains a cycle!") + imgui.text("Please remove the circular dependency.") + if imgui.button("OK"): imgui.close_current_popup() + imgui.end_popup() + + +def render_mma_track_summary(app: App) -> None: + """ + [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] + """ + is_nerv = theme.is_nerv_active() + track_name = app.active_track.description if app.active_track else "None" + if getattr(app, "ui_project_execution_mode", "native") == "beads": track_name = "Beads Graph" + track_stats = project_manager.calculate_track_progress(app.active_track.tickets if app.active_track else app.active_tickets) + total_cost = sum(cost_tracker.estimate_cost(u.get('model','unknown'), u.get('input',0), u.get('output',0)) for u in app.mma_tier_usage.values()) + imgui.text("Track:"); imgui.same_line(); imgui.text_colored(C_VAL, track_name); imgui.same_line(); imgui.text(" | Status:"); imgui.same_line() + if app.mma_status == "paused": + imgui.text_colored(vec4(255, 152, 48) if is_nerv else imgui.ImVec4(1, 0.5, 0, 1), "PIPELINE PAUSED"); imgui.same_line() + status_col = imgui.ImVec4(1, 1, 1, 1) + if app.mma_status == "idle": status_col = imgui.ImVec4(0.7, 0.7, 0.7, 1) + elif app.mma_status == "running": status_col = vec4(80, 255, 80) if is_nerv else imgui.ImVec4(1, 1, 0, 1) + elif app.mma_status == "done": status_col = imgui.ImVec4(0, 1, 0, 1) + elif app.mma_status == "error": status_col = vec4(255, 72, 64) if is_nerv else imgui.ImVec4(1, 0, 0, 1) + elif app.mma_status == "paused": status_col = imgui.ImVec4(1, 0.5, 0, 1) + imgui.text_colored(status_col, app.mma_status.upper()); imgui.same_line(); imgui.text(" | Cost:"); imgui.same_line(); imgui.text_colored(imgui.ImVec4(0, 1, 0, 1), f"${total_cost:,.4f}") + perc = track_stats["percentage"] / 100.0 + p_color = imgui.ImVec4(1, 0, 0, 1) if track_stats["percentage"] < 33 else (imgui.ImVec4(1, 1, 0, 1) if track_stats["percentage"] < 66 else imgui.ImVec4(0, 1, 0, 1)) + imgui.push_style_color(imgui.Col_.plot_histogram, p_color); imgui.progress_bar(perc, imgui.ImVec2(-1, 0), f"{track_stats['percentage']:.1f}%"); imgui.pop_style_color() + if imgui.begin_table("ticket_stats_breakdown", 4): + for lbl, val in [("Completed:", track_stats["completed"]), ("In Progress:", track_stats["in_progress"]), ("Blocked:", track_stats["blocked"]), ("Todo:", track_stats["todo"])]: + imgui.table_next_column(); imgui.text_colored(C_LBL, lbl); imgui.same_line(); imgui.text_colored(C_VAL, str(val)) + imgui.end_table() + if app.active_track: + remaining = track_stats["total"] - track_stats["completed"] + eta_mins = (app._avg_ticket_time * remaining) / 60.0 + imgui.text_colored(C_LBL, "ETA:"); imgui.same_line(); imgui.text_colored(C_VAL, f"~{int(eta_mins)}m ({remaining} tickets remaining)") + + +def render_mma_epic_planner(app: App) -> None: + """ + [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] + """ + imgui.text_colored(C_LBL, 'Epic Planning (Tier 1)') + _, app.ui_epic_input = imgui.input_text_multiline('##epic_input', app.ui_epic_input, imgui.ImVec2(-1, 80)) + if imgui.button('Plan Epic (Tier 1)', imgui.ImVec2(-1, 0)): app._cb_plan_epic() + + +def render_mma_conductor_setup(app: App) -> None: + """ + [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] + """ + if imgui.button("Run Setup Scan"): app._cb_run_conductor_setup() + if app.ui_conductor_setup_summary: imgui.input_text_multiline("##setup_summary", app.ui_conductor_setup_summary, imgui.ImVec2(-1, 120), imgui.InputTextFlags_.read_only) + + +def render_mma_track_browser(app: App) -> None: + """ + [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] + """ + imgui.text("Track Browser") + if imgui.begin_table("mma_tracks_table", 4, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable): + imgui.table_setup_column("Title"); imgui.table_setup_column("Status"); imgui.table_setup_column("Progress"); imgui.table_setup_column("Actions"); imgui.table_headers_row() + for track in app.tracks: + imgui.table_next_row(); imgui.table_next_column(); imgui.text(track.get("title", "Untitled")); imgui.table_next_column() + status = track.get("status", "unknown").lower() + c = imgui.ImVec4(0.7, 0.7, 0.7, 1) if status == "new" else (vec4(80, 255, 80) if status == "active" and theme.is_nerv_active() else (imgui.ImVec4(1, 1, 0, 1) if status == "active" else (imgui.ImVec4(0, 1, 0, 1) if status == "done" else (imgui.ImVec4(1, 0, 0, 1) if status == "blocked" else imgui.ImVec4(1, 1, 1, 1))))) + imgui.text_colored(c, status.upper()); imgui.table_next_column() + prog = track.get("progress", 0.0) + p_c = imgui.ImVec4(1, 0, 0, 1) if prog < 0.33 else (imgui.ImVec4(1, 1, 0, 1) if prog < 0.66 else imgui.ImVec4(0, 1, 0, 1)) + imgui.push_style_color(imgui.Col_.plot_histogram, p_c); imgui.progress_bar(prog, imgui.ImVec2(-1, 0), f"{int(prog*100)}%"); imgui.pop_style_color(); imgui.table_next_column() + if imgui.button(f"Load##{track.get('id')}"): app._cb_load_track(str(track.get("id") or "")) + imgui.end_table() + imgui.text("Create New Track") + _, app.ui_new_track_name = imgui.input_text("Name##new_track", app.ui_new_track_name) + _, app.ui_new_track_desc = imgui.input_text_multiline("Description##new_track", app.ui_new_track_desc, imgui.ImVec2(-1, 60)) + imgui.text("Type:"); imgui.same_line() + if imgui.begin_combo("##track_type", app.ui_new_track_type): + for ttype in ["feature", "chore", "fix"]: + if imgui.selectable(ttype, app.ui_new_track_type == ttype)[0]: app.ui_new_track_type = ttype + imgui.end_combo() + if imgui.button("Create Track"): + app._cb_create_track(app.ui_new_track_name, app.ui_new_track_desc, app.ui_new_track_type) + app.ui_new_track_name = ""; app.ui_new_track_desc = "" + + +def render_mma_global_controls(app: App) -> None: + """ + [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] + """ + changed, app.mma_step_mode = imgui.checkbox("Step Mode (HITL)", app.mma_step_mode) + imgui.same_line(); imgui.text(f"Status: {app.mma_status.upper()}") + if app.controller and hasattr(app.controller, 'engine') and app.controller.engine and hasattr(app.controller.engine, '_pause_event'): + imgui.same_line() + is_paused = app.controller.engine._pause_event.is_set() + if imgui.button("Resume" if is_paused else "Pause"): + if is_paused: app.controller.engine.resume() + else: app.controller.engine.pause() + if app.active_tier: + imgui.same_line(); imgui.text_colored(C_VAL, f"| Active: {app.active_tier}") + any_pending = len(app._pending_mma_spawns) > 0 or len(app._pending_mma_approvals) > 0 or app._pending_ask_dialog + if any_pending: + alpha = abs(math.sin(time.time() * 5)) + c = vec4(255, 72, 64, alpha) if theme.is_nerv_active() else imgui.ImVec4(1, 0.3, 0.3, alpha) + imgui.same_line(); imgui.text_colored(c, " APPROVAL PENDING"); imgui.same_line() + if imgui.button("Go to Approval"): pass + imgui.separator() + imgui.text("Hot Reload:") + imgui.same_line() + if imgui.button("Reload GUI"): + success = app._trigger_hot_reload() + if success: + imgui.text_colored(imgui.ImVec4(0, 1, 0, 1), "Reloaded!") + else: + imgui.text_colored(imgui.ImVec4(1, 0, 0, 1), f"Error: {app._hot_reload_error or 'Unknown'}") + imgui.same_line(); imgui.text_disabled("(Ctrl+Alt+R)") + + +def render_mma_usage_section(app: App) -> None: + """ + [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] + """ + imgui.text("Tier Usage (Tokens & Cost)") + if imgui.begin_table("mma_usage", 5, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg): + imgui.table_setup_column("Tier"); imgui.table_setup_column("Model"); imgui.table_setup_column("Input"); imgui.table_setup_column("Output"); imgui.table_setup_column("Est. Cost"); imgui.table_headers_row() + total_cost = 0.0 + for tier, stats in app.mma_tier_usage.items(): + imgui.table_next_row(); imgui.table_next_column(); imgui.text(tier); imgui.table_next_column(); model = stats.get('model', 'unknown'); imgui.text(model); imgui.table_next_column(); in_t = stats.get('input', 0); imgui.text(f"{in_t:,}"); imgui.table_next_column(); out_t = stats.get('output', 0); imgui.text(f"{out_t:,}"); imgui.table_next_column(); cost = cost_tracker.estimate_cost(model, in_t, out_t); total_cost += cost; imgui.text(f"${cost:,.4f}") + imgui.table_next_row(); imgui.table_set_bg_color(imgui.TableBgTarget_.row_bg0, imgui.get_color_u32(imgui.Col_.plot_lines_hovered)); imgui.table_next_column(); imgui.text("TOTAL"); imgui.table_next_column(); imgui.text(""); imgui.table_next_column(); imgui.text(""); imgui.table_next_column(); imgui.text(""); imgui.table_next_column(); imgui.text(f"${total_cost:,.4f}"); imgui.end_table() + if imgui.collapsing_header("Tier Model Config"): + for tier in app.mma_tier_usage.keys(): + imgui.text(f"{tier}:"); imgui.same_line(); curr_model, curr_prov = app.mma_tier_usage[tier].get("model", "unknown"), app.mma_tier_usage[tier].get("provider", "gemini") + with imscope.id(f"tier_cfg_{tier}"): + imgui.push_item_width(80) + if imgui.begin_combo("##prov", curr_prov): + for p in models.PROVIDERS: + if imgui.selectable(p, p == curr_prov)[0]: + app.mma_tier_usage[tier]["provider"] = p + models_list = app.controller.all_available_models.get(p, []) + if models_list: app.mma_tier_usage[tier]["model"] = models_list[0] + imgui.end_combo() + imgui.pop_item_width(); imgui.same_line(); imgui.push_item_width(150) + models_list = app.controller.all_available_models.get(curr_prov, []) + if imgui.begin_combo("##model", curr_model): + for m in models_list: + if imgui.selectable(m, curr_model == m)[0]: app.mma_tier_usage[tier]["model"] = m + imgui.end_combo() + imgui.pop_item_width(); imgui.same_line(); imgui.push_item_width(-1) + curr_preset = app.mma_tier_usage[tier].get("tool_preset") or "None" + p_names = ["None"] + sorted(app.controller.tool_presets.keys()) + if imgui.begin_combo("##preset", curr_preset): + for pn in p_names: + if imgui.selectable(pn, curr_preset == pn)[0]: app.mma_tier_usage[tier]["tool_preset"] = None if pn == "None" else pn + imgui.end_combo() + imgui.pop_item_width(); imgui.same_line(); imgui.push_item_width(150) + curr_pers = app.mma_tier_usage[tier].get("persona") or "None" + personas = getattr(app.controller, 'personas', {}) + pers_opts = ["None"] + sorted(personas.keys()) + if imgui.begin_combo("##persona", curr_pers): + for pern in pers_opts: + if imgui.selectable(pern, curr_pers == pern)[0]: app.mma_tier_usage[tier]["persona"] = None if pern == "None" else pern + imgui.end_combo() + imgui.pop_item_width() + + +def render_mma_ticket_editor(app: App) -> None: + imgui.separator(); imgui.text_colored(C_VAL, f"Editing: {app.ui_selected_ticket_id}") + ticket = next((t for t in app.active_tickets if str(t.get('id', '')) == app.ui_selected_ticket_id), None) + if ticket: + imgui.text(f"Status: {ticket.get('status', 'todo')}"); prio = ticket.get('priority', 'medium') + imgui.text("Priority:"); imgui.same_line() + if imgui.begin_combo(f"##edit_prio_{ticket.get('id')}", prio): + for p_opt in ['high', 'medium', 'low']: + if imgui.selectable(p_opt, p_opt == prio)[0]: ticket['priority'] = p_opt; app._push_mma_state_update() + imgui.end_combo() + imgui.text(f"Target: {ticket.get('target_file', '')}"); imgui.text(f"Depends on: {', '.join(ticket.get('depends_on', []))}") + personas = getattr(app.controller, 'personas', {}); curr_pers = ticket.get('persona_id', '') + imgui.text("Persona Override:"); imgui.same_line() + pers_opts = ["None"] + sorted(personas.keys()); curr_idx = pers_opts.index(curr_pers) + 1 if curr_pers in pers_opts else 0 + _, curr_idx = imgui.combo(f"##ticket_persona_{ticket.get('id')}", curr_idx, pers_opts) + ticket['persona_id'] = None if curr_idx == 0 or pers_opts[curr_idx] == "None" else pers_opts[curr_idx] + if imgui.button(f"Mark Complete##{app.ui_selected_ticket_id}"): ticket['status'] = 'done'; app._push_mma_state_update() + imgui.same_line() + if imgui.button(f"Delete##{app.ui_selected_ticket_id}"): app.active_tickets = [t for t in app.active_tickets if str(t.get('id', '')) != app.ui_selected_ticket_id]; app.ui_selected_ticket_id = None; app._push_mma_state_update() + + +def render_mma_agent_streams(app: App) -> None: + """ + [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] + """ + imgui.text("Agent Streams") + if imgui.begin_tab_bar("mma_streams_tabs"): + for tier, label, sep_flag_attr in [("Tier 1", "Tier 1", "ui_separate_tier1"), ("Tier 2", "Tier 2 (Tech Lead)", "ui_separate_tier2"), ("Tier 3", None, "ui_separate_tier3"), ("Tier 4", "Tier 4 (QA)", "ui_separate_tier4")]: + with imscope.tab_item(tier) as (exp, _): + if exp: + sep_val = getattr(app, sep_flag_attr); ch, new_val = imgui.checkbox(f"Pop Out {tier}", sep_val) + if ch: + setattr(app, sep_flag_attr, new_val) + app.show_windows[f"{tier}: Strategy" if tier == "Tier 1" else (f"{tier}: Tech Lead" if tier == "Tier 2" else (f"{tier}: Workers" if tier == "Tier 3" else f"{tier}: QA"))] = new_val + if not new_val: render_tier_stream_panel(app, tier, label) + else: imgui.text_disabled(f"{tier} stream is detached.") + if getattr(app, "ui_project_execution_mode", "native") == "beads": + with imscope.tab_item("Beads") as (exp, _): + if exp: render_beads_tab(app) + imgui.end_tab_bar() + + +def render_tier_stream_panel(app: App, tier_key: str, stream_key: str | None) -> None: + """ + [C: tests/test_mma_dashboard_streams.py:TestMMADashboardStreams.test_tier1_renders_stream_content, tests/test_mma_dashboard_streams.py:TestMMADashboardStreams.test_tier3_renders_worker_subheaders] + """ + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_tier_stream_panel") + if app.is_viewing_prior_session: + imgui.text_colored(vec4(255, 200, 100), "HISTORICAL VIEW - READ ONLY") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_tier_stream_panel") + return + if stream_key is not None: + content = app.mma_streams.get(stream_key, "") + imgui.begin_child(f"##stream_content_{tier_key}", imgui.ImVec2(-1, -1)) + render_selectable_label(app, f'stream_{tier_key}', content, width=-1, multiline=True, height=0) + try: + if len(content) != app._tier_stream_last_len.get(stream_key, -1): + imgui.set_scroll_here_y(1.0) + app._tier_stream_last_len[stream_key] = len(content) + imgui.end_child() + except (TypeError, AttributeError): + imgui.end_child() + pass + else: + tier3_keys = [k for k in app.mma_streams if "Tier 3" in k] + if not tier3_keys: + imgui.text_disabled("No worker output yet.") + else: + worker_status = getattr(app, '_worker_status', {}) + for key in tier3_keys: + ticket_id = key.split(": ", 1)[-1] if ": " in key else key + status = worker_status.get(key, "unknown") + if status == "running": + imgui.text_colored(imgui.ImVec4(1, 1, 0, 1), f"{ticket_id} [{status}]") + elif status == "completed": + imgui.text_colored(imgui.ImVec4(0, 1, 0, 1), f"{ticket_id} [{status}]") + elif status == "failed": + imgui.text_colored(imgui.ImVec4(1, 0, 0, 1), f"{ticket_id} [{status}]") + else: + imgui.text(f"{ticket_id} [{status}]") + imgui.begin_child(f"##tier3_{ticket_id}_scroll", imgui.ImVec2(-1, 150), True) + render_selectable_label(app, f'stream_t3_{ticket_id}', app.mma_streams[key], width=-1, multiline=True, height=0) + try: + if len(app.mma_streams[key]) != app._tier_stream_last_len.get(key, -1): + imgui.set_scroll_here_y(1.0) + app._tier_stream_last_len[key] = len(app.mma_streams[key]) + imgui.end_child() + except (TypeError, AttributeError): + imgui.end_child() + pass + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_tier_stream_panel") + + +def render_track_proposal_modal(app: App) -> None: + if app._show_track_proposal_modal: + imgui.open_popup("Track Proposal") + if imgui.begin_popup_modal("Track Proposal", True, imgui.WindowFlags_.always_auto_resize)[0]: + from src import shaders + p_min = imgui.get_window_pos() + p_max = imgui.ImVec2(p_min.x + imgui.get_window_size().x, p_min.y + imgui.get_window_size().y) + # Render soft shadow behind the modal + shaders.draw_soft_shadow(imgui.get_background_draw_list(), p_min, p_max, imgui.ImVec4(0, 0, 0, 0.6), 25.0, 6.0) + + if app._show_track_proposal_modal: + imgui.text_colored(C_IN, "Proposed Implementation Tracks") + imgui.separator() + if not app.proposed_tracks: + imgui.text("No tracks generated.") + else: + for idx, track in enumerate(app.proposed_tracks): + # Title Edit + changed_t, new_t = imgui.input_text(f"Title##{idx}", track.get('title', '')) + if changed_t: + track['title'] = new_t + # Goal Edit + changed_g, new_g = imgui.input_text_multiline(f"Goal##{idx}", track.get('goal', ''), imgui.ImVec2(-1, 60)) + if changed_g: + track['goal'] = new_g + # Buttons + if imgui.button(f"Remove##{idx}"): + app.proposed_tracks.pop(idx) + break + imgui.same_line() + if imgui.button(f"Start This Track##{idx}"): + app._cb_start_track(idx) + imgui.separator() + if imgui.button("Accept", imgui.ImVec2(120, 0)): + app._cb_accept_tracks() + app._show_track_proposal_modal = False + imgui.close_current_popup() + imgui.same_line() + if imgui.button("Cancel", imgui.ImVec2(120, 0)): + app._show_track_proposal_modal = False + imgui.close_current_popup() + else: + imgui.close_current_popup() + imgui.end_popup() + + +#endregion: MMA + +def render_save_preset_modal(app: App) -> None: + if not app._show_save_preset_modal: return + imgui.open_popup("Save Layout Preset") + with imscope.popup_modal("Save Layout Preset", True, imgui.WindowFlags_.always_auto_resize) as (opened, _): + if opened: + imgui.text("Preset Name:") + _, app._new_preset_name = imgui.input_text("##preset_name", app._new_preset_name) + if imgui.button("Save", imgui.ImVec2(120, 0)): + if app._new_preset_name.strip(): + ini_data = imgui.save_ini_settings_to_memory() + app.layout_presets[app._new_preset_name.strip()] = { + "ini": ini_data, + "multi_viewport": app.ui_multi_viewport + } + app.config["layout_presets"] = app.layout_presets + models.save_config(app.config) + app._show_save_preset_modal = False + app._new_preset_name = "" + imgui.close_current_popup() + imgui.same_line() + if imgui.button("Cancel", imgui.ImVec2(120, 0)): + app._show_save_preset_modal = False + imgui.close_current_popup() + + +def render_main_interface(app: App) -> None: + render_error_tint(app) + app.perf_monitor.start_frame() + app._autofocus_response_tab = app.controller._autofocus_response_tab + + #region: Process GUI task queue + app._process_pending_gui_tasks() + app._process_pending_history_adds() + if app.controller._process_pending_tool_calls(): app._tool_log_dirty = True + #endregion: Process GUI task queue + + render_track_proposal_modal(app) + render_patch_modal(app) + render_base_prompt_diff_modal(app) + render_save_preset_modal(app) + render_save_workspace_profile_modal(app) + render_add_context_files_modal(app) + render_preset_manager_window(app) + render_tool_preset_manager_window(app) + render_persona_editor_window(app) + + # Auto-save (every 60s) + now = time.time() + if now - app._last_autosave >= app._autosave_interval: + app._last_autosave = now + try: + app._flush_to_project() + app._flush_to_config() + models.save_config(app.config) + except Exception: + pass # silent — don't disrupt the GUI loop + + # Sync pending comms + with app._pending_comms_lock: + if app._pending_comms: + if app.ui_auto_scroll_comms: app._scroll_comms_to_bottom = True + app._comms_log_dirty = True + for c in app._pending_comms: app._comms_log.append(c) + app._pending_comms.clear() + + if app.ui_focus_agent != app._last_ui_focus_agent: + app._comms_log_dirty = True + app._tool_log_dirty = True + app._last_ui_focus_agent = app.ui_focus_agent + + if app._comms_log_dirty: + if app.is_viewing_prior_session: app._comms_log_cache = app.prior_session_entries + else: + log_raw = list(app._comms_log) + if app.ui_focus_agent: app._comms_log_cache = [e for e in log_raw if e.get("source_tier", "").startswith(app.ui_focus_agent)] + else: app._comms_log_cache = log_raw + app._comms_log_dirty = False + + if app._tool_log_dirty: + if app.is_viewing_prior_session: app._tool_log_cache = app.prior_tool_calls + else: + log_raw = list(app._tool_log) + if app.ui_focus_agent: app._tool_log_cache = [e for e in log_raw if e.get("source_tier", "").startswith(app.ui_focus_agent)] + else: app._tool_log_cache = log_raw + app._tool_log_dirty = False + + app._render_window_if_open("Project Settings", app._render_project_settings_hub) + app._render_window_if_open("Files & Media", app._render_files_and_media) + app._render_window_if_open("AI Settings", app._render_ai_settings_hub) + app._render_window_if_open("Usage Analytics", app._render_usage_analytics_panel, app.ui_separate_usage_analytics) + app._render_window_if_open("MMA Dashboard", app._render_mma_dashboard) + app._render_window_if_open("Task DAG", app._render_task_dag_panel, app.ui_separate_task_dag) + + app._render_window_if_open("Tier 1: Strategy", lambda: render_tier_stream_panel(app, "Tier 1", "Tier 1"), app.ui_separate_tier1) + app._render_window_if_open("Tier 2: Tech Lead", lambda: render_tier_stream_panel(app, "Tier 2", "Tier 2 (Tech Lead)"), app.ui_separate_tier2) + app._render_window_if_open("Tier 3: Workers", lambda: render_tier_stream_panel(app, "Tier 3", None), app.ui_separate_tier3) + app._render_window_if_open("Tier 4: QA", lambda: render_tier_stream_panel(app, "Tier 4", "Tier 4 (QA)"), app.ui_separate_tier4) + + if app.show_windows.get("Theme", False): render_theme_panel(app) + + app._render_window_if_open("Discussion Hub", app._render_discussion_hub) + app._render_window_if_open("Operations Hub", app._render_operations_hub) + + app._render_window_if_open("Message", app._render_message_panel, app.ui_separate_message_panel) + app._render_window_if_open("Response", app._render_response_panel, app.ui_separate_response_panel) + app._render_window_if_open("Tool Calls", app._render_tool_calls_panel, app.ui_separate_tool_calls_panel) + app._render_window_if_open("External Tools", app._render_external_tools_panel, app.ui_separate_external_tools) + app._render_window_if_open("Log Management", app._render_log_management) + app._render_window_if_open("Diagnostics", app._render_diagnostics_panel) + + app.perf_monitor.end_frame() + + # Modals / Popups + render_approve_script_modal(app) + render_mma_modals(app) + + +def render_custom_title_bar(app: App) -> None: + # Obsolete, removed since it renders behind the full screen dock space. + # 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) + + +def render_cache_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_cache_panel") + if app.current_provider != "gemini": + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_cache_panel") + return + imgui.text_colored(C_LBL, 'Cache Analytics') + stats = getattr(app.controller, '_cached_cache_stats', {}) + if not stats.get("cache_exists"): + imgui.text_disabled("No active cache") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_cache_panel") + return + age_sec = stats.get("cache_age_seconds", 0) + ttl_remaining = stats.get("ttl_remaining", 0) + ttl_total = stats.get("ttl_seconds", 3600) + age_str = f"{age_sec/60:.0f}m {age_sec%60:.0f}s" + remaining_str = f"{ttl_remaining/60:.0f}m {ttl_remaining%60:.0f}s" + ttl_pct = (ttl_remaining / ttl_total * 100) if ttl_total > 0 else 0 + imgui.text(f"Age: {age_str}") + imgui.text(f"TTL: {remaining_str} ({ttl_pct:.0f}%)") + color = imgui.ImVec4(0.2, 0.8, 0.2, 1.0) + if ttl_pct < 20: color = imgui.ImVec4(1.0, 0.2, 0.2, 1.0) + elif ttl_pct < 50: color = imgui.ImVec4(1.0, 0.8, 0.0, 1.0) + imgui.push_style_color(imgui.Col_.plot_histogram, color) + imgui.progress_bar(ttl_pct / 100.0, imgui.ImVec2(-1, 0), f"{ttl_pct:.0f}%") + imgui.pop_style_color() + if imgui.button("Clear Cache"): + app.controller.clear_cache() + app._cache_cleared_timestamp = time.time() + if hasattr(app, '_cache_cleared_timestamp') and time.time() - app._cache_cleared_timestamp < 5: + imgui.text_colored(imgui.ImVec4(0.2, 1.0, 0.2, 1.0), "Cache cleared - will rebuild on next request") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_cache_panel") + + +def render_tool_analytics_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_tool_analytics_panel") + imgui.text_colored(C_LBL, 'Tool Usage') + imgui.separator() + now = time.time() + if not hasattr(app, '_tool_stats_cache_time') or now - app._tool_stats_cache_time > 1.0: + app._cached_tool_stats = getattr(app.controller, '_tool_stats', {}) + tool_stats = getattr(app.controller, '_cached_tool_stats', {}) + if not tool_stats: + imgui.text_disabled("No tool usage data") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_tool_analytics_panel") + return + if imgui.begin_table("tool_stats", 4, imgui.TableFlags_.borders | imgui.TableFlags_.sortable): + imgui.table_setup_column("Tool") + imgui.table_setup_column("Count") + imgui.table_setup_column("Avg (ms)") + imgui.table_setup_column("Fail %") + imgui.table_headers_row() + sorted_tools = sorted(tool_stats.items(), key=lambda x: -x[1].get("count", 0)) + for tool_name, stats in sorted_tools: + count = stats.get("count", 0) + total_time = stats.get("total_time_ms", 0) + failures = stats.get("failures", 0) + avg_time = total_time / count if count > 0 else 0 + fail_pct = (failures / count * 100) if count > 0 else 0 + imgui.table_next_row() + imgui.table_set_column_index(0) + imgui.text(tool_name) + imgui.table_set_column_index(1) + imgui.text(str(count)) + imgui.table_set_column_index(2) + imgui.text(f"{avg_time:.0f}") + imgui.table_set_column_index(3) + if fail_pct > 0: imgui.text_colored(imgui.ImVec4(1.0, 0.2, 0.2, 1.0), f"{fail_pct:.0f}%") + else: imgui.text("0%") + imgui.end_table() + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_tool_analytics_panel") + + +def render_token_budget_panel(app: App) -> None: + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_token_budget_panel") + imgui.text_colored(C_LBL, 'Prompt Utilization') + usage = app.session_usage + total = usage["input_tokens"] + usage["output_tokens"] + if total == 0 and usage.get("total_tokens", 0) > 0: total = usage["total_tokens"] + render_selectable_label(app, "session_telemetry_tokens", f"Tokens: {total:,} (In: {usage['input_tokens']:,} Out: {usage['output_tokens']:,})", width=-1, color=C_RES) + if usage.get("last_latency", 0.0) > 0: imgui.text_colored(C_LBL, f" Last Latency: {usage['last_latency']:.2f}s") + if usage["cache_read_input_tokens"]: imgui.text_colored(C_LBL, f" Cache Read: {usage['cache_read_input_tokens']:,} Creation: {usage['cache_creation_input_tokens']:,}") + if app._gemini_cache_text: imgui.text_colored(C_SUB, app._gemini_cache_text) + imgui.separator() + + if app._token_stats_dirty: + app._token_stats_dirty = False + # Offload to background thread via event queue + app.controller.event_queue.put("refresh_api_metrics", {"md_content": app._last_stable_md or ""}) + stats = app._token_stats + if not stats: + imgui.text_disabled("Token stats unavailable") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_token_budget_panel") + return + pct = stats.get("utilization_pct", 0.0) + current = stats.get("estimated_prompt_tokens", stats.get("total_tokens", 0)) + limit = stats.get("max_prompt_tokens", 0) + headroom = stats.get("headroom_tokens", max(0, limit - current)) + if pct < 50.0: color = imgui.ImVec4(0.2, 0.8, 0.2, 1.0) + elif pct < 80.0: color = imgui.ImVec4(1.0, 0.8, 0.0, 1.0) + else: color = imgui.ImVec4(1.0, 0.2, 0.2, 1.0) + imgui.push_style_color(imgui.Col_.plot_histogram, color) + imgui.progress_bar(pct / 100.0, imgui.ImVec2(-1, 0), f"{pct:.1f}%") + imgui.pop_style_color() + imgui.text_disabled(f"{current:,} / {limit:,} tokens ({headroom:,} remaining)") + sys_tok = stats.get("system_tokens", 0) + tool_tok = stats.get("tools_tokens", 0) + hist_tok = stats.get("history_tokens", 0) + total_tok = sys_tok + tool_tok + hist_tok or 1 + if imgui.begin_table("token_breakdown", 3, imgui.TableFlags_.borders_inner_h | imgui.TableFlags_.sizing_fixed_fit): + imgui.table_setup_column("Component") + imgui.table_setup_column("Tokens") + imgui.table_setup_column("Pct") + imgui.table_headers_row() + for lbl, tok in [("System", sys_tok), ("Tools", tool_tok), ("History", hist_tok)]: + imgui.table_next_row() + imgui.table_set_column_index(0); imgui.text(lbl) + imgui.table_set_column_index(1); imgui.text(f"{tok:,}") + imgui.table_set_column_index(2); imgui.text(f"{tok / total_tok * 100:.0f}%") + imgui.end_table() + imgui.separator() + imgui.text("MMA Tier Costs") + if hasattr(app, 'mma_tier_usage') and app.mma_tier_usage: + if imgui.begin_table("tier_cost_breakdown", 4, imgui.TableFlags_.borders_inner_h | imgui.TableFlags_.sizing_fixed_fit): + imgui.table_setup_column("Tier") + imgui.table_setup_column("Model") + imgui.table_setup_column("Tokens") + imgui.table_setup_column("Est. Cost") + imgui.table_headers_row() + for tier, stats in app.mma_tier_usage.items(): + model = stats.get('model', 'unknown') + in_t = stats.get('input', 0) + out_t = stats.get('output', 0) + tokens = in_t + out_t + cost = cost_tracker.estimate_cost(model, in_t, out_t) + imgui.table_next_row() + imgui.table_set_column_index(0); render_selectable_label(app, f"tier_{tier}", tier, width=-1) + imgui.table_set_column_index(1); render_selectable_label(app, f"model_{tier}", model.split("-")[0], width=-1) + imgui.table_set_column_index(2); render_selectable_label(app, f"tokens_{tier}", f"{tokens:,}", width=-1) + imgui.table_set_column_index(3); render_selectable_label(app, f"cost_{tier}", f"${cost:.4f}", width=-1, color=imgui.ImVec4(0.2, 0.9, 0.2, 1)) + imgui.end_table() + tier_total = sum(cost_tracker.estimate_cost(stats.get('model', ''), stats.get('input', 0), stats.get('output', 0)) for stats in app.mma_tier_usage.values()) + render_selectable_label(app, "session_total_cost", f"Session Total: ${tier_total:.4f}", width=-1, color=imgui.ImVec4(0, 1, 0, 1)) + else: + imgui.text_disabled("No MMA tier usage data") + if stats.get("would_trim"): + imgui.text_colored(imgui.ImVec4(1.0, 0.3, 0.0, 1.0), "WARNING: Next call will trim history") + trimmable = stats.get("trimmable_turns", 0) + if trimmable: imgui.text_disabled(f"Trimmable turns: {trimmable}") + msgs = stats.get("messages") + if msgs: + shown = 0 + for msg in msgs: + if shown >= 3: break + if msg.get("trimmable"): + role = msg.get("role", "?") + toks = msg.get("tokens", 0) + imgui.text_disabled(f" [{role}] ~{toks:,} tokens") + shown += 1 + imgui.separator() + cache_stats = getattr(app.controller, '_cached_cache_stats', {}) + if cache_stats.get("cache_exists"): + age = cache_stats.get("cache_age_seconds", 0) + ttl = cache_stats.get("ttl_seconds", 3600) + imgui.text_colored(C_LBL, f"Cache Usage: ACTIVE | Age: {age:.0f}s / {ttl}s | Renews at: {ttl * 0.9:.0f}s") + else: + imgui.text_disabled("Cache Usage: INACTIVE") + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_token_budget_panel") + + +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] @@ -3127,6 +3980,7 @@ def render_context_composition_panel(app: App) -> None: 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] @@ -3266,45 +4120,6 @@ def render_ast_inspector_modal(app: App) -> None: if not opened: app.ui_inspecting_ast_file = None -def render_add_context_files_modal(app: App) -> None: - """ - [C: tests/test_auto_slices.py:test_add_selected_triggers_auto_slices] - """ - if imgui.begin_popup_modal("Select Context Files", None, imgui.WindowFlags_.always_auto_resize)[0]: - imgui.text("Select files from project to add to context:") - imgui.begin_child("ctx_picker_list", imgui.ImVec2(600, 300), True) - if True: - # Create a temporary selection set if not initialized - if not hasattr(app, '_ui_picker_selected'): app._ui_picker_selected = set() - for f in app.files: - fpath = f.path if hasattr(f, 'path') else str(f) - # Skip if already in context - if any((cf.path if hasattr(cf, 'path') else str(cf)) == fpath for cf in app.context_files): - continue - is_sel = fpath in app._ui_picker_selected - clicked, new_sel = imgui.checkbox(f"{fpath}##picker_{fpath}", is_sel) - if clicked: - if new_sel: - app._ui_picker_selected.add(fpath) - else: - app._ui_picker_selected.discard(fpath) - imgui.end_child() - imgui.separator() - - if imgui.button("Add Selected", imgui.ImVec2(120, 0)): - for fpath in app._ui_picker_selected: - f_item = models.FileItem(path=fpath) - app.context_files.append(f_item) - app._populate_auto_slices(f_item) - app._ui_picker_selected.clear() - imgui.close_current_popup() - - imgui.same_line() - if imgui.button("Cancel", imgui.ImVec2(120, 0)): - if hasattr(app, '_ui_picker_selected'): - app._ui_picker_selected.clear() - imgui.close_current_popup() - imgui.end_popup() def render_save_workspace_profile_modal(app: App) -> None: if app._show_save_workspace_profile_modal: @@ -3333,6 +4148,7 @@ def render_save_workspace_profile_modal(app: App) -> None: imgui.end_popup() + def render_context_presets_panel(app: App) -> None: imgui.text_colored(C_IN, "Context Presets") imgui.separator() @@ -3355,44 +4171,10 @@ def render_context_presets_panel(app: App) -> None: 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_batch_actions(app: App, total_lines: int, total_ast: int) -> None: - imgui.text("Batch:") - for mode in ["full", "summary", "skeleton", "outline", "masked", "none"]: - if imgui.button(f"{mode.capitalize()}##batch"): - for f in app.context_files: - f_path = f.path if hasattr(f, "path") else str(f) - if f_path in app.ui_selected_context_files: f.view_mode = mode - imgui.same_line() - if imgui.button("Sel All##selall"): - for f in app.context_files: - f_path = f.path if hasattr(f, "path") else str(f) - app.ui_selected_context_files.add(f_path) - imgui.same_line() - if imgui.button("Unsel All##unselall"): app.ui_selected_context_files.clear() - imgui.same_line() - if imgui.button("Add Files"): imgui.open_popup("Select Context Files") - imgui.same_line() - if imgui.button("Add All##addall"): - context_paths = {f.path if hasattr(f, "path") else str(f) for f in app.context_files} - for f in app.files: - f_path = f.path if hasattr(f, "path") else str(f) - if f_path not in context_paths: - f_copy = copy.deepcopy(f) - app.context_files.append(f_copy) - app._populate_auto_slices(f_copy) - imgui.same_line() - if imgui.button("Del##batch"): - new_files = [] - for f in app.context_files: - f_path = f.path if hasattr(f, "path") else str(f) - if f_path not in app.ui_selected_context_files: new_files.append(f) - app.context_files = new_files - app.ui_selected_context_files.clear() - imgui.same_line() - imgui.text(f" | Total: {len(app.context_files)} files, {total_lines} lines, {total_ast} AST elements") def render_context_files_table(app: App) -> None: imgui.dummy(imgui.ImVec2(0, 4)) @@ -3499,6 +4281,7 @@ def render_context_files_table(app: App) -> None: 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', {}) @@ -3527,17 +4310,6 @@ def render_context_presets(app: App) -> None: app.delete_context_preset(app.ui_active_context_preset) app.ui_active_context_preset = "" -def render_discussion_hub(app: App) -> None: - with imscope.tab_bar("discussion_hub_tabs"): - with imscope.tab_item("Discussion") as (exp, opened): - if exp: render_discussion_tab(app) - with imscope.tab_item("Context Composition") as (exp, opened): - if exp: render_context_composition_panel(app) - with imscope.tab_item("Snapshot") as (exp, opened): - if exp: render_snapshot_tab(app) - with imscope.tab_item("Takes") as (exp, opened): - if exp: render_takes_panel(app) - return def render_discussion_entries(app: App) -> None: with imscope.child("disc_scroll"): @@ -3553,50 +4325,6 @@ def render_discussion_entries(app: App) -> None: render_discussion_entry(app, display_entries[i], i) if app._scroll_disc_to_bottom: imgui.set_scroll_here_y(1.0); app._scroll_disc_to_bottom = False -def render_discussion_entry(app: App, entry: dict, index: int) -> None: - with imscope.id(f"disc_{index}"): - collapsed, read_mode = entry.get("collapsed", False), entry.get("read_mode", False) - if imgui.button("+" if collapsed else "-"): entry["collapsed"] = not collapsed - imgui.same_line(); render_text_viewer(app, f"Entry #{index+1}", entry["content"]); imgui.same_line(); imgui.set_next_item_width(120) - if imgui.begin_combo("##role", entry["role"]): - for r in app.disc_roles: - if imgui.selectable(r, r == entry["role"])[0]: entry["role"] = r - imgui.end_combo() - if not collapsed: - imgui.same_line() - if imgui.button("[Edit]" if read_mode else "[Read]"): entry["read_mode"] = not read_mode - ts_str = entry.get("ts", "") - if ts_str: - imgui.same_line(); imgui.text_colored(vec4(120, 120, 100), str(ts_str)); e_dt = project_manager.parse_ts(ts_str) - if e_dt: - e_unix, next_unix = e_dt.timestamp(), float('inf') - if index + 1 < len(app.disc_entries): - n_ts = app.disc_entries[index+1].get("ts", ""); n_dt = project_manager.parse_ts(n_ts) - if n_dt: next_unix = n_dt.timestamp() - injected = [f for f in app.files if hasattr(f, 'injected_at') and f.injected_at and e_unix <= f.injected_at < next_unix] - if injected: - imgui.same_line(); imgui.text_colored(vec4(100, 255, 100), f"[{len(injected)}+]") - if imgui.is_item_hovered(): imgui.set_tooltip("Files injected at this point:\n" + "\n".join([f.path for f in injected])) - if collapsed: - imgui.same_line() - if imgui.button("Ins"): app.disc_entries.insert(index, {"role": "User", "content": "", "collapsed": True, "ts": project_manager.now_ts()}) - imgui.same_line() - if imgui.button("Del"): app.disc_entries.pop(index); return - imgui.same_line() - if imgui.button("Branch"): app._branch_discussion(index) - imgui.same_line(); preview = entry["content"].replace("\n", " ")[:60] - if len(entry["content"]) > 60: preview += "..." - if not preview.strip() and entry.get("thinking_segments"): - preview = entry["thinking_segments"][0]["content"].replace("\n", " ")[:60] - if len(entry["thinking_segments"][0]["content"]) > 60: preview += "..." - imgui.text_colored(vec4(160, 160, 150), preview) - if not collapsed: - thinking_segments, has_content = entry.get("thinking_segments", []), bool(entry.get("content", "").strip()) - if thinking_segments: render_thinking_trace(app, thinking_segments, index, is_standalone=not has_content) - if read_mode: render_discussion_entry_read_mode(app, entry, index) - else: - if not (bool(thinking_segments) and not has_content): ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150)) - imgui.separator() def render_discussion_entry_controls(app: App) -> None: if imgui.button("+ Entry"): app.disc_entries.append({"role": app.disc_roles[0] if app.disc_roles else "User", "content": "", "collapsed": True, "ts": project_manager.now_ts()}) @@ -3619,51 +4347,6 @@ def render_discussion_entry_controls(app: App) -> None: with app._disc_entries_lock: app.disc_entries = truncate_entries(app.disc_entries, app.ui_disc_truncate_pairs) app.ai_status = f"history truncated to {app.ui_disc_truncate_pairs} pairs" -def render_discussion_entry_read_mode(app: App, entry: dict, index: int) -> None: - content = entry["content"] - if not content.strip(): return - if '## Retrieved Context' in content: - rag_match = re.search(r'## Retrieved Context\n\n([\s\S]*?)(?=\n\n#|\Z)', content) - if rag_match: - rag_section = rag_match.group(1) - if imgui.collapsing_header('Retrieved Context'): - chunks = re.finditer(r'### Chunk (\d+) \(Source: (.*?)\)\n([\s\S]*?)(?=\n### Chunk|\Z)', rag_section) - for chunk_match in chunks: - idx, path, chunk_content = chunk_match.group(1), chunk_match.group(2), chunk_match.group(3) - if imgui.collapsing_header(f'Chunk {idx}: {path}'): - if imgui.button(f'[Source]##rag_{index}_{idx}'): - res = mcp_client.read_file(path) - if res: app.text_viewer_title, app.text_viewer_content, app.text_viewer_type, app.show_text_viewer = path, res, (Path(path).suffix.lstrip('.') if Path(path).suffix else 'text'), True - imgui.text_unformatted(chunk_content) - content = content[:rag_match.start()] + content[rag_match.end():] - pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?") - matches, is_nerv = list(pattern.finditer(content)), theme.is_nerv_active() - if not matches: - with theme.ai_text_style(): - markdown_helper.render(content, context_id=f'disc_{index}') - else: - with imscope.child(f"read_content_{index}", size_y=150, flags=True): - if app.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) - last_idx = 0 - for m_idx, match in enumerate(matches): - before = content[last_idx:match.start()] - if before: - with theme.ai_text_style(): - markdown_helper.render(before, context_id=f'disc_{index}_b_{m_idx}') - header_text, path, code_block = match.group(0).split("\n")[0].strip(), match.group(2), match.group(4) - if imgui.collapsing_header(header_text): - if imgui.button(f"[Source]##{index}_{match.start()}"): - res = mcp_client.read_file(path) - if res: app.text_viewer_title, app.text_viewer_content, app.text_viewer_type, app.show_text_viewer = path, res, (Path(path).suffix.lstrip('.') if Path(path).suffix else 'text'), True - if code_block: - with theme.ai_text_style(): - markdown_helper.render(code_block, context_id=f'disc_{index}_c_{m_idx}') - last_idx = match.end() - after = content[last_idx:] - if after: - with theme.ai_text_style(): - markdown_helper.render(after, context_id=f'disc_{index}_a') - if app.ui_word_wrap: imgui.pop_text_wrap_pos() def render_discussion_metadata(app: App) -> None: disc_data = app.project.get("discussion", {}).get("discussions", {}).get(app.active_discussion, {}) @@ -3687,6 +4370,7 @@ def render_discussion_metadata(app: App) -> None: imgui.same_line() if imgui.button("Delete"): app._delete_discussion(app.active_discussion) + def render_discussion_panel(app: App) -> None: """ [C: tests/test_discussion_takes_gui.py:test_render_discussion_tabs, tests/test_discussion_takes_gui.py:test_switching_discussion_via_tabs, tests/test_gui_discussion_tabs.py:test_discussion_tabs_rendered, tests/test_gui_fast_render.py:test_render_discussion_panel_fast, tests/test_gui_phase4.py:test_track_discussion_toggle, tests/test_gui_symbol_navigation.py:test_render_discussion_panel_symbol_lookup] @@ -3709,6 +4393,7 @@ def render_discussion_panel(app: App) -> None: if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_discussion_panel") return + def render_discussion_roles(app: App) -> None: if imgui.collapsing_header("Roles"): with imscope.child("roles_scroll", size_y=100, flags=True): @@ -3722,6 +4407,7 @@ def render_discussion_roles(app: App) -> None: if r and r not in app.disc_roles: app.disc_roles.append(r); app.ui_disc_new_role_input = "" return + def render_discussion_selector(app: App) -> None: if not imgui.collapsing_header("Discussions", imgui.TreeNodeFlags_.default_open): return @@ -3767,6 +4453,7 @@ def render_discussion_selector(app: App) -> None: render_discussion_metadata(app) return + def render_discussion_tab(app: App) -> None: imgui.begin_child("HistoryChild", size=(0, -app.ui_discussion_split_h)) if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_discussion_panel") @@ -3802,6 +4489,7 @@ 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() @@ -3858,6 +4546,7 @@ def render_takes_panel(app: App) -> None: app.disc_entries.append({"role": "user", "content": prompt, "collapsed": False, "ts": project_manager.now_ts()}) app._handle_generate_send() + 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 @@ -3883,6 +4572,7 @@ def render_prior_session_view(app: App) -> None: 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: @@ -3891,44 +4581,6 @@ def render_thinking_indicator(app: App) -> None: 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_message_panel(app: App) -> None: - if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_message_panel") -# LIVE indicator - is_live = app.ai_status in ["running powershell...", "fetching url...", "searching web...", "powershell done, awaiting AI..."] - if is_live: - val = math.sin(time.time() * 10 * math.pi) - alpha = 1.0 if val > 0 else 0.0 - c = imgui.ImVec4(0.39, 1.0, 0.39, alpha) - if theme.is_nerv_active(): c = vec4(80, 255, 80, alpha) # DATA_GREEN for LIVE in NERV - imgui.text_colored(c, "LIVE") - imgui.separator() - ch, app.ui_ai_input = imgui.input_text_multiline("##ai_in", app.ui_ai_input, imgui.ImVec2(-1, -40)) - # Keyboard shortcuts - io = imgui.get_io() - ctrl_l = io.key_ctrl and imgui.is_key_pressed(imgui.Key.l) - if ctrl_l: app.ui_ai_input = "" - imgui.separator() - is_busy = app.ai_status in ['sending...', 'streaming...'] - send_busy = False - with app._send_thread_lock: - if app.send_thread and app.send_thread.is_alive(): send_busy = True - if is_busy: send_busy = True - - imgui.begin_disabled(send_busy) - ctrl_enter = io.key_ctrl and imgui.is_key_pressed(imgui.Key.enter) - label = "Gen + Send (Busy)" if send_busy else "Gen + Send" - if (imgui.button(label) or ctrl_enter) and not send_busy: app._handle_generate_send() - imgui.end_disabled() - imgui.same_line() - if imgui.button("MD Only"): app._handle_md_only() - imgui.same_line() - if imgui.button("Inject File"): app.show_inject_modal = True - imgui.same_line() - if imgui.button("-> History"): - if app.ui_ai_input: app.disc_entries.append({"role": "User", "content": app.ui_ai_input, "collapsed": False, "ts": project_manager.now_ts()}) - imgui.same_line() - if imgui.button("Reset"): app._handle_reset_session() - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_message_panel") def render_synthesis_panel(app: App) -> None: """ @@ -3962,6 +4614,7 @@ def render_synthesis_panel(app: App) -> None: 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]: @@ -3998,150 +4651,6 @@ def render_snapshot_tab(app: App) -> None: imgui.end_tab_item() imgui.end_tab_bar() -def render_response_panel(app: App) -> None: - if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_response_panel") - if app._trigger_blink: - app._trigger_blink = False - app._is_blinking = True - app._blink_start_time = time.time() - try: - imgui.set_window_focus("Response") # type: ignore[call-arg] - except: - pass - is_blinking = False - blink_color = vec4(0, 0, 0, 0) - if app._is_blinking: - elapsed = time.time() - app._blink_start_time - if elapsed > 1.5: - app._is_blinking = False - else: - is_blinking = True - val = math.sin(elapsed * 8 * math.pi) - alpha = 50/255 if val > 0 else 0 - blink_color = vec4(0, 255, 0, alpha) - - with imscope.style_color(imgui.Col_.frame_bg, blink_color) if is_blinking else nullcontext(): - with imscope.style_color(imgui.Col_.child_bg, blink_color) if is_blinking else nullcontext(): - with imscope.child("response_scroll_area", 0, -40, True): - with theme.ai_text_style(): - segments, parsed_response = thinking_parser.parse_thinking_trace(app.ai_response) - if segments: - render_thinking_trace(app, [{"content": s.content, "marker": s.marker} for s in segments], 9999) - markdown_helper.render(parsed_response, context_id="response") - - imgui.separator() - if imgui.button("-> History"): - if app.ai_response: - segments, response = thinking_parser.parse_thinking_trace(app.ai_response) - entry = {"role": "AI", "content": response, "collapsed": True, "ts": project_manager.now_ts()} - if segments: entry["thinking_segments"] = [{"content": s.content, "marker": s.marker} for s in segments] - app.disc_entries.append(entry) - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_response_panel") - -def render_operations_hub(app: App) -> None: - imgui.push_style_var(imgui.StyleVar_.item_spacing, imgui.ImVec2(10, 4)) - ch1, app.ui_separate_tool_calls_panel = imgui.checkbox("Pop Out Tool Calls", app.ui_separate_tool_calls_panel) - if ch1: app.show_windows["Tool Calls"] = app.ui_separate_tool_calls_panel - imgui.same_line() - ch2, app.ui_separate_usage_analytics = imgui.checkbox("Pop Out Usage Analytics", app.ui_separate_usage_analytics) - if ch2: app.show_windows["Usage Analytics"] = app.ui_separate_usage_analytics - imgui.same_line() - ch3, app.ui_separate_external_tools = imgui.checkbox('Pop Out External Tools', app.ui_separate_external_tools) - if ch3: app.show_windows['External Tools'] = app.ui_separate_external_tools - imgui.pop_style_var() - show_tc_tab, show_usage_tab = not app.ui_separate_tool_calls_panel, not app.ui_separate_usage_analytics - with imscope.tab_bar("ops_tabs"): - with imscope.tab_item("Comms History") as (exp, _): - if exp: render_comms_history_panel(app) - if show_tc_tab: - with imscope.tab_item("Tool Calls") as (exp, _): - if exp: render_tool_calls_panel(app) - if show_usage_tab: - with imscope.tab_item("Usage Analytics") as (exp, _): - if exp: render_usage_analytics_panel(app) - if not app.ui_separate_external_tools: - with imscope.tab_item("External Tools") as (exp, _): - if exp: - render_external_tools_panel(app) - imgui.separator(); imgui.text("") - try: render_external_editor_panel(app) - except Exception as e: imgui.text_colored(vec4(1, 0.3, 0.3, 1), f"Error: {str(e)}") - with imscope.tab_item("Workspace Layouts") as (exp, _): - if exp: - imgui.text("Experimental: Auto-switch layout by Tier") - ch, app.controller.ui_auto_switch_layout = imgui.checkbox("Enable Auto-Switch", app.controller.ui_auto_switch_layout) - if app.controller.ui_auto_switch_layout: - imgui.separator(); imgui.text("Tier Bindings (select profile for each tier)") - profiles = [""] + [p.name for p in app.controller.workspace_profiles.values()] - for t in ["Tier 1", "Tier 2", "Tier 3", "Tier 4"]: - curr = app.controller.ui_tier_layout_bindings.get(t, ""); idx = profiles.index(curr) if curr in profiles else 0 - ch_combo, new_idx = imgui.combo(t, idx, profiles) - if ch_combo: app.controller.ui_tier_layout_bindings[t] = profiles[new_idx] - -def render_tool_calls_panel(app: App) -> None: - if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_tool_calls_panel") - imgui.text("Tool call history") - imgui.same_line() - if imgui.button("Clear##tc"): - app._tool_log.clear() - app._tool_log_dirty = True - imgui.separator() - - log_to_render = app._tool_log_cache - flags = imgui.TableFlags_.resizable | imgui.TableFlags_.hideable | imgui.TableFlags_.borders_inner_v | imgui.TableFlags_.row_bg | imgui.TableFlags_.scroll_y - - if imgui.begin_table("tool_calls_table", 4, flags, imgui.ImVec2(0, 0)): - imgui.table_setup_column("#", imgui.TableColumnFlags_.width_fixed, 40) - imgui.table_setup_column("Tier", imgui.TableColumnFlags_.width_fixed, 60) - imgui.table_setup_column("Script", imgui.TableColumnFlags_.width_stretch) - imgui.table_setup_column("Result", imgui.TableColumnFlags_.width_fixed, 100) - - imgui.table_headers_row() - - clipper = imgui.ListClipper() - clipper.begin(len(log_to_render)) - while clipper.step(): - for i in range(clipper.display_start, clipper.display_end): - entry = log_to_render[i] - imgui.table_next_row() - - imgui.table_next_column() - imgui.text_colored(C_LBL, f"#{i+1}") - - imgui.table_next_column() - imgui.text_colored(C_SUB, f"[{entry.get('source_tier', 'main')}]") - - imgui.table_next_column() - script = entry.get("script", "") - res = entry.get("result", "") - # Use a clear, formatted combined view for the detail window - combined = f"**COMMAND:**\n```powershell\n{script}\n```\n\n---\n**OUTPUT:**\n```text\n{res}\n```" - - script_preview = script.replace("\n", " ")[:150] - if len(script) > 150: script_preview += "..." - render_selectable_label(app, f'tc_script_{i}', script_preview, width=-1) - if imgui.is_item_clicked(): - app.text_viewer_title = f"Tool Call #{i+1} Details" - app.text_viewer_content = combined - app.text_viewer_type = 'markdown' - app.show_text_viewer = True - - imgui.table_next_column() - res_preview = res.replace("\n", " ")[:30] - if len(res) > 30: res_preview += "..." - render_selectable_label(app, f'tc_res_{i}', res_preview, width=-1) - if imgui.is_item_clicked(): - app.text_viewer_title = f"Tool Call #{i+1} Details" - app.text_viewer_content = combined - app.text_viewer_type = 'markdown' - app.show_text_viewer = True - - imgui.end_table() - - if app._scroll_tool_calls_to_bottom: - imgui.set_scroll_here_y(1.0) - app._scroll_tool_calls_to_bottom = False - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_tool_calls_panel") def render_comms_history_panel(app: App) -> None: if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_comms_history_panel") @@ -4261,6 +4770,7 @@ def render_comms_history_panel(app: App) -> None: 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 @@ -4382,6 +4892,7 @@ def render_text_viewer_window(app: App) -> None: 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 @@ -4411,6 +4922,7 @@ def render_base_prompt_diff_modal(app: App) -> None: imgui.close_current_popup() imgui.end_popup() + def render_patch_modal(app: App) -> None: if not app._show_patch_modal: return @@ -4455,6 +4967,7 @@ def render_patch_modal(app: App) -> None: 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") @@ -4501,6 +5014,7 @@ def render_external_editor_panel(app: App) -> None: 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: @@ -4544,6 +5058,7 @@ def render_approve_script_modal(app: App) -> None: imgui.close_current_popup() imgui.end_popup() + def render_markdown_test(app: App) -> None: imgui.text("Markdown Test Panel") imgui.separator() @@ -4561,12 +5076,14 @@ And ***bold italic*** text. [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] @@ -4682,6 +5199,7 @@ def render_ticket_queue(app: App) -> None: imgui.end_table() + def render_task_dag_panel(app: App) -> None: # 4. Task DAG Visualizer imgui.text("Task DAG") if (app.active_track or app.active_tickets) and app.node_editor_ctx: @@ -4825,6 +5343,7 @@ def render_task_dag_panel(app: App) -> None: # 4. Task DAG Visualizer else: imgui.text_disabled("No active MMA track or tickets.") + def render_beads_tab(app: App) -> None: imgui.text("Beads Graph (Dolt-backed)") if imgui.button("Refresh Beads"): @@ -4866,38 +5385,6 @@ 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_mma_dashboard(app: App) -> None: - """ - Main MMA dashboard interface. - [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_approval_badge_shown_when_ask_dialog_pending, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_approval_badge_shown_when_mma_approval_pending, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_approval_badge_shown_when_spawn_pending, tests/test_mma_approval_indicators.py:TestMMAApprovalIndicators.test_no_approval_badge_when_idle] - """ - if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_mma_dashboard") - render_mma_focus_selector(app) - imgui.separator() - if app.is_viewing_prior_session: - c = vec4(255, 152, 48) if theme.is_nerv_active() else vec4(255, 200, 100) - imgui.text_colored(c, "HISTORICAL VIEW - READ ONLY") - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_mma_dashboard") - return - render_mma_track_summary(app) - imgui.separator() - render_mma_epic_planner(app) - imgui.separator() - if imgui.collapsing_header("Conductor Setup"): render_mma_conductor_setup(app) - imgui.separator() - render_mma_track_browser(app) - imgui.separator() - render_mma_global_controls(app) - imgui.separator() - render_mma_usage_section(app) - imgui.separator() - render_ticket_queue(app) - imgui.separator() - app._render_window_if_open("Task DAG", app._render_task_dag_panel, not app.ui_separate_task_dag) - if app.ui_selected_ticket_id: render_mma_ticket_editor(app) - imgui.separator() - render_mma_agent_streams(app) - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_mma_dashboard") def render_mma_focus_selector(app: App) -> None: """ @@ -4913,368 +5400,6 @@ def render_mma_focus_selector(app: App) -> None: imgui.same_line() if app.ui_focus_agent and imgui.button("x##clear_focus"): app.ui_focus_agent = None -def render_mma_modals(app: App) -> None: - """Renders all MMA-specific approval and info modals.""" - is_nerv = theme.is_nerv_active() - # Tool Execution Approval - if app._pending_ask_dialog: - if not app._ask_dialog_open: - imgui.open_popup("Approve Tool Execution") - app._ask_dialog_open = True - else: - app._ask_dialog_open = False - if imgui.begin_popup_modal("Approve Tool Execution", None, imgui.WindowFlags_.always_auto_resize)[0]: - if not app._pending_ask_dialog or app._ask_tool_data is None: imgui.close_current_popup() - else: - tool_name = app._ask_tool_data.get("tool", "unknown"); tool_args = app._ask_tool_data.get("args", {}) - imgui.text("The AI wants to execute a tool:"); imgui.text_colored(vec4(200, 200, 100), f"Tool: {tool_name}"); imgui.separator() - imgui.text("Arguments:"); imgui.begin_child("ask_args_child", imgui.ImVec2(400, 200), True); imgui.text_unformatted(json.dumps(tool_args, indent=2)); imgui.end_child() - imgui.separator() - if imgui.button("Approve", imgui.ImVec2(120, 0)): app._handle_approve_ask(); imgui.close_current_popup() - imgui.same_line() - if imgui.button("Deny", imgui.ImVec2(120, 0)): app._handle_reject_ask(); imgui.close_current_popup() - imgui.end_popup() - # MMA Step Approval - if app._pending_mma_approvals: - if not app._mma_approval_open: - imgui.open_popup("MMA Step Approval") - app._mma_approval_open, app._mma_approval_edit_mode = True, False - app._mma_approval_payload = app._pending_mma_approvals[0].get("payload", "") - else: app._mma_approval_open = False - if imgui.begin_popup_modal("MMA Step Approval", None, imgui.WindowFlags_.always_auto_resize)[0]: - if not app._pending_mma_approvals: imgui.close_current_popup() - else: - ticket_id = app._pending_mma_approvals[0].get("ticket_id", "??") - imgui.text(f"Ticket {ticket_id} is waiting for tool execution approval."); imgui.separator() - if app._mma_approval_edit_mode: - imgui.text("Edit Raw Payload (Manual Memory Mutation):"); _, app._mma_approval_payload = imgui.input_text_multiline("##mma_payload", app._mma_approval_payload, imgui.ImVec2(600, 400)) - else: - imgui.text("Proposed Tool Call:"); imgui.begin_child("mma_preview", imgui.ImVec2(600, 300), True); imgui.text_unformatted(str(app._pending_mma_approvals[0].get("payload", ""))); imgui.end_child() - imgui.separator() - if imgui.button("Approve", imgui.ImVec2(120, 0)): app._handle_mma_respond(approved=True, payload=app._mma_approval_payload); imgui.close_current_popup() - imgui.same_line() - if imgui.button("Edit Payload" if not app._mma_approval_edit_mode else "Show Original", imgui.ImVec2(120, 0)): app._mma_approval_edit_mode = not app._mma_approval_edit_mode - imgui.same_line() - if imgui.button("Abort Ticket", imgui.ImVec2(120, 0)): app._handle_mma_respond(approved=False); imgui.close_current_popup() - imgui.end_popup() - # MMA Spawn Approval - if app._pending_mma_spawns: - if not app._mma_spawn_open: - imgui.open_popup("MMA Spawn Approval") - app._mma_spawn_open, app._mma_spawn_edit_mode = True, False - app._mma_spawn_prompt, app._mma_spawn_context = app._pending_mma_spawns[0].get("prompt", ""), app._pending_mma_spawns[0].get("context_md", "") - else: app._mma_spawn_open = False - if imgui.begin_popup_modal("MMA Spawn Approval", None, imgui.WindowFlags_.always_auto_resize)[0]: - if not app._pending_mma_spawns: imgui.close_current_popup() - else: - role, ticket_id = app._pending_mma_spawns[0].get("role", "??"), app._pending_mma_spawns[0].get("ticket_id", "??") - imgui.text(f"Spawning {role} for Ticket {ticket_id}"); imgui.separator() - if app._mma_spawn_edit_mode: - imgui.text("Edit Prompt:"); _, app._mma_spawn_prompt = imgui.input_text_multiline("##spawn_prompt", app._mma_spawn_prompt, imgui.ImVec2(800, 200)) - imgui.text("Edit Context MD:"); _, app._mma_spawn_context = imgui.input_text_multiline("##spawn_context", app._mma_spawn_context, imgui.ImVec2(800, 300)) - else: - imgui.text("Proposed Prompt:"); imgui.begin_child("spawn_prompt_preview", imgui.ImVec2(800, 150), True); imgui.text_unformatted(app._mma_spawn_prompt); imgui.end_child() - imgui.text("Proposed Context MD:"); imgui.begin_child("spawn_context_preview", imgui.ImVec2(800, 250), True); imgui.text_unformatted(app._mma_spawn_context); imgui.end_child() - imgui.separator() - if imgui.button("Approve", imgui.ImVec2(120, 0)): app._handle_mma_respond(approved=True, prompt=app._mma_spawn_prompt, context_md=app._mma_spawn_context); imgui.close_current_popup() - imgui.same_line() - if imgui.button("Edit Mode" if not app._mma_spawn_edit_mode else "Preview Mode", imgui.ImVec2(120, 0)): app._mma_spawn_edit_mode = not app._mma_spawn_edit_mode - imgui.same_line() - if imgui.button("Abort", imgui.ImVec2(120, 0)): app._handle_mma_respond(approved=False, abort=True); imgui.close_current_popup() - imgui.end_popup() - # Cycle Detection - if imgui.begin_popup_modal("Cycle Detected!", None, imgui.WindowFlags_.always_auto_resize)[0]: - imgui.text_colored(imgui.ImVec4(1, 0.3, 0.3, 1), "The dependency graph contains a cycle!") - imgui.text("Please remove the circular dependency.") - if imgui.button("OK"): imgui.close_current_popup() - imgui.end_popup() - -def render_mma_track_summary(app: App) -> None: - """ - [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] - """ - is_nerv = theme.is_nerv_active() - track_name = app.active_track.description if app.active_track else "None" - if getattr(app, "ui_project_execution_mode", "native") == "beads": track_name = "Beads Graph" - track_stats = project_manager.calculate_track_progress(app.active_track.tickets if app.active_track else app.active_tickets) - total_cost = sum(cost_tracker.estimate_cost(u.get('model','unknown'), u.get('input',0), u.get('output',0)) for u in app.mma_tier_usage.values()) - imgui.text("Track:"); imgui.same_line(); imgui.text_colored(C_VAL, track_name); imgui.same_line(); imgui.text(" | Status:"); imgui.same_line() - if app.mma_status == "paused": - imgui.text_colored(vec4(255, 152, 48) if is_nerv else imgui.ImVec4(1, 0.5, 0, 1), "PIPELINE PAUSED"); imgui.same_line() - status_col = imgui.ImVec4(1, 1, 1, 1) - if app.mma_status == "idle": status_col = imgui.ImVec4(0.7, 0.7, 0.7, 1) - elif app.mma_status == "running": status_col = vec4(80, 255, 80) if is_nerv else imgui.ImVec4(1, 1, 0, 1) - elif app.mma_status == "done": status_col = imgui.ImVec4(0, 1, 0, 1) - elif app.mma_status == "error": status_col = vec4(255, 72, 64) if is_nerv else imgui.ImVec4(1, 0, 0, 1) - elif app.mma_status == "paused": status_col = imgui.ImVec4(1, 0.5, 0, 1) - imgui.text_colored(status_col, app.mma_status.upper()); imgui.same_line(); imgui.text(" | Cost:"); imgui.same_line(); imgui.text_colored(imgui.ImVec4(0, 1, 0, 1), f"${total_cost:,.4f}") - perc = track_stats["percentage"] / 100.0 - p_color = imgui.ImVec4(1, 0, 0, 1) if track_stats["percentage"] < 33 else (imgui.ImVec4(1, 1, 0, 1) if track_stats["percentage"] < 66 else imgui.ImVec4(0, 1, 0, 1)) - imgui.push_style_color(imgui.Col_.plot_histogram, p_color); imgui.progress_bar(perc, imgui.ImVec2(-1, 0), f"{track_stats['percentage']:.1f}%"); imgui.pop_style_color() - if imgui.begin_table("ticket_stats_breakdown", 4): - for lbl, val in [("Completed:", track_stats["completed"]), ("In Progress:", track_stats["in_progress"]), ("Blocked:", track_stats["blocked"]), ("Todo:", track_stats["todo"])]: - imgui.table_next_column(); imgui.text_colored(C_LBL, lbl); imgui.same_line(); imgui.text_colored(C_VAL, str(val)) - imgui.end_table() - if app.active_track: - remaining = track_stats["total"] - track_stats["completed"] - eta_mins = (app._avg_ticket_time * remaining) / 60.0 - imgui.text_colored(C_LBL, "ETA:"); imgui.same_line(); imgui.text_colored(C_VAL, f"~{int(eta_mins)}m ({remaining} tickets remaining)") - -def render_mma_epic_planner(app: App) -> None: - """ - [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] - """ - imgui.text_colored(C_LBL, 'Epic Planning (Tier 1)') - _, app.ui_epic_input = imgui.input_text_multiline('##epic_input', app.ui_epic_input, imgui.ImVec2(-1, 80)) - if imgui.button('Plan Epic (Tier 1)', imgui.ImVec2(-1, 0)): app._cb_plan_epic() - -def render_mma_conductor_setup(app: App) -> None: - """ - [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] - """ - if imgui.button("Run Setup Scan"): app._cb_run_conductor_setup() - if app.ui_conductor_setup_summary: imgui.input_text_multiline("##setup_summary", app.ui_conductor_setup_summary, imgui.ImVec2(-1, 120), imgui.InputTextFlags_.read_only) - -def render_mma_track_browser(app: App) -> None: - """ - [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] - """ - imgui.text("Track Browser") - if imgui.begin_table("mma_tracks_table", 4, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable): - imgui.table_setup_column("Title"); imgui.table_setup_column("Status"); imgui.table_setup_column("Progress"); imgui.table_setup_column("Actions"); imgui.table_headers_row() - for track in app.tracks: - imgui.table_next_row(); imgui.table_next_column(); imgui.text(track.get("title", "Untitled")); imgui.table_next_column() - status = track.get("status", "unknown").lower() - c = imgui.ImVec4(0.7, 0.7, 0.7, 1) if status == "new" else (vec4(80, 255, 80) if status == "active" and theme.is_nerv_active() else (imgui.ImVec4(1, 1, 0, 1) if status == "active" else (imgui.ImVec4(0, 1, 0, 1) if status == "done" else (imgui.ImVec4(1, 0, 0, 1) if status == "blocked" else imgui.ImVec4(1, 1, 1, 1))))) - imgui.text_colored(c, status.upper()); imgui.table_next_column() - prog = track.get("progress", 0.0) - p_c = imgui.ImVec4(1, 0, 0, 1) if prog < 0.33 else (imgui.ImVec4(1, 1, 0, 1) if prog < 0.66 else imgui.ImVec4(0, 1, 0, 1)) - imgui.push_style_color(imgui.Col_.plot_histogram, p_c); imgui.progress_bar(prog, imgui.ImVec2(-1, 0), f"{int(prog*100)}%"); imgui.pop_style_color(); imgui.table_next_column() - if imgui.button(f"Load##{track.get('id')}"): app._cb_load_track(str(track.get("id") or "")) - imgui.end_table() - imgui.text("Create New Track") - _, app.ui_new_track_name = imgui.input_text("Name##new_track", app.ui_new_track_name) - _, app.ui_new_track_desc = imgui.input_text_multiline("Description##new_track", app.ui_new_track_desc, imgui.ImVec2(-1, 60)) - imgui.text("Type:"); imgui.same_line() - if imgui.begin_combo("##track_type", app.ui_new_track_type): - for ttype in ["feature", "chore", "fix"]: - if imgui.selectable(ttype, app.ui_new_track_type == ttype)[0]: app.ui_new_track_type = ttype - imgui.end_combo() - if imgui.button("Create Track"): - app._cb_create_track(app.ui_new_track_name, app.ui_new_track_desc, app.ui_new_track_type) - app.ui_new_track_name = ""; app.ui_new_track_desc = "" - -def render_mma_global_controls(app: App) -> None: - """ - [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] - """ - changed, app.mma_step_mode = imgui.checkbox("Step Mode (HITL)", app.mma_step_mode) - imgui.same_line(); imgui.text(f"Status: {app.mma_status.upper()}") - if app.controller and hasattr(app.controller, 'engine') and app.controller.engine and hasattr(app.controller.engine, '_pause_event'): - imgui.same_line() - is_paused = app.controller.engine._pause_event.is_set() - if imgui.button("Resume" if is_paused else "Pause"): - if is_paused: app.controller.engine.resume() - else: app.controller.engine.pause() - if app.active_tier: - imgui.same_line(); imgui.text_colored(C_VAL, f"| Active: {app.active_tier}") - any_pending = len(app._pending_mma_spawns) > 0 or len(app._pending_mma_approvals) > 0 or app._pending_ask_dialog - if any_pending: - alpha = abs(math.sin(time.time() * 5)) - c = vec4(255, 72, 64, alpha) if theme.is_nerv_active() else imgui.ImVec4(1, 0.3, 0.3, alpha) - imgui.same_line(); imgui.text_colored(c, " APPROVAL PENDING"); imgui.same_line() - if imgui.button("Go to Approval"): pass - imgui.separator() - imgui.text("Hot Reload:") - imgui.same_line() - if imgui.button("Reload GUI"): - success = app._trigger_hot_reload() - if success: - imgui.text_colored(imgui.ImVec4(0, 1, 0, 1), "Reloaded!") - else: - imgui.text_colored(imgui.ImVec4(1, 0, 0, 1), f"Error: {app._hot_reload_error or 'Unknown'}") - imgui.same_line(); imgui.text_disabled("(Ctrl+Alt+R)") - -def render_mma_usage_section(app: App) -> None: - """ - [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] - """ - imgui.text("Tier Usage (Tokens & Cost)") - if imgui.begin_table("mma_usage", 5, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg): - imgui.table_setup_column("Tier"); imgui.table_setup_column("Model"); imgui.table_setup_column("Input"); imgui.table_setup_column("Output"); imgui.table_setup_column("Est. Cost"); imgui.table_headers_row() - total_cost = 0.0 - for tier, stats in app.mma_tier_usage.items(): - imgui.table_next_row(); imgui.table_next_column(); imgui.text(tier); imgui.table_next_column(); model = stats.get('model', 'unknown'); imgui.text(model); imgui.table_next_column(); in_t = stats.get('input', 0); imgui.text(f"{in_t:,}"); imgui.table_next_column(); out_t = stats.get('output', 0); imgui.text(f"{out_t:,}"); imgui.table_next_column(); cost = cost_tracker.estimate_cost(model, in_t, out_t); total_cost += cost; imgui.text(f"${cost:,.4f}") - imgui.table_next_row(); imgui.table_set_bg_color(imgui.TableBgTarget_.row_bg0, imgui.get_color_u32(imgui.Col_.plot_lines_hovered)); imgui.table_next_column(); imgui.text("TOTAL"); imgui.table_next_column(); imgui.text(""); imgui.table_next_column(); imgui.text(""); imgui.table_next_column(); imgui.text(""); imgui.table_next_column(); imgui.text(f"${total_cost:,.4f}"); imgui.end_table() - if imgui.collapsing_header("Tier Model Config"): - for tier in app.mma_tier_usage.keys(): - imgui.text(f"{tier}:"); imgui.same_line(); curr_model, curr_prov = app.mma_tier_usage[tier].get("model", "unknown"), app.mma_tier_usage[tier].get("provider", "gemini") - with imscope.id(f"tier_cfg_{tier}"): - imgui.push_item_width(80) - if imgui.begin_combo("##prov", curr_prov): - for p in models.PROVIDERS: - if imgui.selectable(p, p == curr_prov)[0]: - app.mma_tier_usage[tier]["provider"] = p - models_list = app.controller.all_available_models.get(p, []) - if models_list: app.mma_tier_usage[tier]["model"] = models_list[0] - imgui.end_combo() - imgui.pop_item_width(); imgui.same_line(); imgui.push_item_width(150) - models_list = app.controller.all_available_models.get(curr_prov, []) - if imgui.begin_combo("##model", curr_model): - for m in models_list: - if imgui.selectable(m, curr_model == m)[0]: app.mma_tier_usage[tier]["model"] = m - imgui.end_combo() - imgui.pop_item_width(); imgui.same_line(); imgui.push_item_width(-1) - curr_preset = app.mma_tier_usage[tier].get("tool_preset") or "None" - p_names = ["None"] + sorted(app.controller.tool_presets.keys()) - if imgui.begin_combo("##preset", curr_preset): - for pn in p_names: - if imgui.selectable(pn, curr_preset == pn)[0]: app.mma_tier_usage[tier]["tool_preset"] = None if pn == "None" else pn - imgui.end_combo() - imgui.pop_item_width(); imgui.same_line(); imgui.push_item_width(150) - curr_pers = app.mma_tier_usage[tier].get("persona") or "None" - personas = getattr(app.controller, 'personas', {}) - pers_opts = ["None"] + sorted(personas.keys()) - if imgui.begin_combo("##persona", curr_pers): - for pern in pers_opts: - if imgui.selectable(pern, curr_pers == pern)[0]: app.mma_tier_usage[tier]["persona"] = None if pern == "None" else pern - imgui.end_combo() - imgui.pop_item_width() - -def render_mma_ticket_editor(app: App) -> None: - imgui.separator(); imgui.text_colored(C_VAL, f"Editing: {app.ui_selected_ticket_id}") - ticket = next((t for t in app.active_tickets if str(t.get('id', '')) == app.ui_selected_ticket_id), None) - if ticket: - imgui.text(f"Status: {ticket.get('status', 'todo')}"); prio = ticket.get('priority', 'medium') - imgui.text("Priority:"); imgui.same_line() - if imgui.begin_combo(f"##edit_prio_{ticket.get('id')}", prio): - for p_opt in ['high', 'medium', 'low']: - if imgui.selectable(p_opt, p_opt == prio)[0]: ticket['priority'] = p_opt; app._push_mma_state_update() - imgui.end_combo() - imgui.text(f"Target: {ticket.get('target_file', '')}"); imgui.text(f"Depends on: {', '.join(ticket.get('depends_on', []))}") - personas = getattr(app.controller, 'personas', {}); curr_pers = ticket.get('persona_id', '') - imgui.text("Persona Override:"); imgui.same_line() - pers_opts = ["None"] + sorted(personas.keys()); curr_idx = pers_opts.index(curr_pers) + 1 if curr_pers in pers_opts else 0 - _, curr_idx = imgui.combo(f"##ticket_persona_{ticket.get('id')}", curr_idx, pers_opts) - ticket['persona_id'] = None if curr_idx == 0 or pers_opts[curr_idx] == "None" else pers_opts[curr_idx] - if imgui.button(f"Mark Complete##{app.ui_selected_ticket_id}"): ticket['status'] = 'done'; app._push_mma_state_update() - imgui.same_line() - if imgui.button(f"Delete##{app.ui_selected_ticket_id}"): app.active_tickets = [t for t in app.active_tickets if str(t.get('id', '')) != app.ui_selected_ticket_id]; app.ui_selected_ticket_id = None; app._push_mma_state_update() - -def render_mma_agent_streams(app: App) -> None: - """ - [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] - """ - imgui.text("Agent Streams") - if imgui.begin_tab_bar("mma_streams_tabs"): - for tier, label, sep_flag_attr in [("Tier 1", "Tier 1", "ui_separate_tier1"), ("Tier 2", "Tier 2 (Tech Lead)", "ui_separate_tier2"), ("Tier 3", None, "ui_separate_tier3"), ("Tier 4", "Tier 4 (QA)", "ui_separate_tier4")]: - with imscope.tab_item(tier) as (exp, _): - if exp: - sep_val = getattr(app, sep_flag_attr); ch, new_val = imgui.checkbox(f"Pop Out {tier}", sep_val) - if ch: - setattr(app, sep_flag_attr, new_val) - app.show_windows[f"{tier}: Strategy" if tier == "Tier 1" else (f"{tier}: Tech Lead" if tier == "Tier 2" else (f"{tier}: Workers" if tier == "Tier 3" else f"{tier}: QA"))] = new_val - if not new_val: render_tier_stream_panel(app, tier, label) - else: imgui.text_disabled(f"{tier} stream is detached.") - if getattr(app, "ui_project_execution_mode", "native") == "beads": - with imscope.tab_item("Beads") as (exp, _): - if exp: render_beads_tab(app) - imgui.end_tab_bar() - -def render_tier_stream_panel(app: App, tier_key: str, stream_key: str | None) -> None: - """ - [C: tests/test_mma_dashboard_streams.py:TestMMADashboardStreams.test_tier1_renders_stream_content, tests/test_mma_dashboard_streams.py:TestMMADashboardStreams.test_tier3_renders_worker_subheaders] - """ - if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_tier_stream_panel") - if app.is_viewing_prior_session: - imgui.text_colored(vec4(255, 200, 100), "HISTORICAL VIEW - READ ONLY") - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_tier_stream_panel") - return - if stream_key is not None: - content = app.mma_streams.get(stream_key, "") - imgui.begin_child(f"##stream_content_{tier_key}", imgui.ImVec2(-1, -1)) - render_selectable_label(app, f'stream_{tier_key}', content, width=-1, multiline=True, height=0) - try: - if len(content) != app._tier_stream_last_len.get(stream_key, -1): - imgui.set_scroll_here_y(1.0) - app._tier_stream_last_len[stream_key] = len(content) - imgui.end_child() - except (TypeError, AttributeError): - imgui.end_child() - pass - else: - tier3_keys = [k for k in app.mma_streams if "Tier 3" in k] - if not tier3_keys: - imgui.text_disabled("No worker output yet.") - else: - worker_status = getattr(app, '_worker_status', {}) - for key in tier3_keys: - ticket_id = key.split(": ", 1)[-1] if ": " in key else key - status = worker_status.get(key, "unknown") - if status == "running": - imgui.text_colored(imgui.ImVec4(1, 1, 0, 1), f"{ticket_id} [{status}]") - elif status == "completed": - imgui.text_colored(imgui.ImVec4(0, 1, 0, 1), f"{ticket_id} [{status}]") - elif status == "failed": - imgui.text_colored(imgui.ImVec4(1, 0, 0, 1), f"{ticket_id} [{status}]") - else: - imgui.text(f"{ticket_id} [{status}]") - imgui.begin_child(f"##tier3_{ticket_id}_scroll", imgui.ImVec2(-1, 150), True) - render_selectable_label(app, f'stream_t3_{ticket_id}', app.mma_streams[key], width=-1, multiline=True, height=0) - try: - if len(app.mma_streams[key]) != app._tier_stream_last_len.get(key, -1): - imgui.set_scroll_here_y(1.0) - app._tier_stream_last_len[key] = len(app.mma_streams[key]) - imgui.end_child() - except (TypeError, AttributeError): - imgui.end_child() - pass - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_tier_stream_panel") - -def render_track_proposal_modal(app: App) -> None: - if app._show_track_proposal_modal: - imgui.open_popup("Track Proposal") - if imgui.begin_popup_modal("Track Proposal", True, imgui.WindowFlags_.always_auto_resize)[0]: - from src import shaders - p_min = imgui.get_window_pos() - p_max = imgui.ImVec2(p_min.x + imgui.get_window_size().x, p_min.y + imgui.get_window_size().y) - # Render soft shadow behind the modal - shaders.draw_soft_shadow(imgui.get_background_draw_list(), p_min, p_max, imgui.ImVec4(0, 0, 0, 0.6), 25.0, 6.0) - - if app._show_track_proposal_modal: - imgui.text_colored(C_IN, "Proposed Implementation Tracks") - imgui.separator() - if not app.proposed_tracks: - imgui.text("No tracks generated.") - else: - for idx, track in enumerate(app.proposed_tracks): - # Title Edit - changed_t, new_t = imgui.input_text(f"Title##{idx}", track.get('title', '')) - if changed_t: - track['title'] = new_t - # Goal Edit - changed_g, new_g = imgui.input_text_multiline(f"Goal##{idx}", track.get('goal', ''), imgui.ImVec2(-1, 60)) - if changed_g: - track['goal'] = new_g - # Buttons - if imgui.button(f"Remove##{idx}"): - app.proposed_tracks.pop(idx) - break - imgui.same_line() - if imgui.button(f"Start This Track##{idx}"): - app._cb_start_track(idx) - imgui.separator() - if imgui.button("Accept", imgui.ImVec2(120, 0)): - app._cb_accept_tracks() - app._show_track_proposal_modal = False - imgui.close_current_popup() - imgui.same_line() - if imgui.button("Cancel", imgui.ImVec2(120, 0)): - app._show_track_proposal_modal = False - imgui.close_current_popup() - else: - imgui.close_current_popup() - imgui.end_popup() def render_error_tint(app: App) -> None: """Renders a red tint overlay if hot reload failed."""