diff --git a/src/gui_2.py b/src/gui_2.py index f60974fd..ff1fc01a 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -838,13 +838,6 @@ class App: traceback.print_exc(file=sys.stderr) sys.stderr.flush() - -#region: Diangostics & Analytics - - - -#endregion: Diangostics & Analytics - #region: Logging def cb_load_prior_log(self, path: Optional[str] = None) -> None: if path is None: @@ -892,11 +885,8 @@ class App: self.ai_status = 'paths applied and session reset' #endregion: Project Management -#region: AI Settings - -#endregion: AI Settings - #region: Context Management + def _populate_auto_slices(self, f_item: models.FileItem) -> None: """ [C: tests/test_auto_slices.py:test_populate_auto_slices_basic] @@ -960,15 +950,11 @@ class App: threading.Thread(target=_stats_worker, daemon=True).start() return total_lines, total_ast + #endregion: Context Management -#region: Discussions -#endregion: Discussions - -#region: Operations Monitor -#endregion: Operations Monitor - #region: Misc Tools + def _close_vscode_diff(self) -> None: if hasattr(self, '_vscode_diff_process') and self._vscode_diff_process: try: @@ -1020,12 +1006,11 @@ class App: self._vscode_diff_process = result except Exception as e: self._patch_error_message = str(e) + #endregion: Misc Tools -#region: Sanity Tests -#endregion: Sanity Tests - #region: MMA + def _reorder_ticket(self, src_idx: int, dst_idx: int) -> None: """ [C: tests/test_ticket_queue.py:TestReorder.test_reorder_ticket_invalid, tests/test_ticket_queue.py:TestReorder.test_reorder_ticket_valid] @@ -1135,6 +1120,7 @@ class App: t['status'] = 'todo' changed = True self._push_mma_state_update() + #endregion: MMA def main() -> None: @@ -1144,7 +1130,223 @@ def main() -> None: if __name__ == "__main__": main() + +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") render_token_budget_panel(app) @@ -1274,9 +1476,177 @@ def render_diagnostics_panel(app: App) -> None: imgui.end_table() if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_diagnostics_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_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") + #endregion: Diagnostics & Analytics #region: Logging + def render_log_management(app: App) -> None: """ [C: tests/test_log_management_ui.py:test_render_log_management_logic] @@ -1350,18 +1720,46 @@ def render_log_management(app: App) -> None: imgui.end_table() 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, _): if exp: render_projects_panel(app) with imscope.tab_item('Paths') as (exp, _): if exp: render_paths_panel(app) + +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() + #endregion: Project Management #region: AI Settings + def render_ai_settings_hub(app: App) -> None: render_persona_selector_panel(app) if imgui.collapsing_header("Provider & Model"): render_provider_panel(app) @@ -1830,9 +2228,11 @@ def render_persona_editor_window(app: App, is_embedded: bool = False) -> None: if not is_embedded: imgui.end() + #endregion: AI Settings #region: Context Management + def render_files_and_media(app: App) -> None: """ [C: tests/test_gui_fast_render.py:test_render_files_and_media_fast] @@ -2131,9 +2531,11 @@ def render_add_context_files_modal(app: App) -> None: 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): @@ -2236,9 +2638,239 @@ def render_discussion_entry_read_mode(app: App, entry: dict, index: int) -> None 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_entries(app: App) -> None: + with imscope.child("disc_scroll"): + display_entries = app.disc_entries + if app.ui_focus_agent: + tier_usage = app.mma_tier_usage.get(app.ui_focus_agent) + if tier_usage: + persona_name = tier_usage.get("persona") + if persona_name: display_entries = [e for e in app.disc_entries if e.get("role") == persona_name or e.get("role") == "User"] + clipper = imgui.ListClipper(); clipper.begin(len(display_entries)) + while clipper.step(): + for i in range(clipper.display_start, clipper.display_end): + 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_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()}) + imgui.same_line() + if imgui.button("-All"): + for e in app.disc_entries: e["collapsed"] = True + imgui.same_line() + if imgui.button("+All"): + for e in app.disc_entries: e["collapsed"] = False + imgui.same_line() + if imgui.button("Clear All"): app.disc_entries.clear() + imgui.same_line() + if imgui.button("Save"): app._flush_to_project(); app._flush_to_config(); models.save_config(app.config); app.ai_status = "discussion saved" + _, app.ui_auto_add_history = imgui.checkbox("Auto-add message & response to history", app.ui_auto_add_history) + imgui.text("Keep Pairs:"); imgui.same_line(); imgui.set_next_item_width(80) + ch, app.ui_disc_truncate_pairs = imgui.input_int("##trunc_pairs", app.ui_disc_truncate_pairs, 1) + if app.ui_disc_truncate_pairs < 1: app.ui_disc_truncate_pairs = 1 + imgui.same_line() + if imgui.button("Truncate"): + 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_metadata(app: App) -> None: + disc_data = app.project.get("discussion", {}).get("discussions", {}).get(app.active_discussion, {}) + git_commit, last_updated = disc_data.get("git_commit", ""), disc_data.get("last_updated", "") + imgui.text_colored(C_LBL, "commit:"); imgui.same_line() + render_selectable_label(app, 'git_commit_val', git_commit[:12] if git_commit else '(none)', width=100, color=(C_IN if git_commit else C_LBL)) + imgui.same_line() + if imgui.button("Update Commit"): + if app.ui_project_git_dir: + cmt = project_manager.get_git_commit(app.ui_project_git_dir) + if cmt: disc_data["git_commit"], disc_data["last_updated"], app.ai_status = cmt, project_manager.now_ts(), f"commit: {cmt[:12]}" + imgui.text_colored(C_LBL, "updated:"); imgui.same_line(); imgui.text_colored(C_SUB, last_updated if last_updated else "(never)") + ch, app.ui_disc_new_name_input = imgui.input_text("##new_disc", app.ui_disc_new_name_input); imgui.same_line() + if imgui.button("Create"): + nm = app.ui_disc_new_name_input.strip() + if nm: app._create_discussion(nm); app.ui_disc_new_name_input = "" + imgui.same_line() + if imgui.button("Rename"): + nm = app.ui_disc_new_name_input.strip() + if nm: app._rename_discussion(app.active_discussion, nm); app.ui_disc_new_name_input = "" + 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] + """ + if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_discussion_panel") + render_thinking_indicator(app) + + if app.is_viewing_prior_session: + render_prior_session_view(app) + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_discussion_panel") + return + + render_discussion_selector(app) + + if not app.is_viewing_prior_session: + imgui.separator(); render_discussion_entry_controls(app) + imgui.separator(); render_discussion_roles(app) + imgui.separator(); render_discussion_entries(app) + + 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): + for i, r in enumerate(list(app.disc_roles)): + with imscope.id(f"role_{i}"): + if imgui.button("X"): app.disc_roles.pop(i); break + imgui.same_line(); imgui.text(r) + ch, app.ui_disc_new_role_input = imgui.input_text("##new_role", app.ui_disc_new_role_input); imgui.same_line() + if imgui.button("Add"): + r = app.ui_disc_new_role_input.strip() + 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 + names = app._get_discussion_names(); grouped = {} + for name in names: + base = name.split("_take_")[0]; grouped.setdefault(base, []).append(name) + active_base = app.active_discussion.split("_take_")[0] + if active_base not in grouped: active_base = names[0] if names else "" + base_names = sorted(grouped.keys()) + if imgui.begin_combo("##disc_sel", active_base): + for bname in base_names: + is_selected = (bname == active_base) + if imgui.selectable(bname, is_selected)[0]: + target = bname if bname in names else grouped[bname][0] + if target != app.active_discussion: app._switch_discussion(target) + if is_selected: imgui.set_item_default_focus() + imgui.end_combo() + active_base = app.active_discussion.split("_take_")[0]; current_takes = grouped.get(active_base, []) + if imgui.begin_tab_bar("discussion_takes_tabs"): + for take_name in current_takes: + label = "Original" if take_name == active_base else take_name.replace(f"{active_base}_", "").replace("_", " ").title() + flags = imgui.TabItemFlags_.set_selected if take_name == app.active_discussion else 0 + with imscope.tab_item(f"{label}###{take_name}", flags) as (exp, _): + if exp and take_name != app.active_discussion: app._switch_discussion(take_name) + with imscope.tab_item("Synthesis###Synthesis") as (exp, _): + if exp: render_synthesis_panel(app) + imgui.end_tab_bar() + if "_take_" in app.active_discussion: + if imgui.button("Promote Take"): + base_name = app.active_discussion.split("_take_")[0]; new_name = f"{base_name}_promoted"; counter = 1 + while new_name in names: new_name = f"{base_name}_promoted_{counter}"; counter += 1 + project_manager.promote_take(app.project, app.active_discussion, new_name); app._switch_discussion(new_name) + imgui.same_line() + if app.active_track: + imgui.same_line(); ch, app._track_discussion_active = imgui.checkbox("Track Discussion", app._track_discussion_active) + if ch: + if app._track_discussion_active: + app._flush_disc_entries_to_project() + history_strings = project_manager.load_track_history(app.active_track.id, app.active_project_root) + with app._disc_entries_lock: app.disc_entries = models.parse_history_entries(history_strings, app.disc_roles) + app.ai_status = f"track discussion: {app.active_track.id}" + else: app._flush_disc_entries_to_project(); app._switch_discussion(app.active_discussion); app.ai_status = "track discussion disabled" + 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") + render_discussion_panel(app) + if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_discussion_panel") + imgui.end_child() + imgui.button("###discussion_splitter", imgui.ImVec2(-1, 4)) + if imgui.is_item_active(): app.ui_discussion_split_h = max(150.0, min(imgui.get_window_height() - 150.0, app.ui_discussion_split_h - imgui.get_io().mouse_delta.y)) + imgui.push_style_var(imgui.StyleVar_.item_spacing, imgui.ImVec2(10, 4)) + ch1, app.ui_separate_message_panel = imgui.checkbox("Pop Out Message", app.ui_separate_message_panel); imgui.same_line() + ch2, app.ui_separate_response_panel = imgui.checkbox("Pop Out Response", app.ui_separate_response_panel) + if ch1: app.show_windows["Message"] = app.ui_separate_message_panel + if ch2: app.show_windows["Response"] = app.ui_separate_response_panel + imgui.pop_style_var() + show_message_tab = not app.ui_separate_message_panel + show_response_tab = not app.ui_separate_response_panel + if show_message_tab or show_response_tab: + if imgui.begin_tab_bar("discussion_tabs"): + tab_flags = imgui.TabItemFlags_.none + if app._autofocus_response_tab: + tab_flags = imgui.TabItemFlags_.set_selected + app._autofocus_response_tab = False + app.controller._autofocus_response_tab = False + if show_message_tab: + if imgui.begin_tab_item("Message", None)[0]: + render_message_panel(app) + imgui.end_tab_item() + if show_response_tab: + if imgui.begin_tab_item("Response", None, tab_flags)[0]: + render_response_panel(app) + imgui.end_tab_item() + imgui.end_tab_bar() + else: + imgui.text_disabled("Message & Response panels are detached.") + +def render_takes_panel(app: App) -> None: + imgui.text("Takes & Synthesis") + imgui.separator() + discussions = app.project.get('discussion', {}).get('discussions', {}) + if not hasattr(app, 'ui_synthesis_selected_takes'): + app.ui_synthesis_selected_takes = {name: False for name in discussions} + if not hasattr(app, 'ui_synthesis_prompt'): + app.ui_synthesis_prompt = "" + if imgui.begin_table("takes_table", 3, imgui.TableFlags_.resizable | imgui.TableFlags_.borders): + imgui.table_setup_column("Name", imgui.TableColumnFlags_.width_stretch) + imgui.table_setup_column("Entries", imgui.TableColumnFlags_.width_fixed, 80) + imgui.table_setup_column("Actions", imgui.TableColumnFlags_.width_fixed, 150) + imgui.table_headers_row() + for name, disc in list(discussions.items()): + imgui.table_next_row() + imgui.table_set_column_index(0) + is_active = name == app.active_discussion + if is_active: + imgui.text_colored(C_IN, name) + else: + imgui.text(name) + imgui.table_set_column_index(1) + history = disc.get('history', []) + imgui.text(f"{len(history)}") + imgui.table_set_column_index(2) + if imgui.button(f"Switch##{name}"): + app._switch_discussion(name) + imgui.same_line() + if name != "main" and imgui.button(f"Delete##{name}"): + del discussions[name] + imgui.end_table() + imgui.separator() + imgui.text("Synthesis") + imgui.text("Select takes to synthesize:") + for name in discussions: + _, app.ui_synthesis_selected_takes[name] = imgui.checkbox(name, app.ui_synthesis_selected_takes.get(name, False)) + imgui.spacing() + imgui.text("Synthesis Prompt:") + _, app.ui_synthesis_prompt = imgui.input_text_multiline("##synthesis_prompt", app.ui_synthesis_prompt, imgui.ImVec2(-1, 100)) + if imgui.button("Generate Synthesis"): + selected = [name for name, sel in app.ui_synthesis_selected_takes.items() if sel] + if len(selected) > 1: + from src import synthesis_formatter + takes_dict = {name: discussions.get(name, {}).get('history', []) for name in selected} + diff_text = synthesis_formatter.format_takes_diff(takes_dict) + prompt = f"{app.ui_synthesis_prompt}\n\nHere are the variations:\n{diff_text}" + new_name = "synthesis_take" + counter = 1 + while new_name in discussions: + new_name = f"synthesis_take_{counter}" + counter += 1 + app._create_discussion(new_name) + with app._disc_entries_lock: + app.disc_entries.append({"role": "user", "content": prompt, "collapsed": False, "ts": project_manager.now_ts()}) + app._handle_generate_send() + #endregion: Discussions #region: Operations Monitor + 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) @@ -2466,9 +3098,11 @@ def render_external_tools_panel(app: App) -> None: 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 @@ -2561,9 +3195,11 @@ def render_selectable_label(app: App, label: str, value: str, width: float = 0.0 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. @@ -2959,412 +3595,23 @@ def render_track_proposal_modal(app: App) -> None: else: imgui.close_current_popup() imgui.end_popup() + +def render_mma_focus_selector(app: App) -> None: + """ + [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] + """ + imgui.text("Focus Agent:"); imgui.same_line() + focus_label = app.ui_focus_agent or "All" + if imgui.begin_combo("##focus_agent", focus_label, imgui.ComboFlags_.width_fit_preview): + if imgui.selectable("All", app.ui_focus_agent is None)[0]: app.ui_focus_agent = None + for tier in ["Tier 2", "Tier 3", "Tier 4"]: + if imgui.selectable(tier, app.ui_focus_agent == tier)[0]: app.ui_focus_agent = tier + imgui.end_combo() + imgui.same_line() + if app.ui_focus_agent and imgui.button("x##clear_focus"): app.ui_focus_agent = None + #endregion: MMA -def render_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') @@ -4130,234 +4377,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_entries(app: App) -> None: - with imscope.child("disc_scroll"): - display_entries = app.disc_entries - if app.ui_focus_agent: - tier_usage = app.mma_tier_usage.get(app.ui_focus_agent) - if tier_usage: - persona_name = tier_usage.get("persona") - if persona_name: display_entries = [e for e in app.disc_entries if e.get("role") == persona_name or e.get("role") == "User"] - clipper = imgui.ListClipper(); clipper.begin(len(display_entries)) - while clipper.step(): - for i in range(clipper.display_start, clipper.display_end): - 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_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()}) - imgui.same_line() - if imgui.button("-All"): - for e in app.disc_entries: e["collapsed"] = True - imgui.same_line() - if imgui.button("+All"): - for e in app.disc_entries: e["collapsed"] = False - imgui.same_line() - if imgui.button("Clear All"): app.disc_entries.clear() - imgui.same_line() - if imgui.button("Save"): app._flush_to_project(); app._flush_to_config(); models.save_config(app.config); app.ai_status = "discussion saved" - _, app.ui_auto_add_history = imgui.checkbox("Auto-add message & response to history", app.ui_auto_add_history) - imgui.text("Keep Pairs:"); imgui.same_line(); imgui.set_next_item_width(80) - ch, app.ui_disc_truncate_pairs = imgui.input_int("##trunc_pairs", app.ui_disc_truncate_pairs, 1) - if app.ui_disc_truncate_pairs < 1: app.ui_disc_truncate_pairs = 1 - imgui.same_line() - if imgui.button("Truncate"): - 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_metadata(app: App) -> None: - disc_data = app.project.get("discussion", {}).get("discussions", {}).get(app.active_discussion, {}) - git_commit, last_updated = disc_data.get("git_commit", ""), disc_data.get("last_updated", "") - imgui.text_colored(C_LBL, "commit:"); imgui.same_line() - render_selectable_label(app, 'git_commit_val', git_commit[:12] if git_commit else '(none)', width=100, color=(C_IN if git_commit else C_LBL)) - imgui.same_line() - if imgui.button("Update Commit"): - if app.ui_project_git_dir: - cmt = project_manager.get_git_commit(app.ui_project_git_dir) - if cmt: disc_data["git_commit"], disc_data["last_updated"], app.ai_status = cmt, project_manager.now_ts(), f"commit: {cmt[:12]}" - imgui.text_colored(C_LBL, "updated:"); imgui.same_line(); imgui.text_colored(C_SUB, last_updated if last_updated else "(never)") - ch, app.ui_disc_new_name_input = imgui.input_text("##new_disc", app.ui_disc_new_name_input); imgui.same_line() - if imgui.button("Create"): - nm = app.ui_disc_new_name_input.strip() - if nm: app._create_discussion(nm); app.ui_disc_new_name_input = "" - imgui.same_line() - if imgui.button("Rename"): - nm = app.ui_disc_new_name_input.strip() - if nm: app._rename_discussion(app.active_discussion, nm); app.ui_disc_new_name_input = "" - 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] - """ - if app.perf_profiling_enabled: app.perf_monitor.start_component("_render_discussion_panel") - render_thinking_indicator(app) - - if app.is_viewing_prior_session: - render_prior_session_view(app) - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_discussion_panel") - return - - render_discussion_selector(app) - - if not app.is_viewing_prior_session: - imgui.separator(); render_discussion_entry_controls(app) - imgui.separator(); render_discussion_roles(app) - imgui.separator(); render_discussion_entries(app) - - 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): - for i, r in enumerate(list(app.disc_roles)): - with imscope.id(f"role_{i}"): - if imgui.button("X"): app.disc_roles.pop(i); break - imgui.same_line(); imgui.text(r) - ch, app.ui_disc_new_role_input = imgui.input_text("##new_role", app.ui_disc_new_role_input); imgui.same_line() - if imgui.button("Add"): - r = app.ui_disc_new_role_input.strip() - 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 - names = app._get_discussion_names(); grouped = {} - for name in names: - base = name.split("_take_")[0]; grouped.setdefault(base, []).append(name) - active_base = app.active_discussion.split("_take_")[0] - if active_base not in grouped: active_base = names[0] if names else "" - base_names = sorted(grouped.keys()) - if imgui.begin_combo("##disc_sel", active_base): - for bname in base_names: - is_selected = (bname == active_base) - if imgui.selectable(bname, is_selected)[0]: - target = bname if bname in names else grouped[bname][0] - if target != app.active_discussion: app._switch_discussion(target) - if is_selected: imgui.set_item_default_focus() - imgui.end_combo() - active_base = app.active_discussion.split("_take_")[0]; current_takes = grouped.get(active_base, []) - if imgui.begin_tab_bar("discussion_takes_tabs"): - for take_name in current_takes: - label = "Original" if take_name == active_base else take_name.replace(f"{active_base}_", "").replace("_", " ").title() - flags = imgui.TabItemFlags_.set_selected if take_name == app.active_discussion else 0 - with imscope.tab_item(f"{label}###{take_name}", flags) as (exp, _): - if exp and take_name != app.active_discussion: app._switch_discussion(take_name) - with imscope.tab_item("Synthesis###Synthesis") as (exp, _): - if exp: render_synthesis_panel(app) - imgui.end_tab_bar() - if "_take_" in app.active_discussion: - if imgui.button("Promote Take"): - base_name = app.active_discussion.split("_take_")[0]; new_name = f"{base_name}_promoted"; counter = 1 - while new_name in names: new_name = f"{base_name}_promoted_{counter}"; counter += 1 - project_manager.promote_take(app.project, app.active_discussion, new_name); app._switch_discussion(new_name) - imgui.same_line() - if app.active_track: - imgui.same_line(); ch, app._track_discussion_active = imgui.checkbox("Track Discussion", app._track_discussion_active) - if ch: - if app._track_discussion_active: - app._flush_disc_entries_to_project() - history_strings = project_manager.load_track_history(app.active_track.id, app.active_project_root) - with app._disc_entries_lock: app.disc_entries = models.parse_history_entries(history_strings, app.disc_roles) - app.ai_status = f"track discussion: {app.active_track.id}" - else: app._flush_disc_entries_to_project(); app._switch_discussion(app.active_discussion); app.ai_status = "track discussion disabled" - 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") - render_discussion_panel(app) - if app.perf_profiling_enabled: app.perf_monitor.end_component("_render_discussion_panel") - imgui.end_child() - imgui.button("###discussion_splitter", imgui.ImVec2(-1, 4)) - if imgui.is_item_active(): app.ui_discussion_split_h = max(150.0, min(imgui.get_window_height() - 150.0, app.ui_discussion_split_h - imgui.get_io().mouse_delta.y)) - imgui.push_style_var(imgui.StyleVar_.item_spacing, imgui.ImVec2(10, 4)) - ch1, app.ui_separate_message_panel = imgui.checkbox("Pop Out Message", app.ui_separate_message_panel); imgui.same_line() - ch2, app.ui_separate_response_panel = imgui.checkbox("Pop Out Response", app.ui_separate_response_panel) - if ch1: app.show_windows["Message"] = app.ui_separate_message_panel - if ch2: app.show_windows["Response"] = app.ui_separate_response_panel - imgui.pop_style_var() - show_message_tab = not app.ui_separate_message_panel - show_response_tab = not app.ui_separate_response_panel - if show_message_tab or show_response_tab: - if imgui.begin_tab_bar("discussion_tabs"): - tab_flags = imgui.TabItemFlags_.none - if app._autofocus_response_tab: - tab_flags = imgui.TabItemFlags_.set_selected - app._autofocus_response_tab = False - app.controller._autofocus_response_tab = False - if show_message_tab: - if imgui.begin_tab_item("Message", None)[0]: - render_message_panel(app) - imgui.end_tab_item() - if show_response_tab: - if imgui.begin_tab_item("Response", None, tab_flags)[0]: - render_response_panel(app) - imgui.end_tab_item() - imgui.end_tab_bar() - else: - imgui.text_disabled("Message & Response panels are detached.") - -def render_takes_panel(app: App) -> None: - imgui.text("Takes & Synthesis") - imgui.separator() - discussions = app.project.get('discussion', {}).get('discussions', {}) - if not hasattr(app, 'ui_synthesis_selected_takes'): - app.ui_synthesis_selected_takes = {name: False for name in discussions} - if not hasattr(app, 'ui_synthesis_prompt'): - app.ui_synthesis_prompt = "" - if imgui.begin_table("takes_table", 3, imgui.TableFlags_.resizable | imgui.TableFlags_.borders): - imgui.table_setup_column("Name", imgui.TableColumnFlags_.width_stretch) - imgui.table_setup_column("Entries", imgui.TableColumnFlags_.width_fixed, 80) - imgui.table_setup_column("Actions", imgui.TableColumnFlags_.width_fixed, 150) - imgui.table_headers_row() - for name, disc in list(discussions.items()): - imgui.table_next_row() - imgui.table_set_column_index(0) - is_active = name == app.active_discussion - if is_active: - imgui.text_colored(C_IN, name) - else: - imgui.text(name) - imgui.table_set_column_index(1) - history = disc.get('history', []) - imgui.text(f"{len(history)}") - imgui.table_set_column_index(2) - if imgui.button(f"Switch##{name}"): - app._switch_discussion(name) - imgui.same_line() - if name != "main" and imgui.button(f"Delete##{name}"): - del discussions[name] - imgui.end_table() - imgui.separator() - imgui.text("Synthesis") - imgui.text("Select takes to synthesize:") - for name in discussions: - _, app.ui_synthesis_selected_takes[name] = imgui.checkbox(name, app.ui_synthesis_selected_takes.get(name, False)) - imgui.spacing() - imgui.text("Synthesis Prompt:") - _, app.ui_synthesis_prompt = imgui.input_text_multiline("##synthesis_prompt", app.ui_synthesis_prompt, imgui.ImVec2(-1, 100)) - if imgui.button("Generate Synthesis"): - selected = [name for name, sel in app.ui_synthesis_selected_takes.items() if sel] - if len(selected) > 1: - from src import synthesis_formatter - takes_dict = {name: discussions.get(name, {}).get('history', []) for name in selected} - diff_text = synthesis_formatter.format_takes_diff(takes_dict) - prompt = f"{app.ui_synthesis_prompt}\n\nHere are the variations:\n{diff_text}" - new_name = "synthesis_take" - counter = 1 - while new_name in discussions: - new_name = f"synthesis_take_{counter}" - counter += 1 - app._create_discussion(new_name) - with app._disc_entries_lock: - app.disc_entries.append({"role": "user", "content": prompt, "collapsed": False, "ts": project_manager.now_ts()}) - app._handle_generate_send() - 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 @@ -5183,20 +5202,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_focus_selector(app: App) -> None: - """ - [C: tests/test_gui_progress.py:test_render_mma_dashboard_progress] - """ - imgui.text("Focus Agent:"); imgui.same_line() - focus_label = app.ui_focus_agent or "All" - if imgui.begin_combo("##focus_agent", focus_label, imgui.ComboFlags_.width_fit_preview): - if imgui.selectable("All", app.ui_focus_agent is None)[0]: app.ui_focus_agent = None - for tier in ["Tier 2", "Tier 3", "Tier 4"]: - if imgui.selectable(tier, app.ui_focus_agent == tier)[0]: app.ui_focus_agent = tier - imgui.end_combo() - imgui.same_line() - if app.ui_focus_agent and imgui.button("x##clear_focus"): app.ui_focus_agent = None - def render_error_tint(app: App) -> None: """Renders a red tint overlay if hot reload failed.""" if not HotReloader.is_error_state: return