From f52eff64997a926a24bb34af429cd27271bffc6b Mon Sep 17 00:00:00 2001 From: Ed_ Date: Wed, 13 May 2026 07:01:36 -0400 Subject: [PATCH] more organization of gui_2.py --- src/gui_2.py | 1753 +++++++++++++++++++++++++------------------------- 1 file changed, 888 insertions(+), 865 deletions(-) diff --git a/src/gui_2.py b/src/gui_2.py index 56ceb8d..2ea7203 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -520,37 +520,6 @@ class App: f.write(data) # ---------------------------------------------------------------- helpers - def _populate_auto_slices(self, f_item: models.FileItem) -> None: - from src import mcp_client - import re - mcp_client.configure([{"path": f_item.path}]) - outline = mcp_client.py_get_code_outline(f_item.path) - if outline.startswith("ERROR") or outline.startswith("ACCESS DENIED"): - return - pattern = re.compile(r'^\s*\[(.*?)\] (.*?) \(Lines (\d+)-(\d+)\)', re.MULTILINE) - try: - with open(f_item.path, "r", encoding="utf-8") as f: - text = f.read() - except Exception: - return - try: - from src.fuzzy_anchor import FuzzyAnchor - except ImportError: - FuzzyAnchor = None - for match in pattern.finditer(outline): - kind, name, s_str, e_str = match.groups() - s_line = int(s_str) - e_line = int(e_str) - if any(s.get('start_line') == s_line and s.get('end_line') == e_line for s in f_item.custom_slices): - continue - if FuzzyAnchor: - slice_data = FuzzyAnchor.create_slice(text, s_line, e_line) - else: - slice_data = {"start_line": s_line, "end_line": e_line} - slice_data['tag'] = 'auto-ast' - slice_data['comment'] = name - f_item.custom_slices.append(slice_data) - def _render_text_viewer(self, label: str, content: str, text_type: str = 'text', force_open: bool = False) -> None: if imgui.button("[+]##" + str(id(content))) or force_open: self.text_viewer_type = text_type @@ -922,6 +891,64 @@ class App: if not history: imgui.text("No history available.") else: iterate_history() + def _handle_history_logic(self) -> None: + """ + Logic for capturing UI state for undo/redo. + """ + if self._is_applying_snapshot: + return + + try: + # 2. Debounced snapshotting + current = self._take_snapshot() + if self._last_ui_snapshot is None: + self._last_ui_snapshot = current + return + + # Compare only core fields for performance + changed = ( + current.ai_input != self._last_ui_snapshot.ai_input or + current.project_system_prompt != self._last_ui_snapshot.project_system_prompt or + current.global_system_prompt != self._last_ui_snapshot.global_system_prompt or + current.base_system_prompt != self._last_ui_snapshot.base_system_prompt or + current.use_default_base_prompt != self._last_ui_snapshot.use_default_base_prompt or + abs(current.temperature - self._last_ui_snapshot.temperature) > 1e-5 or + abs(current.top_p - self._last_ui_snapshot.top_p) > 1e-5 or + current.max_tokens != self._last_ui_snapshot.max_tokens or + current.auto_add_history != self._last_ui_snapshot.auto_add_history or + len(current.disc_entries) != len(self._last_ui_snapshot.disc_entries) or + len(current.files) != len(self._last_ui_snapshot.files) or + len(current.context_files) != len(self._last_ui_snapshot.context_files) or + len(current.screenshots) != len(self._last_ui_snapshot.screenshots) + ) + + if not changed and len(current.disc_entries) > 0: + if current.disc_entries[-1].get('content') != self._last_ui_snapshot.disc_entries[-1].get('content'): + changed = True + + if changed: + if not self._pending_snapshot: + self._pending_snapshot = True + self._snapshot_timer = time.time() + # Capture state BEFORE current change + self._state_to_push = self._last_ui_snapshot + else: + # Reset timer for settle debounce + self._snapshot_timer = time.time() + + self._last_ui_snapshot = current + + if self._pending_snapshot and (time.time() - self._snapshot_timer > self._snapshot_debounce): + if self._state_to_push: + self.history.push(self._state_to_push, "UI Update") + self._state_to_push = None + self._pending_snapshot = False + except Exception as e: + import sys, traceback + sys.stderr.write(f"[DEBUG History] ERROR in _handle_history_logic: {e}\n") + traceback.print_exc(file=sys.stderr) + sys.stderr.flush() + def _render_shader_live_editor(self) -> None: """ [C: tests/test_shader_live_editor.py:test_shader_live_editor_renders] @@ -982,6 +1009,17 @@ class App: #region: Diangostics & Analytics + def _render_usage_analytics_panel(self) -> None: + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_usage_analytics_panel") + self._render_token_budget_panel() + imgui.separator() + self._render_cache_panel() + imgui.separator() + self._render_tool_analytics_panel() + imgui.separator() + self._render_session_insights_panel() + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_usage_analytics_panel") + def _render_cache_panel(self) -> None: if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_cache_panel") if self.current_provider != "gemini": @@ -1016,17 +1054,6 @@ class App: imgui.text_colored(imgui.ImVec4(0.2, 1.0, 0.2, 1.0), "Cache cleared - will rebuild on next request") if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_cache_panel") - def _render_usage_analytics_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_usage_analytics_panel") - self._render_token_budget_panel() - imgui.separator() - self._render_cache_panel() - imgui.separator() - self._render_tool_analytics_panel() - imgui.separator() - self._render_session_insights_panel() - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_usage_analytics_panel") - def _render_diagnostics_panel(self) -> None: if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_diagnostics_panel") with imscope.window("Diagnostics", self.show_windows.get("Diagnostics", False)) as (exp, opened): @@ -1131,6 +1158,166 @@ class App: imgui.end_table() if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_diagnostics_panel") + + def _render_tool_analytics_panel(self) -> None: + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_tool_analytics_panel") + imgui.text_colored(C_LBL, 'Tool Usage') + imgui.separator() + now = time.time() + if not hasattr(self, '_tool_stats_cache_time') or now - self._tool_stats_cache_time > 1.0: + self._cached_tool_stats = getattr(self.controller, '_tool_stats', {}) + tool_stats = getattr(self.controller, '_cached_tool_stats', {}) + if not tool_stats: + imgui.text_disabled("No tool usage data") + if self.perf_profiling_enabled: self.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 self.perf_profiling_enabled: self.perf_monitor.end_component("_render_tool_analytics_panel") + + def _render_token_budget_panel(self) -> None: + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_token_budget_panel") + imgui.text_colored(C_LBL, 'Prompt Utilization') + usage = self.session_usage + total = usage["input_tokens"] + usage["output_tokens"] + if total == 0 and usage.get("total_tokens", 0) > 0: + total = usage["total_tokens"] + self._render_selectable_label("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 self._gemini_cache_text: + imgui.text_colored(C_SUB, self._gemini_cache_text) + imgui.separator() + + if self._token_stats_dirty: + self._token_stats_dirty = False + # Offload to background thread via event queue + self.controller.event_queue.put("refresh_api_metrics", {"md_content": self._last_stable_md or ""}) + stats = self._token_stats + if not stats: + imgui.text_disabled("Token stats unavailable") + if self.perf_profiling_enabled: self.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(self, 'mma_tier_usage') and self.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 self.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); self._render_selectable_label(f"tier_{tier}", tier, width=-1) + imgui.table_set_column_index(1); self._render_selectable_label(f"model_{tier}", model.split("-")[0], width=-1) + imgui.table_set_column_index(2); self._render_selectable_label(f"tokens_{tier}", f"{tokens:,}", width=-1) + imgui.table_set_column_index(3); self._render_selectable_label(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 self.mma_tier_usage.values()) + self._render_selectable_label("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(self.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 self.perf_profiling_enabled: self.perf_monitor.end_component("_render_token_budget_panel") + + def _render_session_insights_panel(self) -> None: + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_session_insights_panel") + imgui.text_colored(C_LBL, 'Session Insights') + imgui.separator() + insights = self.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 self.perf_profiling_enabled: self.perf_monitor.end_component("_render_session_insights_panel") #endregion: Diangostics & Analytics @@ -1761,6 +1948,72 @@ class App: self.show_persona_editor_window = False imgui.end_table() + def _render_provider_panel(self) -> None: + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_provider_panel") + imgui.text("Provider") + if imgui.begin_combo("##prov", self.current_provider): + for p in models.PROVIDERS: + if imgui.selectable(p, p == self.current_provider)[0]: + self.current_provider = p + imgui.end_combo() + imgui.separator() + imgui.text("Model") + if imgui.begin_list_box("##models", imgui.ImVec2(-1, 120)): + for m in self.available_models: + if imgui.selectable(m, m == self.current_model)[0]: + self.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) + _, self.temperature = imgui.slider_float("##slider", self.temperature, 0.0, 2.0, "%.2f") + imgui.same_line() + imgui.set_next_item_width(-1) + _, self.temperature = imgui.input_float("Temp", self.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) + _, self.top_p = imgui.slider_float("##slider", self.top_p, 0.0, 1.0, "%.2f") + imgui.same_line() + imgui.set_next_item_width(-1) + _, self.top_p = imgui.input_float("Top-P", self.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) + _, self.max_tokens = imgui.slider_int("##slider", self.max_tokens, 1, 32768) + imgui.same_line() + imgui.set_next_item_width(-1) + _, self.max_tokens = imgui.input_int("MaxTok", self.max_tokens) + imgui.pop_id() + + ch, self.history_trunc_limit = imgui.input_int("History Truncation Limit", self.history_trunc_limit, 1024) + + if self.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(); self._render_selectable_label("gemini_cli_sid", sid, width=200) + if imgui.button("Reset CLI Session"): ai_client.reset_session() + imgui.text("Binary Path") + ch, self.ui_gemini_cli_path = imgui.input_text("##gcli_path", self.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: self.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 = self.ui_gemini_cli_path + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_provider_panel") + #endregion: AI Settings #region: Context Management @@ -1951,9 +2204,289 @@ class App: if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_files_panel") + def _render_context_composition_panel(self) -> None: + if imgui.collapsing_header("Context Composition##panel"): + total_lines, total_ast = self._update_context_file_stats() + self._render_context_batch_actions(total_lines, total_ast) + self._render_context_files_table() + + imgui.separator() + if imgui.collapsing_header("Screenshots"): + self._render_context_screenshots() + imgui.separator() + self._render_context_presets() + + def _render_ast_inspector_modal(self) -> None: + if self._show_ast_inspector: + imgui.open_popup('AST Inspector') + self._show_ast_inspector = False + + #region: AST Inspector + + expanded, opened = imgui.begin_popup_modal('AST Inspector', True, imgui.WindowFlags_.always_auto_resize) + if opened: + if expanded: + if self.ui_inspecting_ast_file is None: + imgui.close_current_popup() + else: + f_item = self.ui_inspecting_ast_file + f_path = f_item.path if hasattr(f_item, "path") else str(f_item) + + if f_path != self._cached_ast_file_path: + outline = "" + try: + if f_path.lower().endswith('.py'): + outline = mcp_client.py_get_code_outline(f_path) + elif f_path.lower().endswith(('.c', '.h')): + outline = mcp_client.ts_c_get_code_outline(f_path) + else: + outline = mcp_client.ts_cpp_get_code_outline(f_path) + except Exception as e: + outline = f"Error fetching outline: {e}" + + self._cached_ast_nodes = [] + import re + pattern = re.compile(r'^(\s*)\[(.*?)\] (.*?) \(Lines (\d+)-(\d+)\)') + stack = [] # (indent, name) + for line in outline.splitlines(): + m = pattern.match(line) + if m: + indent_str, kind, name, start_ln, end_ln = m.groups() + indent = len(indent_str) + while stack and stack[-1][0] >= indent: + stack.pop() + stack.append((indent, name)) + full_path = '::'.join([s[1] for s in stack]) + self._cached_ast_nodes.append({ + 'indent': indent, + 'kind': kind, + 'name': name, + 'full_path': full_path, + 'start_line': int(start_ln), + 'end_line': int(end_ln) + }) + try: + content = mcp_client.read_file(f_path) + self._cached_ast_file_lines = content.splitlines() + except Exception: + self._cached_ast_file_lines = ["Error loading file content."] + self._cached_ast_file_path = f_path + + imgui.text(f"Inspecting AST: {f_path}") + imgui.separator() + + #region: ast_dual_pane + if imgui.begin_table('ast_dual_pane', 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v): + imgui.table_next_column() + + #region: LEFT COLUMN (Tree) --- + if imgui.begin_child("ast_tree_scroll", imgui.ImVec2(0, 600), True): + if not self._cached_ast_nodes: + imgui.text("No AST nodes found or error fetching outline.") + else: + for node in self._cached_ast_nodes: + indent = node['indent'] + kind = node['kind'] + name = node['name'] + full_path = node['full_path'] + + imgui.dummy(imgui.ImVec2(indent * 10, 0)) + imgui.same_line() + imgui.text(f"[{kind}] {name}") + imgui.same_line(imgui.get_window_width() - 200) + + current_mode = f_item.ast_mask.get(full_path, 'hide') + + imgui.push_id(full_path) + if imgui.radio_button("Def", current_mode == 'def'): + f_item.ast_mask[full_path] = 'def' + imgui.same_line() + if imgui.radio_button("Sig", current_mode == 'sig'): + f_item.ast_mask[full_path] = 'sig' + imgui.same_line() + if imgui.radio_button("Hide", current_mode == 'hide'): + f_item.ast_mask[full_path] = 'hide' + imgui.pop_id() + imgui.end_child() + #endregion: LEFT COLUMN (Tree) + + imgui.table_next_column() + + #region: RIGHT COLUMN (Content) --- + if imgui.begin_child("ast_content_scroll", imgui.ImVec2(0, 600), True): + if not hasattr(self, '_cached_ast_file_lines') or not self._cached_ast_file_lines: + imgui.text("No file content loaded.") + else: + draw_list = imgui.get_window_draw_list() + for i, line_text in enumerate(self._cached_ast_file_lines): + line_num = i + 1 + + # Prioritize the most specific node (deepest indent) that covers the line + deepest_node = None + for node in self._cached_ast_nodes: + if node['start_line'] <= line_num <= node['end_line']: + if deepest_node is None or node['indent'] > deepest_node['indent']: + deepest_node = node + + mode = 'hide' + if deepest_node: + mode = f_item.ast_mask.get(deepest_node['full_path'], 'hide') + + pos = imgui.get_cursor_screen_pos() + line_height = imgui.get_text_line_height() + + if mode == 'def': + # Green, alpha 0.2 + draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(0, 255, 0, 0.2))) + elif mode == 'sig': + # Blue, alpha 0.2 + draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(0, 0, 255, 0.2))) + + imgui.text(f"{line_num:4} | {line_text}") + imgui.end_child() + #endregion: RIGHT COLUMN (Content) --- + imgui.end_table() + #endregion: ast_dual_pane + + imgui.separator() + + if imgui.button("Close", imgui.ImVec2(120, 0)): + self.ui_inspecting_ast_file = None + imgui.close_current_popup() + + imgui.end_popup() + + #endregion: AST Inspector + + if not opened: + self.ui_inspecting_ast_file = None + + def _render_add_context_files_modal(self) -> None: + 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:") + if imgui.begin_child("ctx_picker_list", imgui.ImVec2(600, 300), True): + # Create a temporary selection set if not initialized + if not hasattr(self, '_ui_picker_selected'): self._ui_picker_selected = set() + for f in self.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 self.context_files): + continue + is_sel = fpath in self._ui_picker_selected + clicked, new_sel = imgui.checkbox(f"{fpath}##picker_{fpath}", is_sel) + if clicked: + if new_sel: + self._ui_picker_selected.add(fpath) + else: + self._ui_picker_selected.discard(fpath) + imgui.end_child() + imgui.separator() + + if imgui.button("Add Selected", imgui.ImVec2(120, 0)): + for fpath in self._ui_picker_selected: + f_item = models.FileItem(path=fpath) + self.context_files.append(f_item) + self._populate_auto_slices(f_item) + self._ui_picker_selected.clear() + imgui.close_current_popup() + + imgui.same_line() + if imgui.button("Cancel", imgui.ImVec2(120, 0)): + if hasattr(self, '_ui_picker_selected'): + self._ui_picker_selected.clear() + imgui.close_current_popup() + imgui.end_popup() + + def _render_save_workspace_profile_modal(self) -> None: + if self._show_save_workspace_profile_modal: + imgui.open_popup("Save Workspace Profile") + + if imgui.begin_popup_modal("Save Workspace Profile", True, imgui.WindowFlags_.always_auto_resize)[0]: + imgui.text("Name:") + _, self._new_workspace_profile_name = imgui.input_text("##profile_name", self._new_workspace_profile_name) + + imgui.text("Scope:") + if imgui.radio_button("Project", self._new_workspace_profile_scope == "project"): + self._new_workspace_profile_scope = "project" + imgui.same_line() + if imgui.radio_button("Global", self._new_workspace_profile_scope == "global"): + self._new_workspace_profile_scope = "global" + + imgui.separator() + if imgui.button("Save", (120, 0)): + if self._new_workspace_profile_name.strip(): + self.controller._cb_save_workspace_profile(self._new_workspace_profile_name, self._new_workspace_profile_scope) + self._show_save_workspace_profile_modal = False + imgui.close_current_popup() + + imgui.same_line() + if imgui.button("Cancel", (120, 0)): + self._show_save_workspace_profile_modal = False + imgui.close_current_popup() + + imgui.end_popup() + + def _render_context_presets_panel(self) -> None: + imgui.text_colored(C_IN, "Context Presets") + imgui.separator() + changed, new_name = imgui.input_text("Preset Name##new_ctx", self.ui_new_context_preset_name) + if changed: self.ui_new_context_preset_name = new_name + imgui.same_line() + if imgui.button("Save Current"): + if self.ui_new_context_preset_name.strip(): + self.save_context_preset(self.ui_new_context_preset_name.strip()) + + imgui.separator() + presets = self.controller.project.get('context_presets', {}) + for name in sorted(presets.keys()): + preset = presets[name] + n_files = len(preset.get('files', [])) + n_shots = len(preset.get('screenshots', [])) + imgui.text(f"{name} ({n_files} files, {n_shots} shots)") + imgui.same_line() + if imgui.button(f"Load##{name}"): self.load_context_preset(name) + imgui.same_line() + if imgui.button(f"Delete##{name}"): self.delete_context_preset(name) + + def _populate_auto_slices(self, f_item: models.FileItem) -> None: + from src import mcp_client + import re + mcp_client.configure([{"path": f_item.path}]) + outline = mcp_client.py_get_code_outline(f_item.path) + if outline.startswith("ERROR") or outline.startswith("ACCESS DENIED"): + return + pattern = re.compile(r'^\s*\[(.*?)\] (.*?) \(Lines (\d+)-(\d+)\)', re.MULTILINE) + try: + with open(f_item.path, "r", encoding="utf-8") as f: + text = f.read() + except Exception: + return + try: + from src.fuzzy_anchor import FuzzyAnchor + except ImportError: + FuzzyAnchor = None + for match in pattern.finditer(outline): + kind, name, s_str, e_str = match.groups() + s_line = int(s_str) + e_line = int(e_str) + if any(s.get('start_line') == s_line and s.get('end_line') == e_line for s in f_item.custom_slices): + continue + if FuzzyAnchor: + slice_data = FuzzyAnchor.create_slice(text, s_line, e_line) + else: + slice_data = {"start_line": s_line, "end_line": e_line} + slice_data['tag'] = 'auto-ast' + slice_data['comment'] = name + f_item.custom_slices.append(slice_data) + + def _render_context_screenshots(self) -> None: + for i, s in enumerate(self.screenshots): + imgui.text(s) + #endregion: Context Management #region: Discussions + def _render_discussion_hub(self) -> None: with imscope.tab_bar("discussion_hub_tabs"): with imscope.tab_item("Discussion") as (exp, _): @@ -2223,6 +2756,63 @@ class App: imgui.end_tab_bar() else: imgui.text_disabled("Message & Response panels are detached.") + + def _render_takes_panel(self) -> None: + imgui.text("Takes & Synthesis") + imgui.separator() + discussions = self.project.get('discussion', {}).get('discussions', {}) + if not hasattr(self, 'ui_synthesis_selected_takes'): + self.ui_synthesis_selected_takes = {name: False for name in discussions} + if not hasattr(self, 'ui_synthesis_prompt'): + self.ui_synthesis_prompt = "" + 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 == self.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}"): + self._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: + _, self.ui_synthesis_selected_takes[name] = imgui.checkbox(name, self.ui_synthesis_selected_takes.get(name, False)) + imgui.spacing() + imgui.text("Synthesis Prompt:") + _, self.ui_synthesis_prompt = imgui.input_text_multiline("##synthesis_prompt", self.ui_synthesis_prompt, imgui.ImVec2(-1, 100)) + if imgui.button("Generate Synthesis"): + selected = [name for name, sel in self.ui_synthesis_selected_takes.items() if sel] + if len(selected) > 1: + from src import synthesis_formatter + takes_dict = {name: discussions.get(name, {}).get('history', []) for name in selected} + diff_text = synthesis_formatter.format_takes_diff(takes_dict) + prompt = f"{self.ui_synthesis_prompt}\n\nHere are the variations:\n{diff_text}" + new_name = "synthesis_take" + counter = 1 + while new_name in discussions: + new_name = f"synthesis_take_{counter}" + counter += 1 + self._create_discussion(new_name) + with self._disc_entries_lock: + self.disc_entries.append({"role": "user", "content": prompt, "collapsed": False, "ts": project_manager.now_ts()}) + self._handle_generate_send() + #endregion: Discussions def _render_operations_hub(self) -> None: @@ -2305,6 +2895,53 @@ class App: 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(self) -> None: + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_message_panel") + # LIVE indicator + is_live = self.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, self.ui_ai_input = imgui.input_text_multiline("##ai_in", self.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: + self.ui_ai_input = "" + imgui.separator() + is_busy = self.ai_status in ['sending...', 'streaming...'] + send_busy = False + with self._send_thread_lock: + if self.send_thread and self.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: + self._handle_generate_send() + imgui.end_disabled() + imgui.same_line() + if imgui.button("MD Only"): + self._handle_md_only() + imgui.same_line() + if imgui.button("Inject File"): + self.show_inject_modal = True + imgui.same_line() + if imgui.button("-> History"): + if self.ui_ai_input: + self.disc_entries.append({"role": "User", "content": self.ui_ai_input, "collapsed": False, "ts": project_manager.now_ts()}) + imgui.same_line() + if imgui.button("Reset"): + self._handle_reset_session() + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_message_panel") + #region: MMA def _reorder_ticket(self, src_idx: int, dst_idx: int) -> None: @@ -2956,8 +3593,61 @@ class App: imgui.end_child() if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_tier_stream_panel") + def _render_track_proposal_modal(self) -> None: + if self._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 self._show_track_proposal_modal: + imgui.text_colored(C_IN, "Proposed Implementation Tracks") + imgui.separator() + if not self.proposed_tracks: + imgui.text("No tracks generated.") + else: + for idx, track in enumerate(self.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}"): + self.proposed_tracks.pop(idx) + break + imgui.same_line() + if imgui.button(f"Start This Track##{idx}"): + self._cb_start_track(idx) + imgui.separator() + if imgui.button("Accept", imgui.ImVec2(120, 0)): + self._cb_accept_tracks() + self._show_track_proposal_modal = False + imgui.close_current_popup() + imgui.same_line() + if imgui.button("Cancel", imgui.ImVec2(120, 0)): + self._show_track_proposal_modal = False + imgui.close_current_popup() + else: + imgui.close_current_popup() + imgui.end_popup() + #enregion: MMA +#region: Operations Monitor + + + +#endregion: Operations Monitor + +#region: Project Management + def _render_projects_panel(self) -> None: if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_projects_panel") proj_name = self.project.get("project", {}).get("name", Path(self.active_project_path).stem) @@ -3047,212 +3737,34 @@ class App: ch, self.ui_auto_scroll_tool_calls = imgui.checkbox("Auto-scroll Tool History", self.ui_auto_scroll_tool_calls) if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_projects_panel") - def _render_base_prompt_diff_modal(self) -> None: - if not getattr(self.controller, "_show_base_prompt_diff_modal", False): - return - imgui.open_popup("Base Prompt Diff") - if imgui.begin_popup_modal("Base Prompt Diff", True, imgui.WindowFlags_.always_auto_resize)[0]: - imgui.text_colored(C_IN, "Difference between Default and Custom Base System Prompt") - imgui.separator() - - default_lines = ai_client._SYSTEM_PROMPT.splitlines(keepends=True) - custom_lines = self.ui_base_system_prompt.splitlines(keepends=True) - diff = list(difflib.unified_diff(default_lines, custom_lines, fromfile='Default', tofile='Custom')) - - if not diff: - imgui.text("No differences found.") - else: - imgui.begin_child("base_prompt_diff_scroll", imgui.ImVec2(800, 500), True) - for line in diff: - if line.startswith("+++") or line.startswith("---") or line.startswith("@@"): imgui.text_colored(vec4(77, 178, 255), line.rstrip()) - elif line.startswith("+"): imgui.text_colored(vec4(51, 230, 51), line.rstrip()) - elif line.startswith("-"): imgui.text_colored(vec4(230, 51, 51), line.rstrip()) - else: imgui.text(line.rstrip()) - imgui.end_child() - - imgui.separator() - if imgui.button("Close", imgui.ImVec2(120, 0)): - self.controller._show_base_prompt_diff_modal = False - imgui.close_current_popup() - imgui.end_popup() - - def _handle_history_logic(self) -> None: - """ - Logic for capturing UI state for undo/redo. - """ - if self._is_applying_snapshot: - return + def _render_paths_panel(self) -> None: + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_paths_panel") + path_info = paths.get_full_path_info() - try: - # 2. Debounced snapshotting - current = self._take_snapshot() - if self._last_ui_snapshot is None: - self._last_ui_snapshot = current - return + imgui.text_colored(C_IN, "System Path Configuration") + imgui.separator() - # Compare only core fields for performance - changed = ( - current.ai_input != self._last_ui_snapshot.ai_input or - current.project_system_prompt != self._last_ui_snapshot.project_system_prompt or - current.global_system_prompt != self._last_ui_snapshot.global_system_prompt or - current.base_system_prompt != self._last_ui_snapshot.base_system_prompt or - current.use_default_base_prompt != self._last_ui_snapshot.use_default_base_prompt or - abs(current.temperature - self._last_ui_snapshot.temperature) > 1e-5 or - abs(current.top_p - self._last_ui_snapshot.top_p) > 1e-5 or - current.max_tokens != self._last_ui_snapshot.max_tokens or - current.auto_add_history != self._last_ui_snapshot.auto_add_history or - len(current.disc_entries) != len(self._last_ui_snapshot.disc_entries) or - len(current.files) != len(self._last_ui_snapshot.files) or - len(current.context_files) != len(self._last_ui_snapshot.context_files) or - len(current.screenshots) != len(self._last_ui_snapshot.screenshots) - ) + 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']})") - if not changed and len(current.disc_entries) > 0: - if current.disc_entries[-1].get('content') != self._last_ui_snapshot.disc_entries[-1].get('content'): - changed = True + val = getattr(self, attr) + changed, new_val = imgui.input_text(f"##{key}", val) + if imgui.is_item_hovered(): imgui.set_tooltip(tooltip) + if changed: setattr(self, attr, new_val) + imgui.same_line() + if imgui.button(f"Browse##{key}"): + r = hide_tk_root() + d = filedialog.askdirectory(title=f"Select {label}") + r.destroy() + if d: setattr(self, attr, d) - if changed: - if not self._pending_snapshot: - self._pending_snapshot = True - self._snapshot_timer = time.time() - # Capture state BEFORE current change - self._state_to_push = self._last_ui_snapshot - else: - # Reset timer for settle debounce - self._snapshot_timer = time.time() - - self._last_ui_snapshot = current +#endregion: Project Management - if self._pending_snapshot and (time.time() - self._snapshot_timer > self._snapshot_debounce): - if self._state_to_push: - self.history.push(self._state_to_push, "UI Update") - self._state_to_push = None - self._pending_snapshot = False - except Exception as e: - import sys, traceback - sys.stderr.write(f"[DEBUG History] ERROR in _handle_history_logic: {e}\n") - traceback.print_exc(file=sys.stderr) - sys.stderr.flush() - - def _render_patch_modal(self) -> None: - if not self._show_patch_modal: - return - imgui.open_popup("Apply Patch?") - with imscope.popup_modal("Apply Patch?", True, imgui.WindowFlags_.always_auto_resize) as (opened, _): - if opened: - from src import shaders - p_min = imgui.get_window_pos() - p_max = imgui.ImVec2(p_min.x + imgui.get_window_size().x, p_min.y + imgui.get_window_size().y) - shaders.draw_soft_shadow(imgui.get_background_draw_list(), p_min, p_max, imgui.ImVec4(0, 0, 0, 0.6), 25.0, 6.0) - - imgui.text_colored(vec4(255, 230, 77), "Tier 4 QA Generated a Patch") - imgui.separator() - if self._pending_patch_files: - imgui.text("Files to modify:") - for f in self._pending_patch_files: - imgui.text(f" - {f}") - imgui.separator() - if self._patch_error_message: - imgui.text_colored(vec4(255, 77, 77), f"Error: {self._patch_error_message}") - imgui.separator() - imgui.text("Diff Preview:") - imgui.begin_child("patch_diff_scroll", imgui.ImVec2(-1, 280), True) - if self._pending_patch_text: - diff_lines = self._pending_patch_text.split("\n") - for line in diff_lines: - if line.startswith("+++") or line.startswith("---") or line.startswith("@@"): - imgui.text_colored(vec4(77, 178, 255), line) - elif line.startswith("+"): - imgui.text_colored(vec4(51, 230, 51), line) - elif line.startswith("-"): - imgui.text_colored(vec4(230, 51, 51), line) - else: - imgui.text(line) - imgui.end_child() - imgui.separator() - if imgui.button("Open in External Editor"): - self._open_patch_in_external_editor() - imgui.same_line() - if imgui.button("Apply Patch"): - self._apply_pending_patch() - self._close_vscode_diff() - imgui.same_line() - if imgui.button("Reject"): - self._close_vscode_diff() - self._show_patch_modal = False - self._pending_patch_text = None - self._pending_patch_files = [] - self._patch_error_message = None - imgui.close_current_popup() - - def _render_save_preset_modal(self) -> None: - if not self._show_save_preset_modal: return - imgui.open_popup("Save Layout Preset") - with imscope.popup_modal("Save Layout Preset", True, imgui.WindowFlags_.always_auto_resize) as (opened, _): - if opened: - imgui.text("Preset Name:") - _, self._new_preset_name = imgui.input_text("##preset_name", self._new_preset_name) - if imgui.button("Save", imgui.ImVec2(120, 0)): - if self._new_preset_name.strip(): - ini_data = imgui.save_ini_settings_to_memory() - self.layout_presets[self._new_preset_name.strip()] = { - "ini": ini_data, - "multi_viewport": self.ui_multi_viewport - } - self.config["layout_presets"] = self.layout_presets - models.save_config(self.config) - self._show_save_preset_modal = False - self._new_preset_name = "" - imgui.close_current_popup() - imgui.same_line() - if imgui.button("Cancel", imgui.ImVec2(120, 0)): - self._show_save_preset_modal = False - imgui.close_current_popup() - - def _render_track_proposal_modal(self) -> None: - if self._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 self._show_track_proposal_modal: - imgui.text_colored(C_IN, "Proposed Implementation Tracks") - imgui.separator() - if not self.proposed_tracks: - imgui.text("No tracks generated.") - else: - for idx, track in enumerate(self.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}"): - self.proposed_tracks.pop(idx) - break - imgui.same_line() - if imgui.button(f"Start This Track##{idx}"): - self._cb_start_track(idx) - imgui.separator() - if imgui.button("Accept", imgui.ImVec2(120, 0)): - self._cb_accept_tracks() - self._show_track_proposal_modal = False - imgui.close_current_popup() - imgui.same_line() - if imgui.button("Cancel", imgui.ImVec2(120, 0)): - self._show_track_proposal_modal = False - imgui.close_current_popup() - else: - imgui.close_current_popup() - imgui.end_popup() +#region: Misc Tools def _render_text_viewer_window(self) -> None: """Renders the standalone text/code/markdown viewer window.""" @@ -3375,214 +3887,147 @@ class App: self._render_ast_inspector_modal() return - def _render_ast_inspector_modal(self) -> None: - if self._show_ast_inspector: - imgui.open_popup('AST Inspector') - self._show_ast_inspector = False - - #region: AST Inspector - expanded, opened = imgui.begin_popup_modal('AST Inspector', True, imgui.WindowFlags_.always_auto_resize) - if opened: - if expanded: - if self.ui_inspecting_ast_file is None: - imgui.close_current_popup() - else: - f_item = self.ui_inspecting_ast_file - f_path = f_item.path if hasattr(f_item, "path") else str(f_item) - - if f_path != self._cached_ast_file_path: - outline = "" - try: - if f_path.lower().endswith('.py'): - outline = mcp_client.py_get_code_outline(f_path) - elif f_path.lower().endswith(('.c', '.h')): - outline = mcp_client.ts_c_get_code_outline(f_path) - else: - outline = mcp_client.ts_cpp_get_code_outline(f_path) - except Exception as e: - outline = f"Error fetching outline: {e}" - - self._cached_ast_nodes = [] - import re - pattern = re.compile(r'^(\s*)\[(.*?)\] (.*?) \(Lines (\d+)-(\d+)\)') - stack = [] # (indent, name) - for line in outline.splitlines(): - m = pattern.match(line) - if m: - indent_str, kind, name, start_ln, end_ln = m.groups() - indent = len(indent_str) - while stack and stack[-1][0] >= indent: - stack.pop() - stack.append((indent, name)) - full_path = '::'.join([s[1] for s in stack]) - self._cached_ast_nodes.append({ - 'indent': indent, - 'kind': kind, - 'name': name, - 'full_path': full_path, - 'start_line': int(start_ln), - 'end_line': int(end_ln) - }) - try: - content = mcp_client.read_file(f_path) - self._cached_ast_file_lines = content.splitlines() - except Exception: - self._cached_ast_file_lines = ["Error loading file content."] - self._cached_ast_file_path = f_path - - imgui.text(f"Inspecting AST: {f_path}") - imgui.separator() - - #region: ast_dual_pane - if imgui.begin_table('ast_dual_pane', 2, imgui.TableFlags_.resizable | imgui.TableFlags_.borders_inner_v): - imgui.table_next_column() - - #region: LEFT COLUMN (Tree) --- - if imgui.begin_child("ast_tree_scroll", imgui.ImVec2(0, 600), True): - if not self._cached_ast_nodes: - imgui.text("No AST nodes found or error fetching outline.") - else: - for node in self._cached_ast_nodes: - indent = node['indent'] - kind = node['kind'] - name = node['name'] - full_path = node['full_path'] - - imgui.dummy(imgui.ImVec2(indent * 10, 0)) - imgui.same_line() - imgui.text(f"[{kind}] {name}") - imgui.same_line(imgui.get_window_width() - 200) - - current_mode = f_item.ast_mask.get(full_path, 'hide') - - imgui.push_id(full_path) - if imgui.radio_button("Def", current_mode == 'def'): - f_item.ast_mask[full_path] = 'def' - imgui.same_line() - if imgui.radio_button("Sig", current_mode == 'sig'): - f_item.ast_mask[full_path] = 'sig' - imgui.same_line() - if imgui.radio_button("Hide", current_mode == 'hide'): - f_item.ast_mask[full_path] = 'hide' - imgui.pop_id() - imgui.end_child() - #endregion: LEFT COLUMN (Tree) - - imgui.table_next_column() - - #region: RIGHT COLUMN (Content) --- - if imgui.begin_child("ast_content_scroll", imgui.ImVec2(0, 600), True): - if not hasattr(self, '_cached_ast_file_lines') or not self._cached_ast_file_lines: - imgui.text("No file content loaded.") - else: - draw_list = imgui.get_window_draw_list() - for i, line_text in enumerate(self._cached_ast_file_lines): - line_num = i + 1 - - # Prioritize the most specific node (deepest indent) that covers the line - deepest_node = None - for node in self._cached_ast_nodes: - if node['start_line'] <= line_num <= node['end_line']: - if deepest_node is None or node['indent'] > deepest_node['indent']: - deepest_node = node - - mode = 'hide' - if deepest_node: - mode = f_item.ast_mask.get(deepest_node['full_path'], 'hide') - - pos = imgui.get_cursor_screen_pos() - line_height = imgui.get_text_line_height() - - if mode == 'def': - # Green, alpha 0.2 - draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(0, 255, 0, 0.2))) - elif mode == 'sig': - # Blue, alpha 0.2 - draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(0, 0, 255, 0.2))) - - imgui.text(f"{line_num:4} | {line_text}") - imgui.end_child() - #endregion: RIGHT COLUMN (Content) --- - imgui.end_table() - #endregion: ast_dual_pane - - imgui.separator() - - if imgui.button("Close", imgui.ImVec2(120, 0)): - self.ui_inspecting_ast_file = None - imgui.close_current_popup() - - imgui.end_popup() - #endregion: AST Inspector - - if not opened: - self.ui_inspecting_ast_file = None - - def _render_save_workspace_profile_modal(self) -> None: - if self._show_save_workspace_profile_modal: - imgui.open_popup("Save Workspace Profile") - - if imgui.begin_popup_modal("Save Workspace Profile", True, imgui.WindowFlags_.always_auto_resize)[0]: - imgui.text("Name:") - _, self._new_workspace_profile_name = imgui.input_text("##profile_name", self._new_workspace_profile_name) - - imgui.text("Scope:") - if imgui.radio_button("Project", self._new_workspace_profile_scope == "project"): - self._new_workspace_profile_scope = "project" - imgui.same_line() - if imgui.radio_button("Global", self._new_workspace_profile_scope == "global"): - self._new_workspace_profile_scope = "global" - + def _render_base_prompt_diff_modal(self) -> None: + if not getattr(self.controller, "_show_base_prompt_diff_modal", False): + return + imgui.open_popup("Base Prompt Diff") + if imgui.begin_popup_modal("Base Prompt Diff", True, imgui.WindowFlags_.always_auto_resize)[0]: + imgui.text_colored(C_IN, "Difference between Default and Custom Base System Prompt") imgui.separator() - if imgui.button("Save", (120, 0)): - if self._new_workspace_profile_name.strip(): - self.controller._cb_save_workspace_profile(self._new_workspace_profile_name, self._new_workspace_profile_scope) - self._show_save_workspace_profile_modal = False - imgui.close_current_popup() - imgui.same_line() - if imgui.button("Cancel", (120, 0)): - self._show_save_workspace_profile_modal = False - imgui.close_current_popup() + default_lines = ai_client._SYSTEM_PROMPT.splitlines(keepends=True) + custom_lines = self.ui_base_system_prompt.splitlines(keepends=True) + diff = list(difflib.unified_diff(default_lines, custom_lines, fromfile='Default', tofile='Custom')) - imgui.end_popup() - - def _render_add_context_files_modal(self) -> None: - 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:") - if imgui.begin_child("ctx_picker_list", imgui.ImVec2(600, 300), True): - # Create a temporary selection set if not initialized - if not hasattr(self, '_ui_picker_selected'): self._ui_picker_selected = set() - for f in self.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 self.context_files): - continue - is_sel = fpath in self._ui_picker_selected - clicked, new_sel = imgui.checkbox(f"{fpath}##picker_{fpath}", is_sel) - if clicked: - if new_sel: - self._ui_picker_selected.add(fpath) - else: - self._ui_picker_selected.discard(fpath) + if not diff: + imgui.text("No differences found.") + else: + imgui.begin_child("base_prompt_diff_scroll", imgui.ImVec2(800, 500), True) + for line in diff: + if line.startswith("+++") or line.startswith("---") or line.startswith("@@"): imgui.text_colored(vec4(77, 178, 255), line.rstrip()) + elif line.startswith("+"): imgui.text_colored(vec4(51, 230, 51), line.rstrip()) + elif line.startswith("-"): imgui.text_colored(vec4(230, 51, 51), line.rstrip()) + else: imgui.text(line.rstrip()) imgui.end_child() - imgui.separator() - if imgui.button("Add Selected", imgui.ImVec2(120, 0)): - for fpath in self._ui_picker_selected: - f_item = models.FileItem(path=fpath) - self.context_files.append(f_item) - self._populate_auto_slices(f_item) - self._ui_picker_selected.clear() - imgui.close_current_popup() - - imgui.same_line() - if imgui.button("Cancel", imgui.ImVec2(120, 0)): - if hasattr(self, '_ui_picker_selected'): - self._ui_picker_selected.clear() + imgui.separator() + if imgui.button("Close", imgui.ImVec2(120, 0)): + self.controller._show_base_prompt_diff_modal = False imgui.close_current_popup() imgui.end_popup() + def _close_vscode_diff(self) -> None: + if hasattr(self, '_vscode_diff_process') and self._vscode_diff_process: + try: + self._vscode_diff_process.terminate() + except Exception: + pass + self._vscode_diff_process = None + + def _render_patch_modal(self) -> None: + if not self._show_patch_modal: + return + imgui.open_popup("Apply Patch?") + with imscope.popup_modal("Apply Patch?", True, imgui.WindowFlags_.always_auto_resize) as (opened, _): + if opened: + from src import shaders + p_min = imgui.get_window_pos() + p_max = imgui.ImVec2(p_min.x + imgui.get_window_size().x, p_min.y + imgui.get_window_size().y) + shaders.draw_soft_shadow(imgui.get_background_draw_list(), p_min, p_max, imgui.ImVec4(0, 0, 0, 0.6), 25.0, 6.0) + + imgui.text_colored(vec4(255, 230, 77), "Tier 4 QA Generated a Patch") + imgui.separator() + if self._pending_patch_files: + imgui.text("Files to modify:") + for f in self._pending_patch_files: + imgui.text(f" - {f}") + imgui.separator() + if self._patch_error_message: + imgui.text_colored(vec4(255, 77, 77), f"Error: {self._patch_error_message}") + imgui.separator() + imgui.text("Diff Preview:") + imgui.begin_child("patch_diff_scroll", imgui.ImVec2(-1, 280), True) + if self._pending_patch_text: + diff_lines = self._pending_patch_text.split("\n") + for line in diff_lines: + if line.startswith("+++") or line.startswith("---") or line.startswith("@@"): + imgui.text_colored(vec4(77, 178, 255), line) + elif line.startswith("+"): + imgui.text_colored(vec4(51, 230, 51), line) + elif line.startswith("-"): + imgui.text_colored(vec4(230, 51, 51), line) + else: + imgui.text(line) + imgui.end_child() + imgui.separator() + if imgui.button("Open in External Editor"): + self._open_patch_in_external_editor() + imgui.same_line() + if imgui.button("Apply Patch"): + self._apply_pending_patch() + self._close_vscode_diff() + imgui.same_line() + if imgui.button("Reject"): + self._close_vscode_diff() + self._show_patch_modal = False + self._pending_patch_text = None + self._pending_patch_files = [] + self._patch_error_message = None + imgui.close_current_popup() + +#endregion: Misc Tools + +#region: Sanity Tests + + def _render_markdown_test(self) -> None: + imgui.text("Markdown Test Panel") + imgui.separator() + md = """ +# Header 1 +## Header 2 +### Header 3 +This is **bold** text and *italic* text. +And ***bold italic*** text. + +* List item 1 +* List item 2 + * Sub-item + +[Link to Google](https://google.com) + +```python +def hello(): + print("Markdown works!") +``` +""" + markdown_helper.render(md) + +#endregion: Sanity Tests + + def _render_save_preset_modal(self) -> None: + if not self._show_save_preset_modal: return + imgui.open_popup("Save Layout Preset") + with imscope.popup_modal("Save Layout Preset", True, imgui.WindowFlags_.always_auto_resize) as (opened, _): + if opened: + imgui.text("Preset Name:") + _, self._new_preset_name = imgui.input_text("##preset_name", self._new_preset_name) + if imgui.button("Save", imgui.ImVec2(120, 0)): + if self._new_preset_name.strip(): + ini_data = imgui.save_ini_settings_to_memory() + self.layout_presets[self._new_preset_name.strip()] = { + "ini": ini_data, + "multi_viewport": self.ui_multi_viewport + } + self.config["layout_presets"] = self.layout_presets + models.save_config(self.config) + self._show_save_preset_modal = False + self._new_preset_name = "" + imgui.close_current_popup() + imgui.same_line() + if imgui.button("Cancel", imgui.ImVec2(120, 0)): + self._show_save_preset_modal = False + imgui.close_current_popup() + def _set_external_editor_default(self, editor_name: str) -> None: from src import models if "tools" not in self.config: self.config["tools"] = {} @@ -3591,30 +4036,6 @@ class App: models.save_config(self.config) self.ai_status = f"Default editor set to: {editor_name}" - def _render_paths_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_paths_panel") - path_info = paths.get_full_path_info() - - imgui.text_colored(C_IN, "System Path Configuration") - imgui.separator() - - def render_path_field(label: str, attr: str, key: str, tooltip: str): - info = path_info.get(key, {'source': 'unknown'}) - imgui.text(label) - if imgui.is_item_hovered(): imgui.set_tooltip(tooltip) - imgui.same_line() - imgui.text_disabled(f"(Source: {info['source']})") - - val = getattr(self, attr) - changed, new_val = imgui.input_text(f"##{key}", val) - if imgui.is_item_hovered(): imgui.set_tooltip(tooltip) - if changed: setattr(self, attr, new_val) - imgui.same_line() - if imgui.button(f"Browse##{key}"): - r = hide_tk_root() - d = filedialog.askdirectory(title=f"Select {label}") - r.destroy() - if d: setattr(self, attr, d) render_path_field("Logs Directory", "ui_logs_dir", "logs_dir", "Directory where session JSON-L logs and artifacts are stored.") render_path_field("Scripts Directory", "ui_scripts_dir", "scripts_dir", "Directory for AI-generated PowerShell scripts.") @@ -3629,28 +4050,6 @@ class App: if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_paths_panel") - def _render_context_presets_panel(self) -> None: - imgui.text_colored(C_IN, "Context Presets") - imgui.separator() - changed, new_name = imgui.input_text("Preset Name##new_ctx", self.ui_new_context_preset_name) - if changed: self.ui_new_context_preset_name = new_name - imgui.same_line() - if imgui.button("Save Current"): - if self.ui_new_context_preset_name.strip(): - self.save_context_preset(self.ui_new_context_preset_name.strip()) - - imgui.separator() - presets = self.controller.project.get('context_presets', {}) - for name in sorted(presets.keys()): - preset = presets[name] - n_files = len(preset.get('files', [])) - n_shots = len(preset.get('screenshots', [])) - imgui.text(f"{name} ({n_files} files, {n_shots} shots)") - imgui.same_line() - if imgui.button(f"Load##{name}"): self.load_context_preset(name) - imgui.same_line() - if imgui.button(f"Delete##{name}"): self.delete_context_preset(name) - def _apply_pending_patch(self) -> None: if not self._pending_patch_text: self._patch_error_message = "No patch to apply" @@ -3697,14 +4096,6 @@ class App: except Exception as e: self._patch_error_message = str(e) - def _close_vscode_diff(self) -> None: - if hasattr(self, '_vscode_diff_process') and self._vscode_diff_process: - try: - self._vscode_diff_process.terminate() - except Exception: - pass - self._vscode_diff_process = None - def request_patch_from_tier4(self, error: str, file_context: str) -> None: try: from src import ai_client @@ -3986,10 +4377,6 @@ class App: imgui.same_line() imgui.text_colored(imgui.ImVec4(1.0, 0.5, 0.0, 1.0), "[Slices Active]") - def _render_context_screenshots(self) -> None: - for i, s in enumerate(self.screenshots): - imgui.text(s) - def _render_context_presets(self) -> None: imgui.text("Presets") presets = self.controller.project.get('context_presets', {}) @@ -4021,18 +4408,6 @@ class App: self.delete_context_preset(self.ui_active_context_preset) self.ui_active_context_preset = "" - def _render_context_composition_panel(self) -> None: - if imgui.collapsing_header("Context Composition##panel"): - total_lines, total_ast = self._update_context_file_stats() - self._render_context_batch_actions(total_lines, total_ast) - self._render_context_files_table() - - imgui.separator() - if imgui.collapsing_header("Screenshots"): - self._render_context_screenshots() - imgui.separator() - self._render_context_presets() - def _render_snapshot_tab(self) -> None: if imgui.begin_tab_bar("snapshot_tabs"): if imgui.begin_tab_item("Aggregate MD")[0]: @@ -4071,85 +4446,6 @@ class App: imgui.end_tab_item() imgui.end_tab_bar() - def _render_takes_panel(self) -> None: - imgui.text("Takes & Synthesis") - imgui.separator() - discussions = self.project.get('discussion', {}).get('discussions', {}) - if not hasattr(self, 'ui_synthesis_selected_takes'): - self.ui_synthesis_selected_takes = {name: False for name in discussions} - if not hasattr(self, 'ui_synthesis_prompt'): - self.ui_synthesis_prompt = "" - 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 == self.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}"): - self._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: - _, self.ui_synthesis_selected_takes[name] = imgui.checkbox(name, self.ui_synthesis_selected_takes.get(name, False)) - imgui.spacing() - imgui.text("Synthesis Prompt:") - _, self.ui_synthesis_prompt = imgui.input_text_multiline("##synthesis_prompt", self.ui_synthesis_prompt, imgui.ImVec2(-1, 100)) - if imgui.button("Generate Synthesis"): - selected = [name for name, sel in self.ui_synthesis_selected_takes.items() if sel] - if len(selected) > 1: - from src import synthesis_formatter - takes_dict = {name: discussions.get(name, {}).get('history', []) for name in selected} - diff_text = synthesis_formatter.format_takes_diff(takes_dict) - prompt = f"{self.ui_synthesis_prompt}\n\nHere are the variations:\n{diff_text}" - new_name = "synthesis_take" - counter = 1 - while new_name in discussions: - new_name = f"synthesis_take_{counter}" - counter += 1 - self._create_discussion(new_name) - with self._disc_entries_lock: - self.disc_entries.append({"role": "user", "content": prompt, "collapsed": False, "ts": project_manager.now_ts()}) - self._handle_generate_send() - - def _render_markdown_test(self) -> None: - imgui.text("Markdown Test Panel") - imgui.separator() - md = """ -# Header 1 -## Header 2 -### Header 3 -This is **bold** text and *italic* text. -And ***bold italic*** text. - -* List item 1 -* List item 2 - * Sub-item - -[Link to Google](https://google.com) - -```python -def hello(): - print("Markdown works!") -``` -""" - markdown_helper.render(md) - def _render_screenshots_panel(self, height_override: float = 0) -> None: if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_screenshots_panel") imgui.text("Paths"); imgui.same_line(); imgui.text("| Base Dir:"); imgui.same_line(); @@ -4307,279 +4603,6 @@ def hello(): imgui.separator() if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_persona_selector_panel") - def _render_provider_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_provider_panel") - imgui.text("Provider") - if imgui.begin_combo("##prov", self.current_provider): - for p in models.PROVIDERS: - if imgui.selectable(p, p == self.current_provider)[0]: - self.current_provider = p - imgui.end_combo() - imgui.separator() - imgui.text("Model") - if imgui.begin_list_box("##models", imgui.ImVec2(-1, 120)): - for m in self.available_models: - if imgui.selectable(m, m == self.current_model)[0]: - self.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) - _, self.temperature = imgui.slider_float("##slider", self.temperature, 0.0, 2.0, "%.2f") - imgui.same_line() - imgui.set_next_item_width(-1) - _, self.temperature = imgui.input_float("Temp", self.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) - _, self.top_p = imgui.slider_float("##slider", self.top_p, 0.0, 1.0, "%.2f") - imgui.same_line() - imgui.set_next_item_width(-1) - _, self.top_p = imgui.input_float("Top-P", self.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) - _, self.max_tokens = imgui.slider_int("##slider", self.max_tokens, 1, 32768) - imgui.same_line() - imgui.set_next_item_width(-1) - _, self.max_tokens = imgui.input_int("MaxTok", self.max_tokens) - imgui.pop_id() - - ch, self.history_trunc_limit = imgui.input_int("History Truncation Limit", self.history_trunc_limit, 1024) - - if self.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(); self._render_selectable_label("gemini_cli_sid", sid, width=200) - if imgui.button("Reset CLI Session"): ai_client.reset_session() - imgui.text("Binary Path") - ch, self.ui_gemini_cli_path = imgui.input_text("##gcli_path", self.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: self.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 = self.ui_gemini_cli_path - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_provider_panel") - - def _render_token_budget_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_token_budget_panel") - imgui.text_colored(C_LBL, 'Prompt Utilization') - usage = self.session_usage - total = usage["input_tokens"] + usage["output_tokens"] - if total == 0 and usage.get("total_tokens", 0) > 0: - total = usage["total_tokens"] - self._render_selectable_label("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 self._gemini_cache_text: - imgui.text_colored(C_SUB, self._gemini_cache_text) - imgui.separator() - - if self._token_stats_dirty: - self._token_stats_dirty = False - # Offload to background thread via event queue - self.controller.event_queue.put("refresh_api_metrics", {"md_content": self._last_stable_md or ""}) - stats = self._token_stats - if not stats: - imgui.text_disabled("Token stats unavailable") - if self.perf_profiling_enabled: self.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(self, 'mma_tier_usage') and self.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 self.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); self._render_selectable_label(f"tier_{tier}", tier, width=-1) - imgui.table_set_column_index(1); self._render_selectable_label(f"model_{tier}", model.split("-")[0], width=-1) - imgui.table_set_column_index(2); self._render_selectable_label(f"tokens_{tier}", f"{tokens:,}", width=-1) - imgui.table_set_column_index(3); self._render_selectable_label(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 self.mma_tier_usage.values()) - self._render_selectable_label("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(self.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 self.perf_profiling_enabled: self.perf_monitor.end_component("_render_token_budget_panel") - - def _render_tool_analytics_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_tool_analytics_panel") - imgui.text_colored(C_LBL, 'Tool Usage') - imgui.separator() - now = time.time() - if not hasattr(self, '_tool_stats_cache_time') or now - self._tool_stats_cache_time > 1.0: - self._cached_tool_stats = getattr(self.controller, '_tool_stats', {}) - tool_stats = getattr(self.controller, '_cached_tool_stats', {}) - if not tool_stats: - imgui.text_disabled("No tool usage data") - if self.perf_profiling_enabled: self.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 self.perf_profiling_enabled: self.perf_monitor.end_component("_render_tool_analytics_panel") - - def _render_session_insights_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_session_insights_panel") - imgui.text_colored(C_LBL, 'Session Insights') - imgui.separator() - insights = self.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 self.perf_profiling_enabled: self.perf_monitor.end_component("_render_session_insights_panel") - - def _render_message_panel(self) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_message_panel") - # LIVE indicator - is_live = self.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, self.ui_ai_input = imgui.input_text_multiline("##ai_in", self.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: - self.ui_ai_input = "" - imgui.separator() - is_busy = self.ai_status in ['sending...', 'streaming...'] - send_busy = False - with self._send_thread_lock: - if self.send_thread and self.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: - self._handle_generate_send() - imgui.end_disabled() - imgui.same_line() - if imgui.button("MD Only"): - self._handle_md_only() - imgui.same_line() - if imgui.button("Inject File"): - self.show_inject_modal = True - imgui.same_line() - if imgui.button("-> History"): - if self.ui_ai_input: - self.disc_entries.append({"role": "User", "content": self.ui_ai_input, "collapsed": False, "ts": project_manager.now_ts()}) - imgui.same_line() - if imgui.button("Reset"): - self._handle_reset_session() - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_message_panel") - def _render_response_panel(self) -> None: if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_response_panel") if self._trigger_blink: