diff --git a/src/gui_2.py b/src/gui_2.py index 912f52a..56ceb8d 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -11,35 +11,38 @@ import re import shutil import subprocess import sys +import traceback import threading import time -from defer import defer import tomli_w -# from contextlib import ExitStack, nullcontext -import traceback import typing +# from contextlib import ExitStack, nullcontext +from defer import defer +from imgui_bundle import imgui, hello_imgui, immapp, imgui_node_editor as ed, imgui_color_text_edit as ced from pathlib import Path +from pydantic import BaseModel from tkinter import filedialog, Tk from typing import Optional, Any from src import ai_client +from src import aggregate +from src import api_hooks +from src import app_controller +from src import bg_shader from src import cost_tracker -from src import session_logger -from src import project_manager +from src import history +from src import imgui_scopes as imscope from src import paths from src import presets -from src import api_hooks +from src import project_manager +from src import session_logger from src import log_registry from src import log_pruner from src import models -from src import app_controller -from src import history from src import mcp_client -from src import aggregate from src import markdown_helper -from src import bg_shader -from src import thinking_parser from src import theme_2 as theme from src import theme_nerv_fx as theme_fx +from src import thinking_parser from src import workspace_manager if sys.platform == "win32": import win32gui @@ -48,10 +51,6 @@ else: win32gui = None win32con = None -from pydantic import BaseModel -from imgui_bundle import imgui, hello_imgui, immapp, imgui_node_editor as ed, imgui_color_text_edit as ced -from src import imgui_scopes as imscope - COMMS_CLAMP_CHARS: int = 300 def hide_tk_root() -> Tk: @@ -64,11 +63,11 @@ def hide_tk_root() -> Tk: def vec4(r: float, g: float, b: float, a: float = 1.0) -> imgui.ImVec4: return imgui.ImVec4(r/255, g/255, b/255, a) C_OUT: imgui.ImVec4 = vec4(100, 200, 255) -C_IN: imgui.ImVec4 = vec4(140, 255, 160) +C_IN: imgui.ImVec4 = vec4(140, 255, 160) C_REQ: imgui.ImVec4 = vec4(255, 220, 100) C_RES: imgui.ImVec4 = vec4(180, 255, 180) -C_TC: imgui.ImVec4 = vec4(255, 180, 80) -C_TR: imgui.ImVec4 = vec4(180, 220, 255) +C_TC: imgui.ImVec4 = vec4(255, 180, 80) +C_TR: imgui.ImVec4 = vec4(180, 220, 255) C_TRS: imgui.ImVec4 = vec4(200, 180, 255) C_LBL: imgui.ImVec4 = vec4(180, 180, 180) C_VAL: imgui.ImVec4 = vec4(220, 220, 220) @@ -76,9 +75,9 @@ C_KEY: imgui.ImVec4 = vec4(140, 200, 255) C_NUM: imgui.ImVec4 = vec4(180, 255, 180) C_SUB: imgui.ImVec4 = vec4(220, 200, 120) -DIR_COLORS: dict[str, imgui.ImVec4] = {"OUT": C_OUT, "IN": C_IN} +DIR_COLORS: dict[str, imgui.ImVec4] = {"OUT": C_OUT, "IN": C_IN} KIND_COLORS: dict[str, imgui.ImVec4] = {"request": C_REQ, "response": C_RES, "tool_call": C_TC, "tool_result": C_TR, "tool_result_send": C_TRS} -HEAVY_KEYS: set[str] = {"message", "text", "script", "output", "content"} +HEAVY_KEYS: set[str] = {"message", "text", "script", "output", "content"} def truncate_entries(entries: list[dict[str, Any]], max_pairs: int) -> list[dict[str, Any]]: if max_pairs <= 0: @@ -646,868 +645,6 @@ class App: if width > 0: imgui.set_next_item_width(width) imgui.input_text("##" + label, value, imgui.InputTextFlags_.read_only) - def _render_window_if_open(self, name: str, render_func: Callable[[], None], flag_condition: bool = True) -> None: - """Helper to render a window only if its toggle is active.""" - if not flag_condition or not self.show_windows.get(name, False): return - with imscope.window(name, self.show_windows[name]) as (exp, opened): - self.show_windows[name] = bool(opened) - if exp: render_func() - - def _show_menus(self) -> None: - """ - [C: tests/test_gui_window_controls.py:test_gui_window_controls_minimize_maximize_close] - """ - with imscope.menu("manual slop") as (active): - if active and imgui.menu_item("Quit", "Ctrl+Q", False)[0]: - self.runner_params.app_shall_exit = True - with imscope.menu("Windows") as (active): - if (active): - for w in self.show_windows.keys(): - _, self.show_windows[w] = imgui.menu_item(w, "", self.show_windows[w]) - with imscope.menu("Project") as (active): - if active and imgui.menu_item("Save All", "", False)[0]: - self._flush_to_project() - self._flush_to_config() - models.save_config(self.config) - self.ai_status = "config saved" - if imgui.menu_item("Reset Session", "", False)[0]: - ai_client.reset_session() - ai_client.clear_comms_log() - self._tool_log.clear() - self._comms_log.clear() - self.ai_status = "session reset" - self.ai_response = "" - if imgui.menu_item("Generate MD Only", "", False)[0]: - try: - md, path, *_ = self._do_generate() - self.last_md = md - self.last_md_path = path - self.ai_status = f"md written: {path.name}" - except Exception as e: - self.ai_status = f"error: {e}" - with imscope.menu("Layout") as (active): - if active and imgui.menu_item("Save Current...", "", False)[0]: - self._show_save_workspace_profile_modal = True - self._new_workspace_profile_name = "" - imgui.separator() - for profile_id, profile in self.workspace_profiles.items(): - if imgui.menu_item(profile.name, "", False)[0]: - self.controller._cb_load_workspace_profile(profile_id) - imgui.separator() - with imscope.menu("Delete Profile") as (active): - if active: - for profile_id, profile in self.workspace_profiles.items(): - if imgui.menu_item(profile.name, "", False)[0]: - self.controller._cb_delete_workspace_profile(profile_id, self._new_workspace_profile_scope) - - # RAG status indicator - if self.controller.rag_config and self.controller.rag_config.enabled: - imgui.same_line() - status = self.controller.rag_status - if status == "indexing...": color = vec4(100, 255, 100) - elif status == "error": color = vec4(255, 100, 100) - else: color = vec4(180, 180, 180) - - imgui.text_colored(color, f"[RAG: {status}]") - if imgui.is_item_hovered(): imgui.set_tooltip(f"RAG is enabled. Status: {status}. Click to rebuild index.") - if imgui.is_item_clicked(): self.controller.event_queue.put('click', 'btn_rebuild_rag_index') - - # Draw right-aligned window controls directly in the menu bar (Win32 only) - if sys.platform == "win32": - try: - import ctypes - ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p - ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object, ctypes.c_char_p] - hwnd_capsule = imgui.get_main_viewport().platform_handle_raw - hwnd = ctypes.pythonapi.PyCapsule_GetPointer(hwnd_capsule, b"nb_handle") - except Exception: - hwnd = 0 - - if hwnd: - btn_w = 40 - # Use window width (points) instead of display_size (pixels) for correct scaling - window_w = imgui.get_window_width() - bar_h = imgui.get_window_height() - right_x = window_w - (btn_w * 3) - # Drag area check using an explicit invisible button spanning the empty space - curr_x = imgui.get_cursor_pos_x() - drag_w = right_x - curr_x - if drag_w > 0: - imgui.invisible_button("##drag_area", (drag_w, bar_h)) - if imgui.is_item_active() and imgui.is_mouse_dragging(0): - # CRITICAL: We must reset ImGui's mouse_down state BEFORE passing control to Windows. - # Otherwise, the Windows modal drag loop swallows the WM_LBUTTONUP event, - # and ImGui thinks the mouse is permanently held down, causing "sticky" dragging. - imgui.get_io().mouse_down[0] = False - win32gui.ReleaseCapture() - win32gui.SendMessage(hwnd, win32con.WM_NCLBUTTONDOWN, win32con.HTCAPTION, 0) - - imgui.push_style_color(imgui.Col_.button, vec4(0, 0, 0, 0)) - - try: - is_max = win32gui.GetWindowPlacement(hwnd)[1] == win32con.SW_SHOWMAXIMIZED - except Exception: - is_max = False - - # Explicitly set Y to 0 and match button height to bar height for perfect alignment - imgui.set_cursor_pos((right_x, 0)) - if imgui.button("_", (btn_w, bar_h)): - win32gui.ShowWindow(hwnd, win32con.SW_MINIMIZE) - - imgui.set_cursor_pos((right_x + btn_w, 0)) - if imgui.button("[=]" if is_max else "[]", (btn_w, bar_h)): - win32gui.ShowWindow(hwnd, win32con.SW_RESTORE if is_max else win32con.SW_MAXIMIZE) - - imgui.set_cursor_pos((right_x + btn_w * 2, 0)) - imgui.push_style_color(imgui.Col_.button_hovered, vec4(200, 50, 50, 255)) - if imgui.button("X", (btn_w, bar_h)): - win32gui.PostMessage(hwnd, win32con.WM_CLOSE, 0, 0) - imgui.pop_style_color() - - imgui.pop_style_color() - - def _render_custom_title_bar(self) -> None: - # Obsolete, removed since it renders behind the full screen dock space. - # Controls are now embedded in _show_menus. - pass - - def _render_shader_live_editor(self) -> None: - """ - [C: tests/test_shader_live_editor.py:test_shader_live_editor_renders] - """ - if self.show_windows.get('Shader Editor', False): - with imscope.window('Shader Editor', self.show_windows['Shader Editor']) as (exp, opened): - self.show_windows['Shader Editor'] = bool(opened) - if exp: - changed_crt, self.shader_uniforms['crt'] = imgui.slider_float('CRT Curvature', self.shader_uniforms['crt'], 0.0, 2.0) - changed_scan, self.shader_uniforms['scanline'] = imgui.slider_float('Scanline Intensity', self.shader_uniforms['scanline'], 0.0, 1.0) - changed_bloom, self.shader_uniforms['bloom'] = imgui.slider_float('Bloom Threshold', self.shader_uniforms['bloom'], 0.0, 1.0) - - def _render_history_window(self) -> None: - if not self.show_windows.get('Undo/Redo History', False): - return - def iterate_history(history: typing.List[typing.Dict[str, typing.Any]]): - 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: self._handle_jump_to_history(actual_idx) - with imscope.window("Undo/Redo History", self.show_windows['Undo/Redo History']) as (exp, opened): - self.show_windows['Undo/Redo History'] = bool(opened) - if exp: - if imgui.button("Undo") and self.history.can_undo: self._handle_undo(); imgui.same_line() - if imgui.button("Redo") and self.history.can_redo: self._handle_redo() - imgui.separator() - with imscope.child("history_list", imgui.ImVec2(0, 0), True): - history = self.history.get_history() - if not history: imgui.text("No history available.") - else: iterate_history() - - def _render_files_and_media(self) -> None: - avail = imgui.get_content_region_avail().y - if not hasattr(self, 'files_screenshots_split'): self.files_screenshots_split = 0.65 - split_y = int(avail * self.files_screenshots_split) - if imgui.collapsing_header("Files", imgui.TreeNodeFlags_.default_open): - with imscope.child("Files_child", imgui.ImVec2(-1, split_y), True): - if not hasattr(self, 'files_last_selected'): self.files_last_selected = -1 - - with imscope.table("files_table", 5, imgui.TableFlags_.resizable | imgui.TableFlags_.borders): - imgui.table_setup_column("", imgui.TableColumnFlags_.width_fixed, 20) - imgui.table_setup_column("Path", imgui.TableColumnFlags_.width_stretch) - imgui.table_setup_column("Agg", imgui.TableColumnFlags_.width_fixed, 0) - imgui.table_setup_column("Full", imgui.TableColumnFlags_.width_fixed, 0) - imgui.table_setup_column("Cache", imgui.TableColumnFlags_.width_fixed, 0) - imgui.table_headers_row() - - self.files.sort(key=lambda f: f.path.lower() if hasattr(f, 'path') else str(f).lower()) - for i, f_item in enumerate(self.files): - imgui.table_next_row(); imgui.table_set_column_index(0) - clicked, f_item.selected = imgui.checkbox(f"##{i}", f_item.selected) - if clicked: - if (imgui.is_key_down(imgui.Key.left_shift) or imgui.is_key_down(imgui.Key.right_shift)) and self.files_last_selected >= 0: - start_i = min(self.files_last_selected, i) - end_i = max(self.files_last_selected, i) - for j in range(start_i, end_i + 1): self.files[j].selected = True - self.files_last_selected = i - imgui.table_set_column_index(1); imgui.text(f_item.path if hasattr(f_item, 'path') else str(f_item)) - imgui.table_set_column_index(2) - if f_item.auto_aggregate: imgui.text_colored(imgui.ImVec4(0.3, 0.8, 1, 1), "A") - else: imgui.text_disabled(" ") - - imgui.same_line(spacing=1) - if imgui.invisible_button(f"agg{i}", imgui.ImVec2(15, 15)): - f_item.auto_aggregate = not f_item.auto_aggregate - if f_item.auto_aggregate: f_item.force_full = False - - imgui.table_set_column_index(3) - if f_item.force_full: imgui.text_colored(imgui.ImVec4(1, 0.6, 0.3, 1), "F") - else: imgui.text_disabled(" ") - - imgui.same_line(spacing=1) - if imgui.invisible_button(f"full{i}", imgui.ImVec2(15, 15)): - f_item.force_full = not f_item.force_full - if f_item.force_full: f_item.auto_aggregate = False - - imgui.table_set_column_index(4) - fpath = f_item.path if hasattr(f_item, 'path') else str(f_item) - is_cached = any(fpath in c for c in getattr(self, '_cached_files', [])) - if is_cached: imgui.text_colored(imgui.ImVec4(0, 1, 0, 1), "Y") - else: imgui.text_colored(imgui.ImVec4(0.5, 0.5, 0.5, 1), "-") - - if imgui.button("Add Files##addf"): - r = hide_tk_root(); paths = filedialog.askopenfilenames(); r.destroy() - for p in paths: - if p not in [f.path if hasattr(f, 'path') else f for f in self.files]: self.files.append(models.FileItem(path=p)) - - imgui.same_line() - if imgui.button("Sel All##selall"): - for f in self.files: - f.selected = True - - imgui.same_line() - if imgui.button("Unsel##unselall"): - for f in self.files: - f.selected = False - - imgui.same_line() - if imgui.button("None##nonesel"): - for f in self.files: - if f.selected: - f.auto_aggregate = False - f.force_full = False - - imgui.same_line() - if imgui.button("Agg##aggsel"): - for f in self.files: - if f.selected: - f.auto_aggregate = True - f.force_full = False - - imgui.same_line() - if imgui.button("Full##fullsel"): - for f in self.files: - if f.selected: - f.force_full = True - f.auto_aggregate = False - - imgui.same_line() - if imgui.button("Del##dels"): - self.files = [f for f in self.files if not f.selected] - - imgui.separator() - - if imgui.collapsing_header("Screenshots", imgui.TreeNodeFlags_.default_open): - with imscope.child("Shots_child", imgui.ImVec2(-1, -1), True): - for i, s in enumerate(self.screenshots): - if imgui.button(f"x##s{i}"): - self.screenshots.pop(i) - break - imgui.same_line(); imgui.text(s) - if imgui.button("Add Screenshots##adds"): - r = hide_tk_root(); paths = filedialog.askopenfilenames(filetypes=[("Images", "*.png *.jpg *.jpeg *.gif *.bmp *.webp"), ("All", "*.*")]); r.destroy() - for p in paths: - if p not in self.screenshots: self.screenshots.append(p) - return - - def _render_mma_modals(self) -> None: - """Renders all MMA-specific approval and info modals.""" - is_nerv = theme.is_nerv_active() - # Tool Execution Approval - if self._pending_ask_dialog: - if not self._ask_dialog_open: - imgui.open_popup("Approve Tool Execution") - self._ask_dialog_open = True - else: - self._ask_dialog_open = False - if imgui.begin_popup_modal("Approve Tool Execution", None, imgui.WindowFlags_.always_auto_resize)[0]: - if not self._pending_ask_dialog or self._ask_tool_data is None: imgui.close_current_popup() - else: - tool_name = self._ask_tool_data.get("tool", "unknown"); tool_args = self._ask_tool_data.get("args", {}) - imgui.text("The AI wants to execute a tool:"); imgui.text_colored(vec4(200, 200, 100), f"Tool: {tool_name}"); imgui.separator() - imgui.text("Arguments:"); imgui.begin_child("ask_args_child", imgui.ImVec2(400, 200), True); imgui.text_unformatted(json.dumps(tool_args, indent=2)); imgui.end_child() - imgui.separator() - if imgui.button("Approve", imgui.ImVec2(120, 0)): self._handle_approve_ask(); imgui.close_current_popup() - imgui.same_line() - if imgui.button("Deny", imgui.ImVec2(120, 0)): self._handle_reject_ask(); imgui.close_current_popup() - imgui.end_popup() - # MMA Step Approval - if self._pending_mma_approvals: - if not self._mma_approval_open: - imgui.open_popup("MMA Step Approval") - self._mma_approval_open, self._mma_approval_edit_mode = True, False - self._mma_approval_payload = self._pending_mma_approvals[0].get("payload", "") - else: self._mma_approval_open = False - if imgui.begin_popup_modal("MMA Step Approval", None, imgui.WindowFlags_.always_auto_resize)[0]: - if not self._pending_mma_approvals: imgui.close_current_popup() - else: - ticket_id = self._pending_mma_approvals[0].get("ticket_id", "??") - imgui.text(f"Ticket {ticket_id} is waiting for tool execution approval."); imgui.separator() - if self._mma_approval_edit_mode: - imgui.text("Edit Raw Payload (Manual Memory Mutation):"); _, self._mma_approval_payload = imgui.input_text_multiline("##mma_payload", self._mma_approval_payload, imgui.ImVec2(600, 400)) - else: - imgui.text("Proposed Tool Call:"); imgui.begin_child("mma_preview", imgui.ImVec2(600, 300), True); imgui.text_unformatted(str(self._pending_mma_approvals[0].get("payload", ""))); imgui.end_child() - imgui.separator() - if imgui.button("Approve", imgui.ImVec2(120, 0)): self._handle_mma_respond(approved=True, payload=self._mma_approval_payload); imgui.close_current_popup() - imgui.same_line() - if imgui.button("Edit Payload" if not self._mma_approval_edit_mode else "Show Original", imgui.ImVec2(120, 0)): self._mma_approval_edit_mode = not self._mma_approval_edit_mode - imgui.same_line() - if imgui.button("Abort Ticket", imgui.ImVec2(120, 0)): self._handle_mma_respond(approved=False); imgui.close_current_popup() - imgui.end_popup() - # MMA Spawn Approval - if self._pending_mma_spawns: - if not self._mma_spawn_open: - imgui.open_popup("MMA Spawn Approval") - self._mma_spawn_open, self._mma_spawn_edit_mode = True, False - self._mma_spawn_prompt, self._mma_spawn_context = self._pending_mma_spawns[0].get("prompt", ""), self._pending_mma_spawns[0].get("context_md", "") - else: self._mma_spawn_open = False - if imgui.begin_popup_modal("MMA Spawn Approval", None, imgui.WindowFlags_.always_auto_resize)[0]: - if not self._pending_mma_spawns: imgui.close_current_popup() - else: - role, ticket_id = self._pending_mma_spawns[0].get("role", "??"), self._pending_mma_spawns[0].get("ticket_id", "??") - imgui.text(f"Spawning {role} for Ticket {ticket_id}"); imgui.separator() - if self._mma_spawn_edit_mode: - imgui.text("Edit Prompt:"); _, self._mma_spawn_prompt = imgui.input_text_multiline("##spawn_prompt", self._mma_spawn_prompt, imgui.ImVec2(800, 200)) - imgui.text("Edit Context MD:"); _, self._mma_spawn_context = imgui.input_text_multiline("##spawn_context", self._mma_spawn_context, imgui.ImVec2(800, 300)) - else: - imgui.text("Proposed Prompt:"); imgui.begin_child("spawn_prompt_preview", imgui.ImVec2(800, 150), True); imgui.text_unformatted(self._mma_spawn_prompt); imgui.end_child() - imgui.text("Proposed Context MD:"); imgui.begin_child("spawn_context_preview", imgui.ImVec2(800, 250), True); imgui.text_unformatted(self._mma_spawn_context); imgui.end_child() - imgui.separator() - if imgui.button("Approve", imgui.ImVec2(120, 0)): self._handle_mma_respond(approved=True, prompt=self._mma_spawn_prompt, context_md=self._mma_spawn_context); imgui.close_current_popup() - imgui.same_line() - if imgui.button("Edit Mode" if not self._mma_spawn_edit_mode else "Preview Mode", imgui.ImVec2(120, 0)): self._mma_spawn_edit_mode = not self._mma_spawn_edit_mode - imgui.same_line() - if imgui.button("Abort", imgui.ImVec2(120, 0)): self._handle_mma_respond(approved=False, abort=True); imgui.close_current_popup() - imgui.end_popup() - # Cycle Detection - if imgui.begin_popup_modal("Cycle Detected!", None, imgui.WindowFlags_.always_auto_resize)[0]: - imgui.text_colored(imgui.ImVec4(1, 0.3, 0.3, 1), "The dependency graph contains a cycle!") - imgui.text("Please remove the circular dependency.") - if imgui.button("OK"): - imgui.close_current_popup() - imgui.end_popup() - - def _render_approve_script_modal(self) -> None: - """Renders the modal dialog for approving AI-generated PowerShell scripts.""" - with self._pending_dialog_lock: - dlg = self._pending_dialog - if dlg: - if not self._pending_dialog_open: - imgui.open_popup("Approve PowerShell Command") - self._pending_dialog_open = True - else: - self._pending_dialog_open = False - - if imgui.begin_popup_modal("Approve PowerShell Command", None, imgui.WindowFlags_.always_auto_resize)[0]: - if not dlg: - imgui.close_current_popup() - else: - imgui.text("The AI wants to run the following PowerShell script:") - imgui.text_colored(vec4(200, 200, 100), f"base_dir: {dlg._base_dir}") - imgui.separator() - # Checkbox to toggle full preview inside modal - _, self.show_text_viewer = imgui.checkbox("Show Full Preview", self.show_text_viewer) - if self.show_text_viewer: - imgui.begin_child("preview_child", imgui.ImVec2(600, 300), True) - imgui.text_unformatted(dlg._script) - imgui.end_child() - else: - ch, dlg._script = imgui.input_text_multiline("##confirm_script", dlg._script, imgui.ImVec2(-1, 200)) - imgui.separator() - if imgui.button("Approve & Run", imgui.ImVec2(120, 0)): - with dlg._condition: - dlg._approved = True - dlg._done = True - dlg._condition.notify_all() - with self._pending_dialog_lock: - self._pending_dialog = None - imgui.close_current_popup() - imgui.same_line() - if imgui.button("Reject", imgui.ImVec2(120, 0)): - with dlg._condition: - dlg._approved = False - dlg._done = True - dlg._condition.notify_all() - with self._pending_dialog_lock: - self._pending_dialog = None - imgui.close_current_popup() - imgui.end_popup() - - def _render_project_settings_hub(self) -> None: - with imscope.tab_bar('context_hub_tabs'): - with imscope.tab_item('Projects') as (exp, _): - if exp: self._render_projects_panel() - with imscope.tab_item('Paths') as (exp, _): - if exp: self._render_paths_panel() - - def _render_ai_settings_hub(self) -> None: - self._render_persona_selector_panel() - if imgui.collapsing_header("Provider & Model"): self._render_provider_panel() - if imgui.collapsing_header("System Prompts"): self._render_system_prompts_panel() - if imgui.collapsing_header("RAG Settings"): self._render_rag_panel() - self._render_agent_tools_panel() - - def _render_discussion_hub(self) -> None: - with imscope.tab_bar("discussion_hub_tabs"): - with imscope.tab_item("Discussion") as (exp, _): - if exp: self._render_discussion_tab() - with imscope.tab_item("Context Composition") as (exp, _): - if exp: self._render_context_composition_panel() - with imscope.tab_item("Snapshot") as (exp, _): - if exp: self._render_snapshot_tab() - with imscope.tab_item("Takes") as (exp, _): - if exp: self._render_takes_panel() - - def _render_operations_hub(self) -> None: - imgui.push_style_var(imgui.StyleVar_.item_spacing, imgui.ImVec2(10, 4)) - ch1, self.ui_separate_tool_calls_panel = imgui.checkbox("Pop Out Tool Calls", self.ui_separate_tool_calls_panel) - if ch1: self.show_windows["Tool Calls"] = self.ui_separate_tool_calls_panel - imgui.same_line() - ch2, self.ui_separate_usage_analytics = imgui.checkbox("Pop Out Usage Analytics", self.ui_separate_usage_analytics) - if ch2: self.show_windows["Usage Analytics"] = self.ui_separate_usage_analytics - imgui.same_line() - ch3, self.ui_separate_external_tools = imgui.checkbox('Pop Out External Tools', self.ui_separate_external_tools) - if ch3: self.show_windows['External Tools'] = self.ui_separate_external_tools - imgui.pop_style_var() - show_tc_tab, show_usage_tab = not self.ui_separate_tool_calls_panel, not self.ui_separate_usage_analytics - with imscope.tab_bar("ops_tabs"): - with imscope.tab_item("Comms History") as (exp, _): - if exp: self._render_comms_history_panel() - if show_tc_tab: - with imscope.tab_item("Tool Calls") as (exp, _): - if exp: self._render_tool_calls_panel() - if show_usage_tab: - with imscope.tab_item("Usage Analytics") as (exp, _): - if exp: self._render_usage_analytics_panel() - if not self.ui_separate_external_tools: - with imscope.tab_item("External Tools") as (exp, _): - if exp: - self._render_external_tools_panel() - imgui.separator(); imgui.text("") - try: self._render_external_editor_panel() - except Exception as e: imgui.text_colored(vec4(1, 0.3, 0.3, 1), f"Error: {str(e)}") - with imscope.tab_item("Workspace Layouts") as (exp, _): - if exp: - imgui.text("Experimental: Auto-switch layout by Tier") - ch, self.controller.ui_auto_switch_layout = imgui.checkbox("Enable Auto-Switch", self.controller.ui_auto_switch_layout) - if self.controller.ui_auto_switch_layout: - imgui.separator(); imgui.text("Tier Bindings (select profile for each tier)") - profiles = [""] + [p.name for p in self.controller.workspace_profiles.values()] - for t in ["Tier 1", "Tier 2", "Tier 3", "Tier 4"]: - curr = self.controller.ui_tier_layout_bindings.get(t, ""); idx = profiles.index(curr) if curr in profiles else 0 - ch_combo, new_idx = imgui.combo(t, idx, profiles) - if ch_combo: self.controller.ui_tier_layout_bindings[t] = profiles[new_idx] - - def _render_thinking_indicator(self) -> None: - is_thinking = self.ai_status in ['sending...', 'streaming...', 'running powershell...'] - if is_thinking: - val = math.sin(time.time() * 10 * math.pi) - alpha = 1.0 if val > 0 else 0.0 - c = vec4(255, 50, 50, alpha) if theme.is_nerv_active() else vec4(255, 100, 100, alpha) - imgui.text_colored(c, "THINKING..."); imgui.same_line() - - def _render_prior_session_view(self) -> None: - with imscope.style_color(imgui.Col_.child_bg, vec4(50, 40, 20)): - if imgui.button("Exit Prior Session"): self.controller.cb_exit_prior_session(); self._comms_log_dirty = True - imgui.separator() - with imscope.child("prior_scroll"): - clipper = imgui.ListClipper(); clipper.begin(len(self.prior_disc_entries)) - while clipper.step(): - for idx in range(clipper.display_start, clipper.display_end): - entry = self.prior_disc_entries[idx]; - with imscope.id(f"prior_disc_{idx}"): - collapsed = entry.get("collapsed", False) - if imgui.button("+" if collapsed else "-"): entry["collapsed"] = not collapsed - imgui.same_line(); role, ts = entry.get("role", "??"), entry.get("ts", "") - imgui.text_colored(C_LBL, f"[{role}]") - if ts: imgui.same_line(); imgui.text_colored(vec4(160, 160, 160), str(ts)) - content = entry.get("content", "") - if collapsed: - imgui.same_line(); preview = content.replace("\n", " ")[:80] - if len(content) > 80: preview += "..." - imgui.text_colored(vec4(180, 180, 180), preview) - else: - with theme.ai_text_style(): - markdown_helper.render(content, context_id=f'prior_disc_{idx}') - imgui.separator() - - def _render_discussion_selector(self) -> None: - if not imgui.collapsing_header("Discussions", imgui.TreeNodeFlags_.default_open): return - names = self._get_discussion_names(); grouped = {} - for name in names: - base = name.split("_take_")[0]; grouped.setdefault(base, []).append(name) - active_base = self.active_discussion.split("_take_")[0] - if active_base not in grouped: active_base = names[0] if names else "" - base_names = sorted(grouped.keys()) - if imgui.begin_combo("##disc_sel", active_base): - for bname in base_names: - is_selected = (bname == active_base) - if imgui.selectable(bname, is_selected)[0]: - target = bname if bname in names else grouped[bname][0] - if target != self.active_discussion: self._switch_discussion(target) - if is_selected: imgui.set_item_default_focus() - imgui.end_combo() - active_base = self.active_discussion.split("_take_")[0]; current_takes = grouped.get(active_base, []) - if imgui.begin_tab_bar("discussion_takes_tabs"): - for take_name in current_takes: - label = "Original" if take_name == active_base else take_name.replace(f"{active_base}_", "").replace("_", " ").title() - flags = imgui.TabItemFlags_.set_selected if take_name == self.active_discussion else 0 - with imscope.tab_item(f"{label}###{take_name}", flags) as (exp, _): - if exp and take_name != self.active_discussion: self._switch_discussion(take_name) - with imscope.tab_item("Synthesis###Synthesis") as (exp, _): - if exp: self._render_synthesis_panel() - imgui.end_tab_bar() - if "_take_" in self.active_discussion: - if imgui.button("Promote Take"): - base_name = self.active_discussion.split("_take_")[0]; new_name = f"{base_name}_promoted"; counter = 1 - while new_name in names: new_name = f"{base_name}_promoted_{counter}"; counter += 1 - project_manager.promote_take(self.project, self.active_discussion, new_name); self._switch_discussion(new_name) - imgui.same_line() - if self.active_track: - imgui.same_line(); ch, self._track_discussion_active = imgui.checkbox("Track Discussion", self._track_discussion_active) - if ch: - if self._track_discussion_active: - self._flush_disc_entries_to_project() - history_strings = project_manager.load_track_history(self.active_track.id, self.active_project_root) - with self._disc_entries_lock: self.disc_entries = models.parse_history_entries(history_strings, self.disc_roles) - self.ai_status = f"track discussion: {self.active_track.id}" - else: self._flush_disc_entries_to_project(); self._switch_discussion(self.active_discussion); self.ai_status = "track discussion disabled" - self._render_discussion_metadata() - - def _render_discussion_metadata(self) -> None: - disc_data = self.project.get("discussion", {}).get("discussions", {}).get(self.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() - self._render_selectable_label('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 self.ui_project_git_dir: - cmt = project_manager.get_git_commit(self.ui_project_git_dir) - if cmt: disc_data["git_commit"], disc_data["last_updated"], self.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, self.ui_disc_new_name_input = imgui.input_text("##new_disc", self.ui_disc_new_name_input); imgui.same_line() - if imgui.button("Create"): - nm = self.ui_disc_new_name_input.strip() - if nm: self._create_discussion(nm); self.ui_disc_new_name_input = "" - imgui.same_line() - if imgui.button("Rename"): - nm = self.ui_disc_new_name_input.strip() - if nm: self._rename_discussion(self.active_discussion, nm); self.ui_disc_new_name_input = "" - imgui.same_line() - if imgui.button("Delete"): self._delete_discussion(self.active_discussion) - - def _render_discussion_entry_controls(self) -> None: - if imgui.button("+ Entry"): self.disc_entries.append({"role": self.disc_roles[0] if self.disc_roles else "User", "content": "", "collapsed": True, "ts": project_manager.now_ts()}) - imgui.same_line() - if imgui.button("-All"): - for e in self.disc_entries: e["collapsed"] = True - imgui.same_line() - if imgui.button("+All"): - for e in self.disc_entries: e["collapsed"] = False - imgui.same_line() - if imgui.button("Clear All"): self.disc_entries.clear() - imgui.same_line() - if imgui.button("Save"): self._flush_to_project(); self._flush_to_config(); models.save_config(self.config); self.ai_status = "discussion saved" - _, self.ui_auto_add_history = imgui.checkbox("Auto-add message & response to history", self.ui_auto_add_history) - imgui.text("Keep Pairs:"); imgui.same_line(); imgui.set_next_item_width(80) - ch, self.ui_disc_truncate_pairs = imgui.input_int("##trunc_pairs", self.ui_disc_truncate_pairs, 1) - if self.ui_disc_truncate_pairs < 1: self.ui_disc_truncate_pairs = 1 - imgui.same_line() - if imgui.button("Truncate"): - with self._disc_entries_lock: self.disc_entries = truncate_entries(self.disc_entries, self.ui_disc_truncate_pairs) - self.ai_status = f"history truncated to {self.ui_disc_truncate_pairs} pairs" - - def _render_discussion_roles(self) -> None: - if imgui.collapsing_header("Roles"): - with imscope.child("roles_scroll", size_y=100, flags=True): - for i, r in enumerate(list(self.disc_roles)): - with imscope.id(f"role_{i}"): - if imgui.button("X"): self.disc_roles.pop(i); break - imgui.same_line(); imgui.text(r) - ch, self.ui_disc_new_role_input = imgui.input_text("##new_role", self.ui_disc_new_role_input); imgui.same_line() - if imgui.button("Add"): - r = self.ui_disc_new_role_input.strip() - if r and r not in self.disc_roles: self.disc_roles.append(r); self.ui_disc_new_role_input = "" - - def _render_discussion_entries(self) -> None: - with imscope.child("disc_scroll"): - display_entries = self.disc_entries - if self.ui_focus_agent: - tier_usage = self.mma_tier_usage.get(self.ui_focus_agent) - if tier_usage: - persona_name = tier_usage.get("persona") - if persona_name: display_entries = [e for e in self.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): - self._render_discussion_entry(display_entries[i], i) - if self._scroll_disc_to_bottom: imgui.set_scroll_here_y(1.0); self._scroll_disc_to_bottom = False - - def _render_discussion_entry(self, entry: dict, index: int) -> None: - with imscope.id(f"disc_{index}"): - collapsed, read_mode = entry.get("collapsed", False), entry.get("read_mode", False) - if imgui.button("+" if collapsed else "-"): entry["collapsed"] = not collapsed - imgui.same_line(); self._render_text_viewer(f"Entry #{index+1}", entry["content"]); imgui.same_line(); imgui.set_next_item_width(120) - if imgui.begin_combo("##role", entry["role"]): - for r in self.disc_roles: - if imgui.selectable(r, r == entry["role"])[0]: entry["role"] = r - imgui.end_combo() - if not collapsed: - imgui.same_line() - if imgui.button("[Edit]" if read_mode else "[Read]"): entry["read_mode"] = not read_mode - ts_str = entry.get("ts", "") - if ts_str: - imgui.same_line(); imgui.text_colored(vec4(120, 120, 100), str(ts_str)); e_dt = project_manager.parse_ts(ts_str) - if e_dt: - e_unix, next_unix = e_dt.timestamp(), float('inf') - if index + 1 < len(self.disc_entries): - n_ts = self.disc_entries[index+1].get("ts", ""); n_dt = project_manager.parse_ts(n_ts) - if n_dt: next_unix = n_dt.timestamp() - injected = [f for f in self.files if hasattr(f, 'injected_at') and f.injected_at and e_unix <= f.injected_at < next_unix] - if injected: - imgui.same_line(); imgui.text_colored(vec4(100, 255, 100), f"[{len(injected)}+]") - if imgui.is_item_hovered(): imgui.set_tooltip("Files injected at this point:\n" + "\n".join([f.path for f in injected])) - if collapsed: - imgui.same_line() - if imgui.button("Ins"): self.disc_entries.insert(index, {"role": "User", "content": "", "collapsed": True, "ts": project_manager.now_ts()}) - imgui.same_line() - if imgui.button("Del"): self.disc_entries.pop(index); return - imgui.same_line() - if imgui.button("Branch"): self._branch_discussion(index) - imgui.same_line(); preview = entry["content"].replace("\n", " ")[:60] - if len(entry["content"]) > 60: preview += "..." - if not preview.strip() and entry.get("thinking_segments"): - preview = entry["thinking_segments"][0]["content"].replace("\n", " ")[:60] - if len(entry["thinking_segments"][0]["content"]) > 60: preview += "..." - imgui.text_colored(vec4(160, 160, 150), preview) - if not collapsed: - thinking_segments, has_content = entry.get("thinking_segments", []), bool(entry.get("content", "").strip()) - if thinking_segments: self._render_thinking_trace(thinking_segments, index, is_standalone=not has_content) - if read_mode: self._render_discussion_entry_read_mode(entry, index) - else: - if not (bool(thinking_segments) and not has_content): ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150)) - imgui.separator() - - def _render_discussion_entry_read_mode(self, entry: dict, index: int) -> None: - content = entry["content"] - if not content.strip(): return - if '## Retrieved Context' in content: - rag_match = re.search(r'## Retrieved Context\n\n([\s\S]*?)(?=\n\n#|\Z)', content) - if rag_match: - rag_section = rag_match.group(1) - if imgui.collapsing_header('Retrieved Context'): - chunks = re.finditer(r'### Chunk (\d+) \(Source: (.*?)\)\n([\s\S]*?)(?=\n### Chunk|\Z)', rag_section) - for chunk_match in chunks: - idx, path, chunk_content = chunk_match.group(1), chunk_match.group(2), chunk_match.group(3) - if imgui.collapsing_header(f'Chunk {idx}: {path}'): - if imgui.button(f'[Source]##rag_{index}_{idx}'): - res = mcp_client.read_file(path) - if res: self.text_viewer_title, self.text_viewer_content, self.text_viewer_type, self.show_text_viewer = path, res, (Path(path).suffix.lstrip('.') if Path(path).suffix else 'text'), True - imgui.text_unformatted(chunk_content) - content = content[:rag_match.start()] + content[rag_match.end():] - pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?") - matches, is_nerv = list(pattern.finditer(content)), theme.is_nerv_active() - if not matches: - with theme.ai_text_style(): - markdown_helper.render(content, context_id=f'disc_{index}') - else: - with imscope.child(f"read_content_{index}", size_y=150, flags=True): - if self.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) - last_idx = 0 - for m_idx, match in enumerate(matches): - before = content[last_idx:match.start()] - if before: - with theme.ai_text_style(): - markdown_helper.render(before, context_id=f'disc_{index}_b_{m_idx}') - header_text, path, code_block = match.group(0).split("\n")[0].strip(), match.group(2), match.group(4) - if imgui.collapsing_header(header_text): - if imgui.button(f"[Source]##{index}_{match.start()}"): - res = mcp_client.read_file(path) - if res: self.text_viewer_title, self.text_viewer_content, self.text_viewer_type, self.show_text_viewer = path, res, (Path(path).suffix.lstrip('.') if Path(path).suffix else 'text'), True - if code_block: - with theme.ai_text_style(): - markdown_helper.render(code_block, context_id=f'disc_{index}_c_{m_idx}') - last_idx = match.end() - after = content[last_idx:] - if after: - with theme.ai_text_style(): - markdown_helper.render(after, context_id=f'disc_{index}_a') - if self.ui_word_wrap: imgui.pop_text_wrap_pos() - - def _render_mma_focus_selector(self) -> None: - imgui.text("Focus Agent:"); imgui.same_line() - focus_label = self.ui_focus_agent or "All" - if imgui.begin_combo("##focus_agent", focus_label, imgui.ComboFlags_.width_fit_preview): - if imgui.selectable("All", self.ui_focus_agent is None)[0]: self.ui_focus_agent = None - for tier in ["Tier 2", "Tier 3", "Tier 4"]: - if imgui.selectable(tier, self.ui_focus_agent == tier)[0]: self.ui_focus_agent = tier - imgui.end_combo() - imgui.same_line() - if self.ui_focus_agent and imgui.button("x##clear_focus"): self.ui_focus_agent = None - - def _render_mma_track_summary(self) -> None: - is_nerv = theme.is_nerv_active() - track_name = self.active_track.description if self.active_track else "None" - if getattr(self, "ui_project_execution_mode", "native") == "beads": track_name = "Beads Graph" - track_stats = project_manager.calculate_track_progress(self.active_track.tickets if self.active_track else self.active_tickets) - total_cost = sum(cost_tracker.estimate_cost(u.get('model','unknown'), u.get('input',0), u.get('output',0)) for u in self.mma_tier_usage.values()) - imgui.text("Track:"); imgui.same_line(); imgui.text_colored(C_VAL, track_name); imgui.same_line(); imgui.text(" | Status:"); imgui.same_line() - if self.mma_status == "paused": - imgui.text_colored(vec4(255, 152, 48) if is_nerv else imgui.ImVec4(1, 0.5, 0, 1), "PIPELINE PAUSED"); imgui.same_line() - status_col = imgui.ImVec4(1, 1, 1, 1) - if self.mma_status == "idle": status_col = imgui.ImVec4(0.7, 0.7, 0.7, 1) - elif self.mma_status == "running": status_col = vec4(80, 255, 80) if is_nerv else imgui.ImVec4(1, 1, 0, 1) - elif self.mma_status == "done": status_col = imgui.ImVec4(0, 1, 0, 1) - elif self.mma_status == "error": status_col = vec4(255, 72, 64) if is_nerv else imgui.ImVec4(1, 0, 0, 1) - elif self.mma_status == "paused": status_col = imgui.ImVec4(1, 0.5, 0, 1) - imgui.text_colored(status_col, self.mma_status.upper()); imgui.same_line(); imgui.text(" | Cost:"); imgui.same_line(); imgui.text_colored(imgui.ImVec4(0, 1, 0, 1), f"${total_cost:,.4f}") - perc = track_stats["percentage"] / 100.0 - p_color = imgui.ImVec4(1, 0, 0, 1) if track_stats["percentage"] < 33 else (imgui.ImVec4(1, 1, 0, 1) if track_stats["percentage"] < 66 else imgui.ImVec4(0, 1, 0, 1)) - imgui.push_style_color(imgui.Col_.plot_histogram, p_color); imgui.progress_bar(perc, imgui.ImVec2(-1, 0), f"{track_stats['percentage']:.1f}%"); imgui.pop_style_color() - if imgui.begin_table("ticket_stats_breakdown", 4): - for lbl, val in [("Completed:", track_stats["completed"]), ("In Progress:", track_stats["in_progress"]), ("Blocked:", track_stats["blocked"]), ("Todo:", track_stats["todo"])]: - imgui.table_next_column(); imgui.text_colored(C_LBL, lbl); imgui.same_line(); imgui.text_colored(C_VAL, str(val)) - imgui.end_table() - if self.active_track: - remaining = track_stats["total"] - track_stats["completed"] - eta_mins = (self._avg_ticket_time * remaining) / 60.0 - imgui.text_colored(C_LBL, "ETA:"); imgui.same_line(); imgui.text_colored(C_VAL, f"~{int(eta_mins)}m ({remaining} tickets remaining)") - - def _render_mma_epic_planner(self) -> None: - imgui.text_colored(C_LBL, 'Epic Planning (Tier 1)') - _, self.ui_epic_input = imgui.input_text_multiline('##epic_input', self.ui_epic_input, imgui.ImVec2(-1, 80)) - if imgui.button('Plan Epic (Tier 1)', imgui.ImVec2(-1, 0)): self._cb_plan_epic() - - def _render_mma_conductor_setup(self) -> None: - if imgui.button("Run Setup Scan"): self._cb_run_conductor_setup() - if self.ui_conductor_setup_summary: imgui.input_text_multiline("##setup_summary", self.ui_conductor_setup_summary, imgui.ImVec2(-1, 120), imgui.InputTextFlags_.read_only) - - def _render_mma_track_browser(self) -> None: - imgui.text("Track Browser") - if imgui.begin_table("mma_tracks_table", 4, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable): - imgui.table_setup_column("Title"); imgui.table_setup_column("Status"); imgui.table_setup_column("Progress"); imgui.table_setup_column("Actions"); imgui.table_headers_row() - for track in self.tracks: - imgui.table_next_row(); imgui.table_next_column(); imgui.text(track.get("title", "Untitled")); imgui.table_next_column() - status = track.get("status", "unknown").lower() - c = imgui.ImVec4(0.7, 0.7, 0.7, 1) if status == "new" else (vec4(80, 255, 80) if status == "active" and theme.is_nerv_active() else (imgui.ImVec4(1, 1, 0, 1) if status == "active" else (imgui.ImVec4(0, 1, 0, 1) if status == "done" else (imgui.ImVec4(1, 0, 0, 1) if status == "blocked" else imgui.ImVec4(1, 1, 1, 1))))) - imgui.text_colored(c, status.upper()); imgui.table_next_column() - prog = track.get("progress", 0.0) - p_c = imgui.ImVec4(1, 0, 0, 1) if prog < 0.33 else (imgui.ImVec4(1, 1, 0, 1) if prog < 0.66 else imgui.ImVec4(0, 1, 0, 1)) - imgui.push_style_color(imgui.Col_.plot_histogram, p_c); imgui.progress_bar(prog, imgui.ImVec2(-1, 0), f"{int(prog*100)}%"); imgui.pop_style_color(); imgui.table_next_column() - if imgui.button(f"Load##{track.get('id')}"): self._cb_load_track(str(track.get("id") or "")) - imgui.end_table() - imgui.text("Create New Track") - _, self.ui_new_track_name = imgui.input_text("Name##new_track", self.ui_new_track_name) - _, self.ui_new_track_desc = imgui.input_text_multiline("Description##new_track", self.ui_new_track_desc, imgui.ImVec2(-1, 60)) - imgui.text("Type:"); imgui.same_line() - if imgui.begin_combo("##track_type", self.ui_new_track_type): - for ttype in ["feature", "chore", "fix"]: - if imgui.selectable(ttype, self.ui_new_track_type == ttype)[0]: self.ui_new_track_type = ttype - imgui.end_combo() - if imgui.button("Create Track"): - self._cb_create_track(self.ui_new_track_name, self.ui_new_track_desc, self.ui_new_track_type) - self.ui_new_track_name = ""; self.ui_new_track_desc = "" - - def _render_mma_global_controls(self) -> None: - changed, self.mma_step_mode = imgui.checkbox("Step Mode (HITL)", self.mma_step_mode) - imgui.same_line(); imgui.text(f"Status: {self.mma_status.upper()}") - if self.controller and hasattr(self.controller, 'engine') and self.controller.engine and hasattr(self.controller.engine, '_pause_event'): - imgui.same_line() - is_paused = self.controller.engine._pause_event.is_set() - if imgui.button("Resume" if is_paused else "Pause"): - if is_paused: self.controller.engine.resume() - else: self.controller.engine.pause() - if self.active_tier: - imgui.same_line(); imgui.text_colored(C_VAL, f"| Active: {self.active_tier}") - any_pending = len(self._pending_mma_spawns) > 0 or len(self._pending_mma_approvals) > 0 or self._pending_ask_dialog - if any_pending: - alpha = abs(math.sin(time.time() * 5)) - c = vec4(255, 72, 64, alpha) if theme.is_nerv_active() else imgui.ImVec4(1, 0.3, 0.3, alpha) - imgui.same_line(); imgui.text_colored(c, " APPROVAL PENDING"); imgui.same_line() - if imgui.button("Go to Approval"): pass - - def _render_mma_usage_section(self) -> None: - imgui.text("Tier Usage (Tokens & Cost)") - if imgui.begin_table("mma_usage", 5, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg): - imgui.table_setup_column("Tier"); imgui.table_setup_column("Model"); imgui.table_setup_column("Input"); imgui.table_setup_column("Output"); imgui.table_setup_column("Est. Cost"); imgui.table_headers_row() - total_cost = 0.0 - for tier, stats in self.mma_tier_usage.items(): - imgui.table_next_row(); imgui.table_next_column(); imgui.text(tier); imgui.table_next_column(); model = stats.get('model', 'unknown'); imgui.text(model); imgui.table_next_column(); in_t = stats.get('input', 0); imgui.text(f"{in_t:,}"); imgui.table_next_column(); out_t = stats.get('output', 0); imgui.text(f"{out_t:,}"); imgui.table_next_column(); cost = cost_tracker.estimate_cost(model, in_t, out_t); total_cost += cost; imgui.text(f"${cost:,.4f}") - imgui.table_next_row(); imgui.table_set_bg_color(imgui.TableBgTarget_.row_bg0, imgui.get_color_u32(imgui.Col_.plot_lines_hovered)); imgui.table_next_column(); imgui.text("TOTAL"); imgui.table_next_column(); imgui.text(""); imgui.table_next_column(); imgui.text(""); imgui.table_next_column(); imgui.text(""); imgui.table_next_column(); imgui.text(f"${total_cost:,.4f}"); imgui.end_table() - if imgui.collapsing_header("Tier Model Config"): - for tier in self.mma_tier_usage.keys(): - imgui.text(f"{tier}:"); imgui.same_line(); curr_model, curr_prov = self.mma_tier_usage[tier].get("model", "unknown"), self.mma_tier_usage[tier].get("provider", "gemini") - with imscope.id(f"tier_cfg_{tier}"): - imgui.push_item_width(80) - if imgui.begin_combo("##prov", curr_prov): - for p in models.PROVIDERS: - if imgui.selectable(p, p == curr_prov)[0]: - self.mma_tier_usage[tier]["provider"] = p - models_list = self.controller.all_available_models.get(p, []) - if models_list: self.mma_tier_usage[tier]["model"] = models_list[0] - imgui.end_combo() - imgui.pop_item_width(); imgui.same_line(); imgui.push_item_width(150) - models_list = self.controller.all_available_models.get(curr_prov, []) - if imgui.begin_combo("##model", curr_model): - for m in models_list: - if imgui.selectable(m, curr_model == m)[0]: self.mma_tier_usage[tier]["model"] = m - imgui.end_combo() - imgui.pop_item_width(); imgui.same_line(); imgui.push_item_width(-1) - curr_preset = self.mma_tier_usage[tier].get("tool_preset") or "None" - p_names = ["None"] + sorted(self.controller.tool_presets.keys()) - if imgui.begin_combo("##preset", curr_preset): - for pn in p_names: - if imgui.selectable(pn, curr_preset == pn)[0]: self.mma_tier_usage[tier]["tool_preset"] = None if pn == "None" else pn - imgui.end_combo() - imgui.pop_item_width(); imgui.same_line(); imgui.push_item_width(150) - curr_pers = self.mma_tier_usage[tier].get("persona") or "None" - personas = getattr(self.controller, 'personas', {}) - pers_opts = ["None"] + sorted(personas.keys()) - if imgui.begin_combo("##persona", curr_pers): - for pern in pers_opts: - if imgui.selectable(pern, curr_pers == pern)[0]: self.mma_tier_usage[tier]["persona"] = None if pern == "None" else pern - imgui.end_combo() - imgui.pop_item_width() - - def _render_mma_ticket_editor(self) -> None: - imgui.separator(); imgui.text_colored(C_VAL, f"Editing: {self.ui_selected_ticket_id}") - ticket = next((t for t in self.active_tickets if str(t.get('id', '')) == self.ui_selected_ticket_id), None) - if ticket: - imgui.text(f"Status: {ticket.get('status', 'todo')}"); prio = ticket.get('priority', 'medium') - imgui.text("Priority:"); imgui.same_line() - if imgui.begin_combo(f"##edit_prio_{ticket.get('id')}", prio): - for p_opt in ['high', 'medium', 'low']: - if imgui.selectable(p_opt, p_opt == prio)[0]: ticket['priority'] = p_opt; self._push_mma_state_update() - imgui.end_combo() - imgui.text(f"Target: {ticket.get('target_file', '')}"); imgui.text(f"Depends on: {', '.join(ticket.get('depends_on', []))}") - personas = getattr(self.controller, 'personas', {}); curr_pers = ticket.get('persona_id', '') - imgui.text("Persona Override:"); imgui.same_line() - pers_opts = ["None"] + sorted(personas.keys()); curr_idx = pers_opts.index(curr_pers) + 1 if curr_pers in pers_opts else 0 - _, curr_idx = imgui.combo(f"##ticket_persona_{ticket.get('id')}", curr_idx, pers_opts) - ticket['persona_id'] = None if curr_idx == 0 or pers_opts[curr_idx] == "None" else pers_opts[curr_idx] - if imgui.button(f"Mark Complete##{self.ui_selected_ticket_id}"): ticket['status'] = 'done'; self._push_mma_state_update() - imgui.same_line() - if imgui.button(f"Delete##{self.ui_selected_ticket_id}"): self.active_tickets = [t for t in self.active_tickets if str(t.get('id', '')) != self.ui_selected_ticket_id]; self.ui_selected_ticket_id = None; self._push_mma_state_update() - - def _render_mma_agent_streams(self) -> None: - imgui.text("Agent Streams") - if imgui.begin_tab_bar("mma_streams_tabs"): - for tier, label, sep_flag_attr in [("Tier 1", "Tier 1", "ui_separate_tier1"), ("Tier 2", "Tier 2 (Tech Lead)", "ui_separate_tier2"), ("Tier 3", None, "ui_separate_tier3"), ("Tier 4", "Tier 4 (QA)", "ui_separate_tier4")]: - with imscope.tab_item(tier) as (exp, _): - if exp: - sep_val = getattr(self, sep_flag_attr); ch, new_val = imgui.checkbox(f"Pop Out {tier}", sep_val) - if ch: - setattr(self, sep_flag_attr, new_val) - self.show_windows[f"{tier}: Strategy" if tier == "Tier 1" else (f"{tier}: Tech Lead" if tier == "Tier 2" else (f"{tier}: Workers" if tier == "Tier 3" else f"{tier}: QA"))] = new_val - if not new_val: self._render_tier_stream_panel(tier, label) - else: imgui.text_disabled(f"{tier} stream is detached.") - if getattr(self, "ui_project_execution_mode", "native") == "beads": - with imscope.tab_item("Beads") as (exp, _): - if exp: self._render_beads_tab() - imgui.end_tab_bar() - def _gui_func(self) -> None: self._render_custom_title_bar() self._render_shader_live_editor() @@ -1637,550 +774,545 @@ class App: self._render_approve_script_modal() self._render_mma_modals() - 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 _render_window_if_open(self, name: str, render_func: Callable[[], None], flag_condition: bool = True) -> None: + """Helper to render a window only if its toggle is active.""" + if not flag_condition or not self.show_windows.get(name, False): return + with imscope.window(name, self.show_windows[name]) as (exp, opened): + self.show_windows[name] = bool(opened) + if exp: render_func() - def _handle_history_logic(self) -> None: + def _render_custom_title_bar(self) -> None: + # Obsolete, removed since it renders behind the full screen dock space. + # Controls are now embedded in _show_menus. + pass + + def _show_menus(self) -> None: """ - Logic for capturing UI state for undo/redo. + [C: tests/test_gui_window_controls.py:test_gui_window_controls_minimize_maximize_close] """ - 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_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() - - def _render_text_viewer_window(self) -> None: - """Renders the standalone text/code/markdown viewer window.""" - if not self.show_text_viewer: return - imgui.set_next_window_size(imgui.ImVec2(900, 700), imgui.Cond_.first_use_ever) - expanded, opened = imgui.begin(f"Text Viewer - {self.text_viewer_title}", self.show_text_viewer) - self.show_text_viewer = bool(opened) - if not opened: - self.ui_editing_slices_file = None - self._slice_sel_start = -1 - self._slice_sel_end = -1 - if expanded: - if self.ui_editing_slices_file is not None: - imgui.text_colored(C_IN, "Slice Management (Click-drag lines to select range)") - if imgui.button("Add Selection as Slice"): - if self._slice_sel_start != -1 and self._slice_sel_end != -1: - s_line = min(self._slice_sel_start, self._slice_sel_end) - e_line = max(self._slice_sel_start, self._slice_sel_end) - from src.fuzzy_anchor import FuzzyAnchor - slice_data = FuzzyAnchor.create_slice(self.text_viewer_content, s_line, e_line) - slice_data['tag'] = ""; slice_data['comment'] = "" - self.ui_editing_slices_file.custom_slices.append(slice_data) - self._slice_sel_start = -1; self._slice_sel_end = -1 - imgui.same_line() - if imgui.button("Clear Selection"): self._slice_sel_start = -1; self._slice_sel_end = -1 - to_remove = -1 - for idx, slc in enumerate(self.ui_editing_slices_file.custom_slices): - imgui.push_id(f"slc_row_{idx}"); imgui.text(f"Slice {idx+1}: {slc['start_line']}-{slc['end_line']}"); imgui.same_line() - imgui.set_next_item_width(100); changed_tag, new_tag = imgui.input_text("Tag", slc.get('tag', '')) - if changed_tag: slc['tag'] = new_tag - imgui.same_line(); imgui.set_next_item_width(200); changed_comm, new_comm = imgui.input_text("Comment", slc.get('comment', '')) - if changed_comm: slc['comment'] = new_comm - imgui.same_line() - if imgui.button("Remove"): to_remove = idx - imgui.pop_id() - if to_remove != -1: self.ui_editing_slices_file.custom_slices.pop(to_remove) - imgui.separator() - if imgui.button("Copy"): imgui.set_clipboard_text(self.text_viewer_content) - imgui.same_line(); _, self.text_viewer_wrap = imgui.checkbox("Word Wrap", self.text_viewer_wrap) - imgui.separator() - renderer = markdown_helper.get_renderer(); tv_type = getattr(self, "text_viewer_type", "text") - if tv_type == 'markdown': - with imscope.child("tv_md_scroll", -1, -1, True): markdown_helper.render(self.text_viewer_content, context_id='text_viewer') - elif self.ui_editing_slices_file is not None: - with imscope.child("slice_editor_content", -1, -1, True): - lines = self.text_viewer_content.splitlines(); draw_list = imgui.get_window_draw_list() - for i, line_text in enumerate(lines): - line_num = i + 1; pos = imgui.get_cursor_screen_pos(); line_height = imgui.get_text_line_height() - is_sliced = any(slc['start_line'] <= line_num <= slc['end_line'] for slc in self.ui_editing_slices_file.custom_slices) - if is_sliced: draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(255, 165, 0, 0.2))) - if self._slice_sel_start != -1 and self._slice_sel_end != -1: - s, e = min(self._slice_sel_start, self._slice_sel_end), max(self._slice_sel_start, self._slice_sel_end) - if s <= line_num <= e: draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(100, 100, 255, 0.3))) - imgui.selectable(f"{line_num:4} | {line_text}##ln{line_num}", False) - if imgui.is_item_clicked(): self._slice_sel_start = line_num; self._slice_sel_end = line_num - if imgui.is_item_hovered() and imgui.is_mouse_down(0): self._slice_sel_end = line_num - elif tv_type in renderer._lang_map: - if self._text_viewer_editor is None: - self._text_viewer_editor = ced.TextEditor(); self._text_viewer_editor.set_read_only_enabled(True); self._text_viewer_editor.set_show_line_numbers_enabled(True) + with imscope.menu("manual slop") as (active): + if active and imgui.menu_item("Quit", "Ctrl+Q", False)[0]: + self.runner_params.app_shall_exit = True + with imscope.menu("Windows") as (active): + if (active): + for w in self.show_windows.keys(): + _, self.show_windows[w] = imgui.menu_item(w, "", self.show_windows[w]) + with imscope.menu("Project") as (active): + if active and imgui.menu_item("Save All", "", False)[0]: + self._flush_to_project() + self._flush_to_config() + models.save_config(self.config) + self.ai_status = "config saved" + if imgui.menu_item("Reset Session", "", False)[0]: + ai_client.reset_session() + ai_client.clear_comms_log() + self._tool_log.clear() + self._comms_log.clear() + self.ai_status = "session reset" + self.ai_response = "" + if imgui.menu_item("Generate MD Only", "", False)[0]: try: - self._text_viewer_editor.set_text(self.text_viewer_content) - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_text_viewer_ced") - self._text_viewer_editor.render(f"##ced_{self.text_viewer_title}", imgui.ImVec2(-1, -1)) - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_text_viewer_ced") - except Exception as e: imgui.text_colored(vec4(255, 100, 100), f"CED Error: {e}"); imgui.text_unformatted(self.text_viewer_content) - else: - with imscope.child("tv_scroll", -1, -1, True): - if self.text_viewer_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) - imgui.text_unformatted(self.text_viewer_content) - if self.text_viewer_wrap: imgui.pop_text_wrap_pos() - imgui.end() - # Sync text and language - - #region: Inject File Modal - if getattr(self, "show_inject_modal", False): - imgui.open_popup("Inject File") - self.show_inject_modal = False - - if imgui.begin_popup_modal("Inject File", None, imgui.WindowFlags_.always_auto_resize)[0]: - files = self.project.get('files', {}).get('paths', []) - imgui.text("Select File to Inject:") - imgui.begin_child("inject_file_list", imgui.ImVec2(0, 200), True) - for f_path in files: - is_selected = (self._inject_file_path == f_path) - if imgui.selectable(f_path, is_selected)[0]: - self._inject_file_path = f_path - self.controller._update_inject_preview() - imgui.end_child() + md, path, *_ = self._do_generate() + self.last_md = md + self.last_md_path = path + self.ai_status = f"md written: {path.name}" + except Exception as e: + self.ai_status = f"error: {e}" + with imscope.menu("Layout") as (active): + if active and imgui.menu_item("Save Current...", "", False)[0]: + self._show_save_workspace_profile_modal = True + self._new_workspace_profile_name = "" imgui.separator() - if imgui.radio_button("Skeleton", self._inject_mode == "skeleton"): - self._inject_mode = "skeleton" - self.controller._update_inject_preview() - imgui.same_line() - if imgui.radio_button("Full", self._inject_mode == "full"): - self._inject_mode = "full" - self.controller._update_inject_preview() + for profile_id, profile in self.workspace_profiles.items(): + if imgui.menu_item(profile.name, "", False)[0]: + self.controller._cb_load_workspace_profile(profile_id) imgui.separator() - imgui.text("Preview:") - imgui.begin_child("inject_preview_area", imgui.ImVec2(600, 300), True) - imgui.text_unformatted(self._inject_preview) - imgui.end_child() - imgui.separator() - if imgui.button("Inject", imgui.ImVec2(120, 0)): - formatted = f"## File: {self._inject_file_path}\n```python\n{self._inject_preview}\n```\n" - with self._disc_entries_lock: - self.disc_entries.append({ - "role": "Context", - "content": formatted, - "collapsed": True, - "ts": project_manager.now_ts() - }) - self._scroll_disc_to_bottom = True - imgui.close_current_popup() + with imscope.menu("Delete Profile") as (active): + if active: + for profile_id, profile in self.workspace_profiles.items(): + if imgui.menu_item(profile.name, "", False)[0]: + self.controller._cb_delete_workspace_profile(profile_id, self._new_workspace_profile_scope) + + # RAG status indicator + if self.controller.rag_config and self.controller.rag_config.enabled: imgui.same_line() - if imgui.button("Cancel", imgui.ImVec2(120, 0)): - imgui.close_current_popup() - imgui.end_popup() - #endregion: Inject File Modal - - self._render_ast_inspector_modal() - return + status = self.controller.rag_status + if status == "indexing...": color = vec4(100, 255, 100) + elif status == "error": color = vec4(255, 100, 100) + else: color = vec4(180, 180, 180) - def _render_ast_inspector_modal(self) -> None: - if self._show_ast_inspector: - imgui.open_popup('AST Inspector') - self._show_ast_inspector = False + imgui.text_colored(color, f"[RAG: {status}]") + if imgui.is_item_hovered(): imgui.set_tooltip(f"RAG is enabled. Status: {status}. Click to rebuild index.") + if imgui.is_item_clicked(): self.controller.event_queue.put('click', 'btn_rebuild_rag_index') - #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 + # Draw right-aligned window controls directly in the menu bar (Win32 only) + if sys.platform == "win32": + try: + import ctypes + ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p + ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object, ctypes.c_char_p] + hwnd_capsule = imgui.get_main_viewport().platform_handle_raw + hwnd = ctypes.pythonapi.PyCapsule_GetPointer(hwnd_capsule, b"nb_handle") + except Exception: + hwnd = 0 - 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_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): - from src import models - # Create a temporary selection set if not initialized - if not hasattr(self, '_ui_picker_selected'): - self._ui_picker_selected = set() + if hwnd: + btn_w = 40 + # Use window width (points) instead of display_size (pixels) for correct scaling + window_w = imgui.get_window_width() + bar_h = imgui.get_window_height() + right_x = window_w - (btn_w * 3) + # Drag area check using an explicit invisible button spanning the empty space + curr_x = imgui.get_cursor_pos_x() + drag_w = right_x - curr_x + if drag_w > 0: + imgui.invisible_button("##drag_area", (drag_w, bar_h)) + if imgui.is_item_active() and imgui.is_mouse_dragging(0): + # CRITICAL: We must reset ImGui's mouse_down state BEFORE passing control to Windows. + # Otherwise, the Windows modal drag loop swallows the WM_LBUTTONUP event, + # and ImGui thinks the mouse is permanently held down, causing "sticky" dragging. + imgui.get_io().mouse_down[0] = False + win32gui.ReleaseCapture() + win32gui.SendMessage(hwnd, win32con.WM_NCLBUTTONDOWN, win32con.HTCAPTION, 0) - 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.push_style_color(imgui.Col_.button, vec4(0, 0, 0, 0)) + + try: + is_max = win32gui.GetWindowPlacement(hwnd)[1] == win32con.SW_SHOWMAXIMIZED + except Exception: + is_max = False + + # Explicitly set Y to 0 and match button height to bar height for perfect alignment + imgui.set_cursor_pos((right_x, 0)) + if imgui.button("_", (btn_w, bar_h)): + win32gui.ShowWindow(hwnd, win32con.SW_MINIMIZE) + + imgui.set_cursor_pos((right_x + btn_w, 0)) + if imgui.button("[=]" if is_max else "[]", (btn_w, bar_h)): + win32gui.ShowWindow(hwnd, win32con.SW_RESTORE if is_max else win32con.SW_MAXIMIZE) + + imgui.set_cursor_pos((right_x + btn_w * 2, 0)) + imgui.push_style_color(imgui.Col_.button_hovered, vec4(200, 50, 50, 255)) + if imgui.button("X", (btn_w, bar_h)): + win32gui.PostMessage(hwnd, win32con.WM_CLOSE, 0, 0) + imgui.pop_style_color() + + imgui.pop_style_color() - imgui.same_line() - if imgui.button("Cancel", imgui.ImVec2(120, 0)): - if hasattr(self, '_ui_picker_selected'): - self._ui_picker_selected.clear() + def _render_history_window(self) -> None: + if not self.show_windows.get('Undo/Redo History', False): + return + def iterate_history(history: typing.List[typing.Dict[str, typing.Any]]): + 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: self._handle_jump_to_history(actual_idx) + with imscope.window("Undo/Redo History", self.show_windows['Undo/Redo History']) as (exp, opened): + self.show_windows['Undo/Redo History'] = bool(opened) + if exp: + if imgui.button("Undo") and self.history.can_undo: self._handle_undo(); imgui.same_line() + if imgui.button("Redo") and self.history.can_redo: self._handle_redo() + imgui.separator() + with imscope.child("history_list", imgui.ImVec2(0, 0), True): + history = self.history.get_history() + if not history: imgui.text("No history available.") + else: iterate_history() + + def _render_shader_live_editor(self) -> None: + """ + [C: tests/test_shader_live_editor.py:test_shader_live_editor_renders] + """ + if self.show_windows.get('Shader Editor', False): + with imscope.window('Shader Editor', self.show_windows['Shader Editor']) as (exp, opened): + self.show_windows['Shader Editor'] = bool(opened) + if exp: + changed_crt, self.shader_uniforms['crt'] = imgui.slider_float('CRT Curvature', self.shader_uniforms['crt'], 0.0, 2.0) + changed_scan, self.shader_uniforms['scanline'] = imgui.slider_float('Scanline Intensity', self.shader_uniforms['scanline'], 0.0, 1.0) + changed_bloom, self.shader_uniforms['bloom'] = imgui.slider_float('Bloom Threshold', self.shader_uniforms['bloom'], 0.0, 1.0) + + def _render_approve_script_modal(self) -> None: + """Renders the modal dialog for approving AI-generated PowerShell scripts.""" + with self._pending_dialog_lock: + dlg = self._pending_dialog + if dlg: + if not self._pending_dialog_open: + imgui.open_popup("Approve PowerShell Command") + self._pending_dialog_open = True + else: + self._pending_dialog_open = False + + if imgui.begin_popup_modal("Approve PowerShell Command", None, imgui.WindowFlags_.always_auto_resize)[0]: + if not dlg: imgui.close_current_popup() + else: + imgui.text("The AI wants to run the following PowerShell script:") + imgui.text_colored(vec4(200, 200, 100), f"base_dir: {dlg._base_dir}") + imgui.separator() + # Checkbox to toggle full preview inside modal + _, self.show_text_viewer = imgui.checkbox("Show Full Preview", self.show_text_viewer) + if self.show_text_viewer: + imgui.begin_child("preview_child", imgui.ImVec2(600, 300), True) + imgui.text_unformatted(dlg._script) + imgui.end_child() + else: + ch, dlg._script = imgui.input_text_multiline("##confirm_script", dlg._script, imgui.ImVec2(-1, 200)) + imgui.separator() + if imgui.button("Approve & Run", imgui.ImVec2(120, 0)): + with dlg._condition: + dlg._approved = True + dlg._done = True + dlg._condition.notify_all() + with self._pending_dialog_lock: + self._pending_dialog = None + imgui.close_current_popup() + imgui.same_line() + if imgui.button("Reject", imgui.ImVec2(120, 0)): + with dlg._condition: + dlg._approved = False + dlg._done = True + dlg._condition.notify_all() + with self._pending_dialog_lock: + self._pending_dialog = None + imgui.close_current_popup() imgui.end_popup() +#region: Diangostics & Analytics + + def _render_cache_panel(self) -> None: + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_cache_panel") + if self.current_provider != "gemini": + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_cache_panel") + return + imgui.text_colored(C_LBL, 'Cache Analytics') + stats = getattr(self.controller, '_cached_cache_stats', {}) + if not stats.get("cache_exists"): + imgui.text_disabled("No active cache") + if self.perf_profiling_enabled: self.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"): + self.controller.clear_cache() + self._cache_cleared_timestamp = time.time() + if hasattr(self, '_cache_cleared_timestamp') and time.time() - self._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 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): + self.show_windows["Diagnostics"] = bool(opened) + if exp: + metrics = self.perf_monitor.get_metrics() + imgui.text("Performance Telemetry") + imgui.same_line() + _, self.perf_profiling_enabled = imgui.checkbox("Enable Profiling", self.perf_profiling_enabled) + imgui.separator() + + if imgui.begin_table("perf_table", 3, imgui.TableFlags_.borders_inner_h): + imgui.table_setup_column("Metric") + imgui.table_setup_column("Value") + imgui.table_setup_column("Graph") + imgui.table_headers_row() + + for label, key, format_str in [ + ("FPS", "fps", "%.1f"), + ("Frame Time (ms)", "frame_time_ms", "%.2f"), + ("CPU %", "cpu_percent", "%.1f"), + ("Input Lag (ms)", "input_lag_ms", "%.1f") + ]: + imgui.table_next_row() + imgui.table_next_column() + imgui.text(label) + imgui.table_next_column() + if key == "fps": + avg_val = imgui.get_io().framerate + else: + avg_val = metrics.get(f"{key}_avg", metrics.get(key, 0.0)) + imgui.text(format_str % avg_val) + imgui.table_next_column() + self.perf_show_graphs.setdefault(key, False) + _, self.perf_show_graphs[key] = imgui.checkbox(f"##g_{key}", self.perf_show_graphs[key]) + imgui.end_table() + + if self.perf_profiling_enabled: + imgui.separator() + imgui.text("Detailed Component Timings (Moving Average)") + if imgui.begin_table("comp_timings", 6, imgui.TableFlags_.borders): + imgui.table_setup_column("Component") + imgui.table_setup_column("Avg (ms)") + imgui.table_setup_column("Count") + imgui.table_setup_column("Max (ms)") + imgui.table_setup_column("Min (ms)") + imgui.table_setup_column("Graph") + imgui.table_headers_row() + for key, val in metrics.items(): + if key.startswith("time_") and key.endswith("_ms") and not key.endswith("_avg"): + comp_name = key[5:-3] + avg_val = metrics.get(f"{key}_avg", val) + count = int(metrics.get(f"count_{comp_name}", 0)) + max_val = metrics.get(f"max_{comp_name}_ms", 0.0) + min_val = metrics.get(f"min_{comp_name}_ms", 0.0) + imgui.table_next_row() + imgui.table_next_column() + imgui.text(comp_name) + imgui.table_next_column() + if avg_val > 10.0: + imgui.text_colored(imgui.ImVec4(1.0, 0.2, 0.2, 1.0), f"{avg_val:.2f}") + else: + imgui.text(f"{avg_val:.2f}") + imgui.table_next_column() + imgui.text(f"{count}") + imgui.table_next_column() + imgui.text(f"{max_val:.2f}") + imgui.table_next_column() + imgui.text(f"{min_val:.2f}") + imgui.table_next_column() + self.perf_show_graphs.setdefault(comp_name, False) + _, self.perf_show_graphs[comp_name] = imgui.checkbox(f"##g_{comp_name}", self.perf_show_graphs[comp_name]) + imgui.end_table() + + imgui.separator() + imgui.text("Performance Graphs") + for key, show in self.perf_show_graphs.items(): + if show: + imgui.text(f"History: {key}") + hist_data = self.perf_monitor.get_history(key) + if hist_data: + import numpy as np + imgui.plot_lines(f"##plot_{key}", np.array(hist_data, dtype=np.float32), graph_size=imgui.ImVec2(-1, 60)) + else: + imgui.text_disabled(f"(no history data for {key})") + + imgui.separator() + imgui.text("Diagnostic Log") + if imgui.begin_table("diag_log_table", 3, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable): + imgui.table_setup_column("Timestamp", imgui.TableColumnFlags_.width_fixed, 150) + imgui.table_setup_column("Type", imgui.TableColumnFlags_.width_fixed, 100) + imgui.table_setup_column("Message") + imgui.table_headers_row() + for entry in reversed(self.controller.diagnostic_log): + imgui.table_next_row() + imgui.table_next_column() + imgui.text(entry.get("ts", "")) + imgui.table_next_column() + imgui.text(entry.get("type", "")) + imgui.table_next_column() + imgui.text_wrapped(entry.get("message", "")) + imgui.end_table() + + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_diagnostics_panel") + +#endregion: Diangostics & Analytics + +#region: AI Settings + + def _render_ai_settings_hub(self) -> None: + self._render_persona_selector_panel() + if imgui.collapsing_header("Provider & Model"): self._render_provider_panel() + if imgui.collapsing_header("System Prompts"): self._render_system_prompts_panel() + if imgui.collapsing_header("RAG Settings"): self._render_rag_panel() + self._render_agent_tools_panel() + + def _render_rag_panel(self) -> None: + conf = self.controller.rag_config + if not conf: return + ch, conf.enabled = imgui.checkbox("Enable RAG", conf.enabled) + + imgui.text("Vector Store Provider") + providers = ['chroma', 'qdrant', 'mock'] + try: + idx = providers.index(conf.vector_store.provider) + except (ValueError, AttributeError): + idx = 0 + ch2, next_idx = imgui.combo("##rag_provider", idx, providers) + if ch2: + conf.vector_store.provider = providers[next_idx] + + imgui.text("Embedding Provider") + emb_providers = ['gemini', 'local'] + try: + idx_e = emb_providers.index(conf.embedding_provider) + except (ValueError, AttributeError): + idx_e = 0 + ch3, next_idx_e = imgui.combo("##rag_emb_provider", idx_e, emb_providers) + if ch3: + conf.embedding_provider = emb_providers[next_idx_e] + + imgui.text("Chunk Size") + imgui.set_next_item_width(150) + ch4, conf.chunk_size = imgui.input_int("##rag_chunk_size", conf.chunk_size) + imgui.text("Chunk Overlap") + imgui.set_next_item_width(150) + ch5, conf.chunk_overlap = imgui.input_int("##rag_chunk_overlap", conf.chunk_overlap) + + imgui.separator() + imgui.text(f"Status: {self.controller.rag_status}") + + if imgui.button("Rebuild Index"): + self.controller.event_queue.put('click', 'btn_rebuild_rag_index') + + def _render_system_prompts_panel(self) -> None: + imgui.text("Global System Prompt (all projects)") + preset_names = sorted(self.controller.presets.keys()) + current_global = self.controller.ui_global_preset_name or "Select Preset..." + imgui.set_next_item_width(200) + if imgui.begin_combo("##global_preset", current_global): + for name in preset_names: + is_sel = (name == current_global) + if imgui.selectable(name, is_sel)[0]: + self.controller._apply_preset(name, "global") + if is_sel: + imgui.set_item_default_focus() + imgui.end_combo() + imgui.same_line(0, 8) + if imgui.button("Manage Presets##global"): + self.show_preset_manager_window = True + imgui.set_item_tooltip("Open preset management modal") + ch, self.ui_global_system_prompt = imgui.input_text_multiline("##gsp", self.ui_global_system_prompt, imgui.ImVec2(-1, 100)) + imgui.separator() + _, self.ui_use_default_base_prompt = imgui.checkbox("Use Default Base System Prompt", self.ui_use_default_base_prompt) + imgui.same_line() + if imgui.button("Reset to Default##btn_reset_base_prompt"): + self.controller._cb_reset_base_prompt() + imgui.same_line() + if imgui.button("Show Diff##btn_show_base_prompt_diff"): + self.controller._cb_show_base_prompt_diff() + imgui.set_item_tooltip("Compare current base prompt with the default.") + + imgui.same_line() + imgui.text_disabled("(?)") + imgui.set_item_tooltip("The Base System Prompt contains foundational instructions for the AI, including its role as a coding assistant and safety guidelines. You can override it here if needed.") + + header_flags = imgui.TreeNodeFlags_.default_open if not self.ui_use_default_base_prompt else 0 + if imgui.collapsing_header("Base System Prompt (foundational instructions)", header_flags): + if self.ui_use_default_base_prompt: + imgui.begin_disabled() + imgui.input_text_multiline("##base_prompt_def", ai_client._SYSTEM_PROMPT, imgui.ImVec2(-1, 100), imgui.InputTextFlags_.read_only) + imgui.end_disabled() + imgui.text_disabled(f"Characters: {len(ai_client._SYSTEM_PROMPT)}") + else: + ch, self.ui_base_system_prompt = imgui.input_text_multiline("##base_prompt", self.ui_base_system_prompt, imgui.ImVec2(-1, 150)) + imgui.text_disabled(f"Characters: {len(self.ui_base_system_prompt)}") + imgui.separator() + imgui.text("Project System Prompt") + current_project = self.controller.ui_project_preset_name or "Select Preset..." + imgui.set_next_item_width(200) + if imgui.begin_combo("##project_preset", current_project): + for name in preset_names: + is_sel = (name == current_project) + if imgui.selectable(name, is_sel)[0]: + self.controller._apply_preset(name, "project") + if is_sel: + imgui.set_item_default_focus() + imgui.end_combo() + imgui.same_line(0, 8) + if imgui.button("Manage Presets##project"): + self.show_preset_manager_window = True + imgui.set_item_tooltip("Open preset management modal") + ch, self.ui_project_system_prompt = imgui.input_text_multiline("##psp", self.ui_project_system_prompt, imgui.ImVec2(-1, 100)) + + def _render_agent_tools_panel(self) -> None: + if imgui.collapsing_header("Active Tool Presets & Biases", imgui.TreeNodeFlags_.default_open): + imgui.text("Tool Preset") + presets = self.controller.tool_presets + preset_names = [""] + sorted(list(presets.keys())) + + active = getattr(self, "ui_active_tool_preset", "") + if active is None: active = "" + try: + idx = preset_names.index(active) + except ValueError: + idx = 0 + + ch, new_idx = imgui.combo("##tool_preset_select", idx, preset_names) + if ch: + self.ui_active_tool_preset = preset_names[new_idx] + + imgui.same_line() + if imgui.button("Manage Presets##tools"): + self.show_tool_preset_manager_window = True + if imgui.is_item_hovered(): + imgui.set_tooltip("Configure tool availability and default modes.") + + imgui.dummy(imgui.ImVec2(0, 4)) + imgui.text("Bias Profile") + if imgui.begin_combo("##bias", getattr(self, 'ui_active_bias_profile', "") or "None"): + if imgui.selectable("None", not getattr(self, 'ui_active_bias_profile', ""))[0]: + self.ui_active_bias_profile = "" + ai_client.set_bias_profile(None) + for bname in sorted(self.controller.bias_profiles.keys()): + if not bname: + continue + if imgui.selectable(bname, bname == getattr(self, 'ui_active_bias_profile', ""))[0]: + self.ui_active_bias_profile = bname + ai_client.set_bias_profile(bname) + imgui.end_combo() + + imgui.dummy(imgui.ImVec2(0, 8)) + cat_options = ["All"] + sorted(list(models.DEFAULT_TOOL_CATEGORIES.keys())) + try: + f_idx = cat_options.index(self.ui_tool_filter_category) + except ValueError: + f_idx = 0 + imgui.set_next_item_width(200) + ch_cat, next_f_idx = imgui.combo("Filter Category##agent", f_idx, cat_options) + if ch_cat: self.ui_tool_filter_category = cat_options[next_f_idx] + + imgui.dummy(imgui.ImVec2(0, 8)) + active_name = self.ui_active_tool_preset + if active_name and active_name in presets: + preset = presets[active_name] + for cat_name, tools in preset.categories.items(): + if self.ui_tool_filter_category != "All" and self.ui_tool_filter_category != cat_name: continue + if imgui.tree_node(cat_name): + for tool in tools: + if tool.weight >= 5: + imgui.text_colored(vec4(255, 100, 100), "[HIGH]"); imgui.same_line() + elif tool.weight == 4: + imgui.text_colored(vec4(255, 255, 100), "[PREF]"); imgui.same_line() + elif tool.weight == 2: + imgui.text_colored(vec4(255, 150, 50), "[REJECT]"); imgui.same_line() + elif tool.weight <= 1: + imgui.text_colored(vec4(180, 180, 180), "[LOW]"); imgui.same_line() + + imgui.text(tool.name); imgui.same_line(180) + + mode = tool.approval + if imgui.radio_button(f"Auto##{cat_name}_{tool.name}", mode == "auto"): tool.approval = "auto" + imgui.same_line() + if imgui.radio_button(f"Ask##{cat_name}_{tool.name}", mode == "ask"): tool.approval = "ask" + imgui.tree_pop() + def _render_preset_manager_content(self, is_embedded: bool = False) -> None: avail = imgui.get_content_region_avail() if not hasattr(self, "_prompt_md_preview"): self._prompt_md_preview = False @@ -2629,6 +1761,1203 @@ class App: self.show_persona_editor_window = False imgui.end_table() +#endregion: AI Settings + +#region: Context Management + + def _render_files_and_media(self) -> None: + avail = imgui.get_content_region_avail().y + if not hasattr(self, 'files_screenshots_split'): self.files_screenshots_split = 0.65 + split_y = int(avail * self.files_screenshots_split) + if imgui.collapsing_header("Files", imgui.TreeNodeFlags_.default_open): + with imscope.child("Files_child", imgui.ImVec2(-1, split_y), True): + if not hasattr(self, 'files_last_selected'): self.files_last_selected = -1 + + with imscope.table("files_table", 5, imgui.TableFlags_.resizable | imgui.TableFlags_.borders): + imgui.table_setup_column("", imgui.TableColumnFlags_.width_fixed, 20) + imgui.table_setup_column("Path", imgui.TableColumnFlags_.width_stretch) + imgui.table_setup_column("Agg", imgui.TableColumnFlags_.width_fixed, 0) + imgui.table_setup_column("Full", imgui.TableColumnFlags_.width_fixed, 0) + imgui.table_setup_column("Cache", imgui.TableColumnFlags_.width_fixed, 0) + imgui.table_headers_row() + + self.files.sort(key=lambda f: f.path.lower() if hasattr(f, 'path') else str(f).lower()) + for i, f_item in enumerate(self.files): + imgui.table_next_row(); imgui.table_set_column_index(0) + clicked, f_item.selected = imgui.checkbox(f"##{i}", f_item.selected) + if clicked: + if (imgui.is_key_down(imgui.Key.left_shift) or imgui.is_key_down(imgui.Key.right_shift)) and self.files_last_selected >= 0: + start_i = min(self.files_last_selected, i) + end_i = max(self.files_last_selected, i) + for j in range(start_i, end_i + 1): self.files[j].selected = True + self.files_last_selected = i + imgui.table_set_column_index(1); imgui.text(f_item.path if hasattr(f_item, 'path') else str(f_item)) + imgui.table_set_column_index(2) + if f_item.auto_aggregate: imgui.text_colored(imgui.ImVec4(0.3, 0.8, 1, 1), "A") + else: imgui.text_disabled(" ") + + imgui.same_line(spacing=1) + if imgui.invisible_button(f"agg{i}", imgui.ImVec2(15, 15)): + f_item.auto_aggregate = not f_item.auto_aggregate + if f_item.auto_aggregate: f_item.force_full = False + + imgui.table_set_column_index(3) + if f_item.force_full: imgui.text_colored(imgui.ImVec4(1, 0.6, 0.3, 1), "F") + else: imgui.text_disabled(" ") + + imgui.same_line(spacing=1) + if imgui.invisible_button(f"full{i}", imgui.ImVec2(15, 15)): + f_item.force_full = not f_item.force_full + if f_item.force_full: f_item.auto_aggregate = False + + imgui.table_set_column_index(4) + fpath = f_item.path if hasattr(f_item, 'path') else str(f_item) + is_cached = any(fpath in c for c in getattr(self, '_cached_files', [])) + if is_cached: imgui.text_colored(imgui.ImVec4(0, 1, 0, 1), "Y") + else: imgui.text_colored(imgui.ImVec4(0.5, 0.5, 0.5, 1), "-") + + if imgui.button("Add Files##addf"): + r = hide_tk_root(); paths = filedialog.askopenfilenames(); r.destroy() + for p in paths: + if p not in [f.path if hasattr(f, 'path') else f for f in self.files]: self.files.append(models.FileItem(path=p)) + + imgui.same_line() + if imgui.button("Sel All##selall"): + for f in self.files: + f.selected = True + + imgui.same_line() + if imgui.button("Unsel##unselall"): + for f in self.files: + f.selected = False + + imgui.same_line() + if imgui.button("None##nonesel"): + for f in self.files: + if f.selected: + f.auto_aggregate = False + f.force_full = False + + imgui.same_line() + if imgui.button("Agg##aggsel"): + for f in self.files: + if f.selected: + f.auto_aggregate = True + f.force_full = False + + imgui.same_line() + if imgui.button("Full##fullsel"): + for f in self.files: + if f.selected: + f.force_full = True + f.auto_aggregate = False + + imgui.same_line() + if imgui.button("Del##dels"): + self.files = [f for f in self.files if not f.selected] + + imgui.separator() + + if imgui.collapsing_header("Screenshots", imgui.TreeNodeFlags_.default_open): + with imscope.child("Shots_child", imgui.ImVec2(-1, -1), True): + for i, s in enumerate(self.screenshots): + if imgui.button(f"x##s{i}"): + self.screenshots.pop(i) + break + imgui.same_line(); imgui.text(s) + if imgui.button("Add Screenshots##adds"): + r = hide_tk_root(); paths = filedialog.askopenfilenames(filetypes=[("Images", "*.png *.jpg *.jpeg *.gif *.bmp *.webp"), ("All", "*.*")]); r.destroy() + for p in paths: + if p not in self.screenshots: self.screenshots.append(p) + return + + def _render_files_panel(self, height_override: float = 0) -> None: + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_files_panel") + imgui.text("Paths") + imgui.same_line() + imgui.text("| Base Dir:") + imgui.same_line() + imgui.set_next_item_width(-100) + ch, self.ui_files_base_dir = imgui.input_text("##f_base", self.ui_files_base_dir) + imgui.same_line() + if imgui.button("Browse##fb"): + r = hide_tk_root() + d = filedialog.askdirectory() + r.destroy() + if d: self.ui_files_base_dir = d + imgui.separator() + # Calculate content-based height: use override if provided, else content-based + if height_override > 0: + child_h = height_override + else: + row_count = max(len(self.files), 1) + child_h = min(row_count * 28 + 40, 300) + # BEGIN f_paths child window + imgui.begin_child("f_paths", imgui.ImVec2(0, child_h), True) + if imgui.begin_table("files_table", 4, imgui.TableFlags_.resizable | imgui.TableFlags_.borders): + imgui.table_setup_column("Actions", imgui.TableColumnFlags_.width_fixed, 40) + imgui.table_setup_column("File Path", imgui.TableColumnFlags_.width_stretch) + imgui.table_setup_column("Flags", imgui.TableColumnFlags_.width_fixed, 150) + imgui.table_setup_column("Cache", imgui.TableColumnFlags_.width_fixed, 40) + imgui.table_headers_row() + + for i, f_item in enumerate(self.files): + imgui.table_next_row() + # Actions + imgui.table_set_column_index(0) + if imgui.button(f"x##f{i}"): + self.files.pop(i) + break + # File Path + imgui.table_set_column_index(1) + imgui.text(f_item.path if hasattr(f_item, "path") else str(f_item)) + # Flags + imgui.table_set_column_index(2) + if hasattr(f_item, "auto_aggregate"): + changed_agg, f_item.auto_aggregate = imgui.checkbox(f"Agg##a{i}", f_item.auto_aggregate) + imgui.same_line() + changed_full, f_item.force_full = imgui.checkbox(f"Full##f{i}", f_item.force_full) + # Cache + imgui.table_set_column_index(3) + path = f_item.path if hasattr(f_item, "path") else str(f_item) + is_cached = any(path in c for c in getattr(self, "_cached_files", [])) + if is_cached: + imgui.text_colored("●", imgui.ImVec4(0, 1, 0, 1)) # Green dot + else: + imgui.text_disabled("○") + imgui.end_table() + imgui.end_child() + if imgui.button("Add File(s)"): + r = hide_tk_root() + paths = filedialog.askopenfilenames() + r.destroy() + for p in paths: + if p not in [f.path if hasattr(f, "path") else f for f in self.files]: + self.files.append(models.FileItem(path=p)) + imgui.same_line() + if imgui.button("Add Wildcard"): + r = hide_tk_root() + d = filedialog.askdirectory() + r.destroy() + if d: self.files.append(models.FileItem(path=str(Path(d) / "**" / "*"))) + + imgui.separator() + from src import summarize + stats = summarize._summary_cache.get_stats() + imgui.text_disabled(f"Summary Cache: {stats['entries']} entries ({stats['size_bytes']} bytes)") + imgui.same_line() + if imgui.button("Clear Summary Cache##btn_clear_summary_cache"): + self.controller._cb_clear_summary_cache() + + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_files_panel") + +#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, _): + if exp: self._render_discussion_tab() + with imscope.tab_item("Context Composition") as (exp, _): + if exp: self._render_context_composition_panel() + with imscope.tab_item("Snapshot") as (exp, _): + if exp: self._render_snapshot_tab() + with imscope.tab_item("Takes") as (exp, _): + if exp: self._render_takes_panel() + + def _render_discussion_entries(self) -> None: + with imscope.child("disc_scroll"): + display_entries = self.disc_entries + if self.ui_focus_agent: + tier_usage = self.mma_tier_usage.get(self.ui_focus_agent) + if tier_usage: + persona_name = tier_usage.get("persona") + if persona_name: display_entries = [e for e in self.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): + self._render_discussion_entry(display_entries[i], i) + if self._scroll_disc_to_bottom: imgui.set_scroll_here_y(1.0); self._scroll_disc_to_bottom = False + + def _render_discussion_entry(self, entry: dict, index: int) -> None: + with imscope.id(f"disc_{index}"): + collapsed, read_mode = entry.get("collapsed", False), entry.get("read_mode", False) + if imgui.button("+" if collapsed else "-"): entry["collapsed"] = not collapsed + imgui.same_line(); self._render_text_viewer(f"Entry #{index+1}", entry["content"]); imgui.same_line(); imgui.set_next_item_width(120) + if imgui.begin_combo("##role", entry["role"]): + for r in self.disc_roles: + if imgui.selectable(r, r == entry["role"])[0]: entry["role"] = r + imgui.end_combo() + if not collapsed: + imgui.same_line() + if imgui.button("[Edit]" if read_mode else "[Read]"): entry["read_mode"] = not read_mode + ts_str = entry.get("ts", "") + if ts_str: + imgui.same_line(); imgui.text_colored(vec4(120, 120, 100), str(ts_str)); e_dt = project_manager.parse_ts(ts_str) + if e_dt: + e_unix, next_unix = e_dt.timestamp(), float('inf') + if index + 1 < len(self.disc_entries): + n_ts = self.disc_entries[index+1].get("ts", ""); n_dt = project_manager.parse_ts(n_ts) + if n_dt: next_unix = n_dt.timestamp() + injected = [f for f in self.files if hasattr(f, 'injected_at') and f.injected_at and e_unix <= f.injected_at < next_unix] + if injected: + imgui.same_line(); imgui.text_colored(vec4(100, 255, 100), f"[{len(injected)}+]") + if imgui.is_item_hovered(): imgui.set_tooltip("Files injected at this point:\n" + "\n".join([f.path for f in injected])) + if collapsed: + imgui.same_line() + if imgui.button("Ins"): self.disc_entries.insert(index, {"role": "User", "content": "", "collapsed": True, "ts": project_manager.now_ts()}) + imgui.same_line() + if imgui.button("Del"): self.disc_entries.pop(index); return + imgui.same_line() + if imgui.button("Branch"): self._branch_discussion(index) + imgui.same_line(); preview = entry["content"].replace("\n", " ")[:60] + if len(entry["content"]) > 60: preview += "..." + if not preview.strip() and entry.get("thinking_segments"): + preview = entry["thinking_segments"][0]["content"].replace("\n", " ")[:60] + if len(entry["thinking_segments"][0]["content"]) > 60: preview += "..." + imgui.text_colored(vec4(160, 160, 150), preview) + if not collapsed: + thinking_segments, has_content = entry.get("thinking_segments", []), bool(entry.get("content", "").strip()) + if thinking_segments: self._render_thinking_trace(thinking_segments, index, is_standalone=not has_content) + if read_mode: self._render_discussion_entry_read_mode(entry, index) + else: + if not (bool(thinking_segments) and not has_content): ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150)) + imgui.separator() + + def _render_discussion_entry_controls(self) -> None: + if imgui.button("+ Entry"): self.disc_entries.append({"role": self.disc_roles[0] if self.disc_roles else "User", "content": "", "collapsed": True, "ts": project_manager.now_ts()}) + imgui.same_line() + if imgui.button("-All"): + for e in self.disc_entries: e["collapsed"] = True + imgui.same_line() + if imgui.button("+All"): + for e in self.disc_entries: e["collapsed"] = False + imgui.same_line() + if imgui.button("Clear All"): self.disc_entries.clear() + imgui.same_line() + if imgui.button("Save"): self._flush_to_project(); self._flush_to_config(); models.save_config(self.config); self.ai_status = "discussion saved" + _, self.ui_auto_add_history = imgui.checkbox("Auto-add message & response to history", self.ui_auto_add_history) + imgui.text("Keep Pairs:"); imgui.same_line(); imgui.set_next_item_width(80) + ch, self.ui_disc_truncate_pairs = imgui.input_int("##trunc_pairs", self.ui_disc_truncate_pairs, 1) + if self.ui_disc_truncate_pairs < 1: self.ui_disc_truncate_pairs = 1 + imgui.same_line() + if imgui.button("Truncate"): + with self._disc_entries_lock: self.disc_entries = truncate_entries(self.disc_entries, self.ui_disc_truncate_pairs) + self.ai_status = f"history truncated to {self.ui_disc_truncate_pairs} pairs" + + def _render_discussion_entry_read_mode(self, entry: dict, index: int) -> None: + content = entry["content"] + if not content.strip(): return + if '## Retrieved Context' in content: + rag_match = re.search(r'## Retrieved Context\n\n([\s\S]*?)(?=\n\n#|\Z)', content) + if rag_match: + rag_section = rag_match.group(1) + if imgui.collapsing_header('Retrieved Context'): + chunks = re.finditer(r'### Chunk (\d+) \(Source: (.*?)\)\n([\s\S]*?)(?=\n### Chunk|\Z)', rag_section) + for chunk_match in chunks: + idx, path, chunk_content = chunk_match.group(1), chunk_match.group(2), chunk_match.group(3) + if imgui.collapsing_header(f'Chunk {idx}: {path}'): + if imgui.button(f'[Source]##rag_{index}_{idx}'): + res = mcp_client.read_file(path) + if res: self.text_viewer_title, self.text_viewer_content, self.text_viewer_type, self.show_text_viewer = path, res, (Path(path).suffix.lstrip('.') if Path(path).suffix else 'text'), True + imgui.text_unformatted(chunk_content) + content = content[:rag_match.start()] + content[rag_match.end():] + pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?") + matches, is_nerv = list(pattern.finditer(content)), theme.is_nerv_active() + if not matches: + with theme.ai_text_style(): + markdown_helper.render(content, context_id=f'disc_{index}') + else: + with imscope.child(f"read_content_{index}", size_y=150, flags=True): + if self.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) + last_idx = 0 + for m_idx, match in enumerate(matches): + before = content[last_idx:match.start()] + if before: + with theme.ai_text_style(): + markdown_helper.render(before, context_id=f'disc_{index}_b_{m_idx}') + header_text, path, code_block = match.group(0).split("\n")[0].strip(), match.group(2), match.group(4) + if imgui.collapsing_header(header_text): + if imgui.button(f"[Source]##{index}_{match.start()}"): + res = mcp_client.read_file(path) + if res: self.text_viewer_title, self.text_viewer_content, self.text_viewer_type, self.show_text_viewer = path, res, (Path(path).suffix.lstrip('.') if Path(path).suffix else 'text'), True + if code_block: + with theme.ai_text_style(): + markdown_helper.render(code_block, context_id=f'disc_{index}_c_{m_idx}') + last_idx = match.end() + after = content[last_idx:] + if after: + with theme.ai_text_style(): + markdown_helper.render(after, context_id=f'disc_{index}_a') + if self.ui_word_wrap: imgui.pop_text_wrap_pos() + + def _render_discussion_metadata(self) -> None: + disc_data = self.project.get("discussion", {}).get("discussions", {}).get(self.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() + self._render_selectable_label('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 self.ui_project_git_dir: + cmt = project_manager.get_git_commit(self.ui_project_git_dir) + if cmt: disc_data["git_commit"], disc_data["last_updated"], self.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, self.ui_disc_new_name_input = imgui.input_text("##new_disc", self.ui_disc_new_name_input); imgui.same_line() + if imgui.button("Create"): + nm = self.ui_disc_new_name_input.strip() + if nm: self._create_discussion(nm); self.ui_disc_new_name_input = "" + imgui.same_line() + if imgui.button("Rename"): + nm = self.ui_disc_new_name_input.strip() + if nm: self._rename_discussion(self.active_discussion, nm); self.ui_disc_new_name_input = "" + imgui.same_line() + if imgui.button("Delete"): self._delete_discussion(self.active_discussion) + + def _render_discussion_panel(self) -> 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_phase4.py:test_track_discussion_toggle, tests/test_gui_symbol_navigation.py:test_render_discussion_panel_symbol_lookup] + """ + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_discussion_panel") + self._render_thinking_indicator() + + if self.is_viewing_prior_session: + self._render_prior_session_view() + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_discussion_panel") + return + + self._render_discussion_selector() + + if not self.is_viewing_prior_session: + imgui.separator(); self._render_discussion_entry_controls() + imgui.separator(); self._render_discussion_roles() + imgui.separator(); self._render_discussion_entries() + + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_discussion_panel") + + def _render_discussion_roles(self) -> None: + if imgui.collapsing_header("Roles"): + with imscope.child("roles_scroll", size_y=100, flags=True): + for i, r in enumerate(list(self.disc_roles)): + with imscope.id(f"role_{i}"): + if imgui.button("X"): self.disc_roles.pop(i); break + imgui.same_line(); imgui.text(r) + ch, self.ui_disc_new_role_input = imgui.input_text("##new_role", self.ui_disc_new_role_input); imgui.same_line() + if imgui.button("Add"): + r = self.ui_disc_new_role_input.strip() + if r and r not in self.disc_roles: self.disc_roles.append(r); self.ui_disc_new_role_input = "" + + def _render_discussion_selector(self) -> None: + if not imgui.collapsing_header("Discussions", imgui.TreeNodeFlags_.default_open): return + names = self._get_discussion_names(); grouped = {} + for name in names: + base = name.split("_take_")[0]; grouped.setdefault(base, []).append(name) + active_base = self.active_discussion.split("_take_")[0] + if active_base not in grouped: active_base = names[0] if names else "" + base_names = sorted(grouped.keys()) + if imgui.begin_combo("##disc_sel", active_base): + for bname in base_names: + is_selected = (bname == active_base) + if imgui.selectable(bname, is_selected)[0]: + target = bname if bname in names else grouped[bname][0] + if target != self.active_discussion: self._switch_discussion(target) + if is_selected: imgui.set_item_default_focus() + imgui.end_combo() + active_base = self.active_discussion.split("_take_")[0]; current_takes = grouped.get(active_base, []) + if imgui.begin_tab_bar("discussion_takes_tabs"): + for take_name in current_takes: + label = "Original" if take_name == active_base else take_name.replace(f"{active_base}_", "").replace("_", " ").title() + flags = imgui.TabItemFlags_.set_selected if take_name == self.active_discussion else 0 + with imscope.tab_item(f"{label}###{take_name}", flags) as (exp, _): + if exp and take_name != self.active_discussion: self._switch_discussion(take_name) + with imscope.tab_item("Synthesis###Synthesis") as (exp, _): + if exp: self._render_synthesis_panel() + imgui.end_tab_bar() + if "_take_" in self.active_discussion: + if imgui.button("Promote Take"): + base_name = self.active_discussion.split("_take_")[0]; new_name = f"{base_name}_promoted"; counter = 1 + while new_name in names: new_name = f"{base_name}_promoted_{counter}"; counter += 1 + project_manager.promote_take(self.project, self.active_discussion, new_name); self._switch_discussion(new_name) + imgui.same_line() + if self.active_track: + imgui.same_line(); ch, self._track_discussion_active = imgui.checkbox("Track Discussion", self._track_discussion_active) + if ch: + if self._track_discussion_active: + self._flush_disc_entries_to_project() + history_strings = project_manager.load_track_history(self.active_track.id, self.active_project_root) + with self._disc_entries_lock: self.disc_entries = models.parse_history_entries(history_strings, self.disc_roles) + self.ai_status = f"track discussion: {self.active_track.id}" + else: self._flush_disc_entries_to_project(); self._switch_discussion(self.active_discussion); self.ai_status = "track discussion disabled" + self._render_discussion_metadata() + + def _render_discussion_tab(self) -> None: + imgui.begin_child("HistoryChild", size=(0, -self.ui_discussion_split_h)) + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_discussion_panel") + self._render_discussion_panel() + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_discussion_panel") + imgui.end_child() + imgui.button("###discussion_splitter", imgui.ImVec2(-1, 4)) + if imgui.is_item_active(): self.ui_discussion_split_h = max(150.0, min(imgui.get_window_height() - 150.0, self.ui_discussion_split_h - imgui.get_io().mouse_delta.y)) + imgui.push_style_var(imgui.StyleVar_.item_spacing, imgui.ImVec2(10, 4)) + ch1, self.ui_separate_message_panel = imgui.checkbox("Pop Out Message", self.ui_separate_message_panel); imgui.same_line() + ch2, self.ui_separate_response_panel = imgui.checkbox("Pop Out Response", self.ui_separate_response_panel) + if ch1: self.show_windows["Message"] = self.ui_separate_message_panel + if ch2: self.show_windows["Response"] = self.ui_separate_response_panel + imgui.pop_style_var() + show_message_tab = not self.ui_separate_message_panel + show_response_tab = not self.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 self._autofocus_response_tab: + tab_flags = imgui.TabItemFlags_.set_selected + self._autofocus_response_tab = False + self.controller._autofocus_response_tab = False + if show_message_tab: + if imgui.begin_tab_item("Message", None)[0]: + self._render_message_panel() + imgui.end_tab_item() + if show_response_tab: + if imgui.begin_tab_item("Response", None, tab_flags)[0]: + self._render_response_panel() + imgui.end_tab_item() + imgui.end_tab_bar() + else: + imgui.text_disabled("Message & Response panels are detached.") +#endregion: Discussions + + def _render_operations_hub(self) -> None: + imgui.push_style_var(imgui.StyleVar_.item_spacing, imgui.ImVec2(10, 4)) + ch1, self.ui_separate_tool_calls_panel = imgui.checkbox("Pop Out Tool Calls", self.ui_separate_tool_calls_panel) + if ch1: self.show_windows["Tool Calls"] = self.ui_separate_tool_calls_panel + imgui.same_line() + ch2, self.ui_separate_usage_analytics = imgui.checkbox("Pop Out Usage Analytics", self.ui_separate_usage_analytics) + if ch2: self.show_windows["Usage Analytics"] = self.ui_separate_usage_analytics + imgui.same_line() + ch3, self.ui_separate_external_tools = imgui.checkbox('Pop Out External Tools', self.ui_separate_external_tools) + if ch3: self.show_windows['External Tools'] = self.ui_separate_external_tools + imgui.pop_style_var() + show_tc_tab, show_usage_tab = not self.ui_separate_tool_calls_panel, not self.ui_separate_usage_analytics + with imscope.tab_bar("ops_tabs"): + with imscope.tab_item("Comms History") as (exp, _): + if exp: self._render_comms_history_panel() + if show_tc_tab: + with imscope.tab_item("Tool Calls") as (exp, _): + if exp: self._render_tool_calls_panel() + if show_usage_tab: + with imscope.tab_item("Usage Analytics") as (exp, _): + if exp: self._render_usage_analytics_panel() + if not self.ui_separate_external_tools: + with imscope.tab_item("External Tools") as (exp, _): + if exp: + self._render_external_tools_panel() + imgui.separator(); imgui.text("") + try: self._render_external_editor_panel() + except Exception as e: imgui.text_colored(vec4(1, 0.3, 0.3, 1), f"Error: {str(e)}") + with imscope.tab_item("Workspace Layouts") as (exp, _): + if exp: + imgui.text("Experimental: Auto-switch layout by Tier") + ch, self.controller.ui_auto_switch_layout = imgui.checkbox("Enable Auto-Switch", self.controller.ui_auto_switch_layout) + if self.controller.ui_auto_switch_layout: + imgui.separator(); imgui.text("Tier Bindings (select profile for each tier)") + profiles = [""] + [p.name for p in self.controller.workspace_profiles.values()] + for t in ["Tier 1", "Tier 2", "Tier 3", "Tier 4"]: + curr = self.controller.ui_tier_layout_bindings.get(t, ""); idx = profiles.index(curr) if curr in profiles else 0 + ch_combo, new_idx = imgui.combo(t, idx, profiles) + if ch_combo: self.controller.ui_tier_layout_bindings[t] = profiles[new_idx] + + def _render_prior_session_view(self) -> None: + with imscope.style_color(imgui.Col_.child_bg, vec4(50, 40, 20)): + if imgui.button("Exit Prior Session"): self.controller.cb_exit_prior_session(); self._comms_log_dirty = True + imgui.separator() + with imscope.child("prior_scroll"): + clipper = imgui.ListClipper(); clipper.begin(len(self.prior_disc_entries)) + while clipper.step(): + for idx in range(clipper.display_start, clipper.display_end): + entry = self.prior_disc_entries[idx]; + with imscope.id(f"prior_disc_{idx}"): + collapsed = entry.get("collapsed", False) + if imgui.button("+" if collapsed else "-"): entry["collapsed"] = not collapsed + imgui.same_line(); role, ts = entry.get("role", "??"), entry.get("ts", "") + imgui.text_colored(C_LBL, f"[{role}]") + if ts: imgui.same_line(); imgui.text_colored(vec4(160, 160, 160), str(ts)) + content = entry.get("content", "") + if collapsed: + imgui.same_line(); preview = content.replace("\n", " ")[:80] + if len(content) > 80: preview += "..." + imgui.text_colored(vec4(180, 180, 180), preview) + else: + with theme.ai_text_style(): + markdown_helper.render(content, context_id=f'prior_disc_{idx}') + imgui.separator() + + def _render_project_settings_hub(self) -> None: + with imscope.tab_bar('context_hub_tabs'): + with imscope.tab_item('Projects') as (exp, _): + if exp: self._render_projects_panel() + with imscope.tab_item('Paths') as (exp, _): + if exp: self._render_paths_panel() + + def _render_thinking_indicator(self) -> None: + is_thinking = self.ai_status in ['sending...', 'streaming...', 'running powershell...'] + if is_thinking: + val = math.sin(time.time() * 10 * math.pi) + alpha = 1.0 if val > 0 else 0.0 + c = vec4(255, 50, 50, alpha) if theme.is_nerv_active() else vec4(255, 100, 100, alpha) + imgui.text_colored(c, "THINKING..."); imgui.same_line() + +#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] + """ + if src_idx == dst_idx: return + new_tickets = list(self.active_tickets) + ticket = new_tickets.pop(src_idx) + new_tickets.insert(dst_idx, ticket) + # Validate dependencies: a ticket cannot be placed before any of its dependencies + id_to_idx = {str(t.get('id', '')): i for i, t in enumerate(new_tickets)} + valid = True + for i, t in enumerate(new_tickets): + deps = t.get('depends_on', []) + for d_id in deps: + if d_id in id_to_idx and id_to_idx[d_id] >= i: + valid = False + break + if not valid: break + if valid: + self.active_tickets = new_tickets + self._push_mma_state_update() + + def _render_ticket_queue(self) -> None: + """ + [C: tests/test_gui_kill_button.py:test_render_ticket_queue_table_columns] + """ + imgui.text("Ticket Queue Management") + if not self.active_track: + imgui.text_disabled("No active track.") + return + + # Select All / None + if imgui.button("Select All"): self.ui_selected_tickets = {str(t.get('id', '')) for t in self.active_tickets} + imgui.same_line() + if imgui.button("Select None"): self.ui_selected_tickets.clear() + + imgui.same_line(); imgui.spacing(); imgui.same_line() + + # Bulk Actions + if imgui.button("Bulk Execute"): self.bulk_execute() + imgui.same_line() + if imgui.button("Bulk Skip"): self.bulk_skip() + imgui.same_line() + if imgui.button("Bulk Block"): self.bulk_block() + # Table + flags = imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable | imgui.TableFlags_.scroll_y + if imgui.begin_table("ticket_queue_table", 7, flags, imgui.ImVec2(0, 300)): + imgui.table_setup_column("Select", imgui.TableColumnFlags_.width_fixed, 40) + imgui.table_setup_column("ID", imgui.TableColumnFlags_.width_fixed, 80) + imgui.table_setup_column("Priority", imgui.TableColumnFlags_.width_fixed, 100) + imgui.table_setup_column("Model", imgui.TableColumnFlags_.width_fixed, 150) + imgui.table_setup_column("Status", imgui.TableColumnFlags_.width_fixed, 100) + imgui.table_setup_column("Description", imgui.TableColumnFlags_.width_stretch) + imgui.table_setup_column("Actions", imgui.TableColumnFlags_.width_fixed, 80) + imgui.table_headers_row() + + for i, t in enumerate(self.active_tickets): + tid = str(t.get('id', '')) + imgui.table_next_row() + + # Select + imgui.table_next_column() + is_sel = tid in self.ui_selected_tickets + changed, is_sel = imgui.checkbox(f"##sel_{tid}", is_sel) + if changed: + if is_sel: self.ui_selected_tickets.add(tid) + else: self.ui_selected_tickets.discard(tid) + + # ID + imgui.table_next_column() + is_selected = (tid == self.ui_selected_ticket_id) + opened, _ = imgui.selectable(f"{tid}##drag_{tid}", is_selected) + if opened: self.ui_selected_ticket_id = tid + + if imgui.begin_drag_drop_source(): + imgui.set_drag_drop_payload("TICKET_REORDER", i) + imgui.text(f"Moving {tid}") + imgui.end_drag_drop_source() + + if imgui.begin_drag_drop_target(): + payload = imgui.accept_drag_drop_payload("TICKET_REORDER") + if payload: + src_idx = int(payload.data) + self._reorder_ticket(src_idx, i) + imgui.end_drag_drop_target() + + # Priority + + imgui.table_next_column() + prio = t.get('priority', 'medium') + p_col = vec4(180, 180, 180) # gray + if prio == 'high': _col = vec4(255, 100, 100) # red + elif prio == 'medium': p_col = vec4(255, 255, 100) # yellow + + imgui.push_style_color(imgui.Col_.text, p_col) + if imgui.begin_combo(f"##prio_{tid}", prio, imgui.ComboFlags_.height_small): + for p_opt in ['high', 'medium', 'low']: + if imgui.selectable(p_opt, p_opt == prio)[0]: + t['priority'] = p_opt + self._push_mma_state_update() + imgui.end_combo() + imgui.pop_style_color() + + # Model + imgui.table_next_column() + model_override = t.get('model_override') + current_model = model_override if model_override else "Default" + if imgui.begin_combo(f"##model_{tid}", current_model, imgui.ComboFlags_.height_small): + if imgui.selectable("Default", model_override is None)[0]: + t['model_override'] = None; self._push_mma_state_update() + for model in ["gemini-2.5-flash-lite", "gemini-2.5-flash", "gemini-3-flash-preview", "gemini-3.1-pro-preview", "deepseek-v3"]: + if imgui.selectable(model, model_override == model)[0]: + t['model_override'] = model; self._push_mma_state_update() + imgui.end_combo() + + # Status + imgui.table_next_column() + status = t.get('status', 'todo') + if t.get('model_override'): imgui.text_colored(imgui.ImVec4(1, 0.5, 0, 1), f"{status} [{t.get('model_override')}]") + else: imgui.text(t.get('status', 'todo')) + + # Description + imgui.table_next_column() + imgui.text(t.get('description', '')) + + # Actions - Kill button for in_progress tickets + imgui.table_next_column() + status = t.get('status', 'todo') + if status == 'in_progress': + if imgui.button(f"Kill##{tid}"): self._cb_kill_ticket(tid) + elif status == 'todo': + if imgui.button(f"Block##{tid}"): self._cb_block_ticket(tid) + elif status == 'blocked' and t.get('manual_block', False): + if imgui.button(f"Unblock##{tid}"): self._cb_unblock_ticket(tid) + + imgui.end_table() + + def _render_task_dag_panel(self) -> None: # 4. Task DAG Visualizer + imgui.text("Task DAG") + if (self.active_track or self.active_tickets) and self.node_editor_ctx: + ed.set_current_editor(self.node_editor_ctx) + ed.begin('Visual DAG') + # Selection detection + selected = ed.get_selected_nodes() + if selected: + for node_id in selected: + node_val = node_id.id() + for t in self.active_tickets: + if abs(hash(str(t.get('id', '')))) == node_val: + self.ui_selected_ticket_id = str(t.get('id', '')) + break + break + for t in self.active_tickets: + tid = str(t.get('id', '??')) + int_id = abs(hash(tid)) + ed.begin_node(ed.NodeId(int_id)) + if getattr(self, "ui_project_execution_mode", "native") == "beads": + imgui.text_colored(imgui.ImVec4(0, 1, 1, 1), "[B] ") + imgui.same_line() + imgui.text_colored(C_KEY, f"Ticket: {tid}") + status = t.get('status', 'todo') + s_col = C_VAL + if status == 'done' or status == 'complete': s_col = C_IN + elif status == 'in_progress' or status == 'running': s_col = C_OUT + elif status == 'error': s_col = imgui.ImVec4(1, 0.4, 0.4, 1) + imgui.text("Status: ") + imgui.same_line() + imgui.text_colored(s_col, status) + imgui.text(f"Target: {t.get('target_file','')}") + ed.begin_pin(ed.PinId(abs(hash(tid + "_in"))), ed.PinKind.input) + imgui.text("->") + ed.end_pin() + imgui.same_line() + ed.begin_pin(ed.PinId(abs(hash(tid + "_out"))), ed.PinKind.output) + imgui.text("->") + ed.end_pin() + ed.end_node() + for t in self.active_tickets: + tid = str(t.get('id', '??')) + for dep in t.get('depends_on', []): + ed.link(ed.LinkId(abs(hash(dep + "_" + tid))), ed.PinId(abs(hash(dep + "_out"))), ed.PinId(abs(hash(tid + "_in")))) + + # Handle link creation + if ed.begin_create(): + start_pin = ed.PinId() + end_pin = ed.PinId() + if ed.query_new_link(start_pin, end_pin): + if ed.accept_new_item(): + s_id = start_pin.id() + e_id = end_pin.id() + source_tid = None + target_tid = None + for t in self.active_tickets: + tid = str(t.get('id', '')) + if abs(hash(tid + "_out")) == s_id: source_tid = tid + if abs(hash(tid + "_out")) == e_id: source_tid = tid + if abs(hash(tid + "_in")) == s_id: target_tid = tid + if abs(hash(tid + "_in")) == e_id: target_tid = tid + if source_tid and target_tid and source_tid != target_tid: + for t in self.active_tickets: + if str(t.get('id', '')) == target_tid: + if source_tid not in t.get('depends_on', []): + t.setdefault('depends_on', []).append(source_tid) + self._push_mma_state_update() + break + ed.end_create() + + # Handle link deletion + if ed.begin_delete(): + link_id = ed.LinkId() + while ed.query_deleted_link(link_id): + if ed.accept_deleted_item(): + lid_val = link_id.id() + for t in self.active_tickets: + tid = str(t.get('id', '')) + deps = t.get('depends_on', []) + if any(abs(hash(d + "_" + tid)) == lid_val for d in deps): + t['depends_on'] = [dep for dep in deps if abs(hash(dep + "_" + tid)) != lid_val] + self._push_mma_state_update() + break + ed.end_delete() + # Validate DAG after any changes + try: + from src.dag_engine import TrackDAG + ticket_dicts = [{'id': str(t.get('id', '')), 'depends_on': t.get('depends_on', [])} for t in self.active_tickets] + temp_dag = TrackDAG(ticket_dicts) + if temp_dag.has_cycle(): + imgui.open_popup("Cycle Detected!") + except Exception: + pass + ed.end() + # 5. Add Ticket Form + imgui.separator() + if imgui.button("Add Ticket"): + self._show_add_ticket_form = not self._show_add_ticket_form + if self._show_add_ticket_form: + # Default Ticket ID + max_id = 0 + for t in self.active_tickets: + tid = t.get('id', '') + if tid.startswith('T-'): + try: max_id = max(max_id, int(tid[2:])) + except: pass + self.ui_new_ticket_id = f"T-{max_id + 1:03d}" + self.ui_new_ticket_desc = "" + self.ui_new_ticket_target = "" + self.ui_new_ticket_deps = "" + if self._show_add_ticket_form: + imgui.begin_child("add_ticket_form", imgui.ImVec2(-1, 220), True) + imgui.text_colored(C_VAL, "New Ticket Details") + _, self.ui_new_ticket_id = imgui.input_text("ID##new_ticket", self.ui_new_ticket_id) + _, self.ui_new_ticket_desc = imgui.input_text_multiline("Description##new_ticket", self.ui_new_ticket_desc, imgui.ImVec2(-1, 60)) + _, self.ui_new_ticket_target = imgui.input_text("Target File##new_ticket", self.ui_new_ticket_target) + _, self.ui_new_ticket_deps = imgui.input_text("Depends On (IDs, comma-separated)##new_ticket", self.ui_new_ticket_deps) + imgui.text("Priority:") + imgui.same_line() + if imgui.begin_combo("##new_prio", self.ui_new_ticket_priority): + for p_opt in ['high', 'medium', 'low']: + if imgui.selectable(p_opt, p_opt == self.ui_new_ticket_priority)[0]: + self.ui_new_ticket_priority = p_opt + imgui.end_combo() + if imgui.button("Create"): + new_ticket = { + "id": self.ui_new_ticket_id, + "description": self.ui_new_ticket_desc, + "status": "todo", + "priority": self.ui_new_ticket_priority, + "assigned_to": "tier3-worker", + "target_file": self.ui_new_ticket_target, + "depends_on": [d.strip() for d in self.ui_new_ticket_deps.split(",") if d.strip()] + } + self.active_tickets.append(new_ticket) + self._show_add_ticket_form = False + self._push_mma_state_update() + imgui.same_line() + if imgui.button("Cancel"): + self._show_add_ticket_form = False + imgui.end_child() + else: + imgui.text_disabled("No active MMA track or tickets.") + + def _render_beads_tab(self) -> None: + imgui.text("Beads Graph (Dolt-backed)") + if imgui.button("Refresh Beads"): + pass + imgui.separator() + + # Check for dolt/bd dependencies + dolt_path = shutil.which("dolt") + bd_path = shutil.which("bd") + if not dolt_path or not bd_path: + missing = [] + if not dolt_path: missing.append("'dolt'") + if not bd_path: missing.append("'bd'") + imgui.text_colored(imgui.ImVec4(1, 0.5, 0, 1), f"Warning: {', '.join(missing)} not found in PATH.") + imgui.text_wrapped("Beads mode requires Dolt and the Beads (bd) CLI tools.") + + if getattr(self, "ui_project_execution_mode", "native") == "beads": + try: + from src import beads_client + bclient = beads_client.BeadsClient(Path(self.active_project_root)) + beads = bclient.list_beads() + if not beads: + imgui.text_disabled("No beads found.") + else: + if imgui.begin_table("beads_table", 3, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable): + imgui.table_setup_column("ID") + imgui.table_setup_column("Status") + imgui.table_setup_column("Title") + imgui.table_headers_row() + for b in beads: + imgui.table_next_row() + imgui.table_next_column() + imgui.text(str(b.id)) + imgui.table_next_column() + imgui.text(str(b.status)) + imgui.table_next_column() + imgui.text(str(b.title)) + imgui.end_table() + except Exception as e: + imgui.text_colored(imgui.ImVec4(1, 0, 0, 1), f"Error loading beads: {e}") + + def _render_mma_dashboard(self) -> None: + """Main MMA dashboard interface.""" + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_mma_dashboard") + self._render_mma_focus_selector() + imgui.separator() + if self.is_viewing_prior_session: + c = vec4(255, 152, 48) if theme.is_nerv_active() else vec4(255, 200, 100) + imgui.text_colored(c, "HISTORICAL VIEW - READ ONLY") + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_mma_dashboard") + return + self._render_mma_track_summary() + imgui.separator() + self._render_mma_epic_planner() + imgui.separator() + if imgui.collapsing_header("Conductor Setup"): self._render_mma_conductor_setup() + imgui.separator() + self._render_mma_track_browser() + imgui.separator() + self._render_mma_global_controls() + imgui.separator() + self._render_mma_usage_section() + imgui.separator() + self._render_ticket_queue() + imgui.separator() + self._render_window_if_open("Task DAG", self._render_task_dag_panel, not self.ui_separate_task_dag) + if self.ui_selected_ticket_id: self._render_mma_ticket_editor() + imgui.separator() + self._render_mma_agent_streams() + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_mma_dashboard") + + def _render_mma_focus_selector(self) -> None: + imgui.text("Focus Agent:"); imgui.same_line() + focus_label = self.ui_focus_agent or "All" + if imgui.begin_combo("##focus_agent", focus_label, imgui.ComboFlags_.width_fit_preview): + if imgui.selectable("All", self.ui_focus_agent is None)[0]: self.ui_focus_agent = None + for tier in ["Tier 2", "Tier 3", "Tier 4"]: + if imgui.selectable(tier, self.ui_focus_agent == tier)[0]: self.ui_focus_agent = tier + imgui.end_combo() + imgui.same_line() + if self.ui_focus_agent and imgui.button("x##clear_focus"): self.ui_focus_agent = None + + def _render_mma_modals(self) -> None: + """Renders all MMA-specific approval and info modals.""" + is_nerv = theme.is_nerv_active() + # Tool Execution Approval + if self._pending_ask_dialog: + if not self._ask_dialog_open: + imgui.open_popup("Approve Tool Execution") + self._ask_dialog_open = True + else: + self._ask_dialog_open = False + if imgui.begin_popup_modal("Approve Tool Execution", None, imgui.WindowFlags_.always_auto_resize)[0]: + if not self._pending_ask_dialog or self._ask_tool_data is None: imgui.close_current_popup() + else: + tool_name = self._ask_tool_data.get("tool", "unknown"); tool_args = self._ask_tool_data.get("args", {}) + imgui.text("The AI wants to execute a tool:"); imgui.text_colored(vec4(200, 200, 100), f"Tool: {tool_name}"); imgui.separator() + imgui.text("Arguments:"); imgui.begin_child("ask_args_child", imgui.ImVec2(400, 200), True); imgui.text_unformatted(json.dumps(tool_args, indent=2)); imgui.end_child() + imgui.separator() + if imgui.button("Approve", imgui.ImVec2(120, 0)): self._handle_approve_ask(); imgui.close_current_popup() + imgui.same_line() + if imgui.button("Deny", imgui.ImVec2(120, 0)): self._handle_reject_ask(); imgui.close_current_popup() + imgui.end_popup() + # MMA Step Approval + if self._pending_mma_approvals: + if not self._mma_approval_open: + imgui.open_popup("MMA Step Approval") + self._mma_approval_open, self._mma_approval_edit_mode = True, False + self._mma_approval_payload = self._pending_mma_approvals[0].get("payload", "") + else: self._mma_approval_open = False + if imgui.begin_popup_modal("MMA Step Approval", None, imgui.WindowFlags_.always_auto_resize)[0]: + if not self._pending_mma_approvals: imgui.close_current_popup() + else: + ticket_id = self._pending_mma_approvals[0].get("ticket_id", "??") + imgui.text(f"Ticket {ticket_id} is waiting for tool execution approval."); imgui.separator() + if self._mma_approval_edit_mode: + imgui.text("Edit Raw Payload (Manual Memory Mutation):"); _, self._mma_approval_payload = imgui.input_text_multiline("##mma_payload", self._mma_approval_payload, imgui.ImVec2(600, 400)) + else: + imgui.text("Proposed Tool Call:"); imgui.begin_child("mma_preview", imgui.ImVec2(600, 300), True); imgui.text_unformatted(str(self._pending_mma_approvals[0].get("payload", ""))); imgui.end_child() + imgui.separator() + if imgui.button("Approve", imgui.ImVec2(120, 0)): self._handle_mma_respond(approved=True, payload=self._mma_approval_payload); imgui.close_current_popup() + imgui.same_line() + if imgui.button("Edit Payload" if not self._mma_approval_edit_mode else "Show Original", imgui.ImVec2(120, 0)): self._mma_approval_edit_mode = not self._mma_approval_edit_mode + imgui.same_line() + if imgui.button("Abort Ticket", imgui.ImVec2(120, 0)): self._handle_mma_respond(approved=False); imgui.close_current_popup() + imgui.end_popup() + # MMA Spawn Approval + if self._pending_mma_spawns: + if not self._mma_spawn_open: + imgui.open_popup("MMA Spawn Approval") + self._mma_spawn_open, self._mma_spawn_edit_mode = True, False + self._mma_spawn_prompt, self._mma_spawn_context = self._pending_mma_spawns[0].get("prompt", ""), self._pending_mma_spawns[0].get("context_md", "") + else: self._mma_spawn_open = False + if imgui.begin_popup_modal("MMA Spawn Approval", None, imgui.WindowFlags_.always_auto_resize)[0]: + if not self._pending_mma_spawns: imgui.close_current_popup() + else: + role, ticket_id = self._pending_mma_spawns[0].get("role", "??"), self._pending_mma_spawns[0].get("ticket_id", "??") + imgui.text(f"Spawning {role} for Ticket {ticket_id}"); imgui.separator() + if self._mma_spawn_edit_mode: + imgui.text("Edit Prompt:"); _, self._mma_spawn_prompt = imgui.input_text_multiline("##spawn_prompt", self._mma_spawn_prompt, imgui.ImVec2(800, 200)) + imgui.text("Edit Context MD:"); _, self._mma_spawn_context = imgui.input_text_multiline("##spawn_context", self._mma_spawn_context, imgui.ImVec2(800, 300)) + else: + imgui.text("Proposed Prompt:"); imgui.begin_child("spawn_prompt_preview", imgui.ImVec2(800, 150), True); imgui.text_unformatted(self._mma_spawn_prompt); imgui.end_child() + imgui.text("Proposed Context MD:"); imgui.begin_child("spawn_context_preview", imgui.ImVec2(800, 250), True); imgui.text_unformatted(self._mma_spawn_context); imgui.end_child() + imgui.separator() + if imgui.button("Approve", imgui.ImVec2(120, 0)): self._handle_mma_respond(approved=True, prompt=self._mma_spawn_prompt, context_md=self._mma_spawn_context); imgui.close_current_popup() + imgui.same_line() + if imgui.button("Edit Mode" if not self._mma_spawn_edit_mode else "Preview Mode", imgui.ImVec2(120, 0)): self._mma_spawn_edit_mode = not self._mma_spawn_edit_mode + imgui.same_line() + if imgui.button("Abort", imgui.ImVec2(120, 0)): self._handle_mma_respond(approved=False, abort=True); imgui.close_current_popup() + imgui.end_popup() + # Cycle Detection + if imgui.begin_popup_modal("Cycle Detected!", None, imgui.WindowFlags_.always_auto_resize)[0]: + imgui.text_colored(imgui.ImVec4(1, 0.3, 0.3, 1), "The dependency graph contains a cycle!") + imgui.text("Please remove the circular dependency.") + if imgui.button("OK"): + imgui.close_current_popup() + imgui.end_popup() + + def _render_mma_track_summary(self) -> None: + is_nerv = theme.is_nerv_active() + track_name = self.active_track.description if self.active_track else "None" + if getattr(self, "ui_project_execution_mode", "native") == "beads": track_name = "Beads Graph" + track_stats = project_manager.calculate_track_progress(self.active_track.tickets if self.active_track else self.active_tickets) + total_cost = sum(cost_tracker.estimate_cost(u.get('model','unknown'), u.get('input',0), u.get('output',0)) for u in self.mma_tier_usage.values()) + imgui.text("Track:"); imgui.same_line(); imgui.text_colored(C_VAL, track_name); imgui.same_line(); imgui.text(" | Status:"); imgui.same_line() + if self.mma_status == "paused": + imgui.text_colored(vec4(255, 152, 48) if is_nerv else imgui.ImVec4(1, 0.5, 0, 1), "PIPELINE PAUSED"); imgui.same_line() + status_col = imgui.ImVec4(1, 1, 1, 1) + if self.mma_status == "idle": status_col = imgui.ImVec4(0.7, 0.7, 0.7, 1) + elif self.mma_status == "running": status_col = vec4(80, 255, 80) if is_nerv else imgui.ImVec4(1, 1, 0, 1) + elif self.mma_status == "done": status_col = imgui.ImVec4(0, 1, 0, 1) + elif self.mma_status == "error": status_col = vec4(255, 72, 64) if is_nerv else imgui.ImVec4(1, 0, 0, 1) + elif self.mma_status == "paused": status_col = imgui.ImVec4(1, 0.5, 0, 1) + imgui.text_colored(status_col, self.mma_status.upper()); imgui.same_line(); imgui.text(" | Cost:"); imgui.same_line(); imgui.text_colored(imgui.ImVec4(0, 1, 0, 1), f"${total_cost:,.4f}") + perc = track_stats["percentage"] / 100.0 + p_color = imgui.ImVec4(1, 0, 0, 1) if track_stats["percentage"] < 33 else (imgui.ImVec4(1, 1, 0, 1) if track_stats["percentage"] < 66 else imgui.ImVec4(0, 1, 0, 1)) + imgui.push_style_color(imgui.Col_.plot_histogram, p_color); imgui.progress_bar(perc, imgui.ImVec2(-1, 0), f"{track_stats['percentage']:.1f}%"); imgui.pop_style_color() + if imgui.begin_table("ticket_stats_breakdown", 4): + for lbl, val in [("Completed:", track_stats["completed"]), ("In Progress:", track_stats["in_progress"]), ("Blocked:", track_stats["blocked"]), ("Todo:", track_stats["todo"])]: + imgui.table_next_column(); imgui.text_colored(C_LBL, lbl); imgui.same_line(); imgui.text_colored(C_VAL, str(val)) + imgui.end_table() + if self.active_track: + remaining = track_stats["total"] - track_stats["completed"] + eta_mins = (self._avg_ticket_time * remaining) / 60.0 + imgui.text_colored(C_LBL, "ETA:"); imgui.same_line(); imgui.text_colored(C_VAL, f"~{int(eta_mins)}m ({remaining} tickets remaining)") + + def _render_mma_epic_planner(self) -> None: + imgui.text_colored(C_LBL, 'Epic Planning (Tier 1)') + _, self.ui_epic_input = imgui.input_text_multiline('##epic_input', self.ui_epic_input, imgui.ImVec2(-1, 80)) + if imgui.button('Plan Epic (Tier 1)', imgui.ImVec2(-1, 0)): self._cb_plan_epic() + + def _render_mma_conductor_setup(self) -> None: + if imgui.button("Run Setup Scan"): self._cb_run_conductor_setup() + if self.ui_conductor_setup_summary: imgui.input_text_multiline("##setup_summary", self.ui_conductor_setup_summary, imgui.ImVec2(-1, 120), imgui.InputTextFlags_.read_only) + + def _render_mma_track_browser(self) -> None: + imgui.text("Track Browser") + if imgui.begin_table("mma_tracks_table", 4, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable): + imgui.table_setup_column("Title"); imgui.table_setup_column("Status"); imgui.table_setup_column("Progress"); imgui.table_setup_column("Actions"); imgui.table_headers_row() + for track in self.tracks: + imgui.table_next_row(); imgui.table_next_column(); imgui.text(track.get("title", "Untitled")); imgui.table_next_column() + status = track.get("status", "unknown").lower() + c = imgui.ImVec4(0.7, 0.7, 0.7, 1) if status == "new" else (vec4(80, 255, 80) if status == "active" and theme.is_nerv_active() else (imgui.ImVec4(1, 1, 0, 1) if status == "active" else (imgui.ImVec4(0, 1, 0, 1) if status == "done" else (imgui.ImVec4(1, 0, 0, 1) if status == "blocked" else imgui.ImVec4(1, 1, 1, 1))))) + imgui.text_colored(c, status.upper()); imgui.table_next_column() + prog = track.get("progress", 0.0) + p_c = imgui.ImVec4(1, 0, 0, 1) if prog < 0.33 else (imgui.ImVec4(1, 1, 0, 1) if prog < 0.66 else imgui.ImVec4(0, 1, 0, 1)) + imgui.push_style_color(imgui.Col_.plot_histogram, p_c); imgui.progress_bar(prog, imgui.ImVec2(-1, 0), f"{int(prog*100)}%"); imgui.pop_style_color(); imgui.table_next_column() + if imgui.button(f"Load##{track.get('id')}"): self._cb_load_track(str(track.get("id") or "")) + imgui.end_table() + imgui.text("Create New Track") + _, self.ui_new_track_name = imgui.input_text("Name##new_track", self.ui_new_track_name) + _, self.ui_new_track_desc = imgui.input_text_multiline("Description##new_track", self.ui_new_track_desc, imgui.ImVec2(-1, 60)) + imgui.text("Type:"); imgui.same_line() + if imgui.begin_combo("##track_type", self.ui_new_track_type): + for ttype in ["feature", "chore", "fix"]: + if imgui.selectable(ttype, self.ui_new_track_type == ttype)[0]: self.ui_new_track_type = ttype + imgui.end_combo() + if imgui.button("Create Track"): + self._cb_create_track(self.ui_new_track_name, self.ui_new_track_desc, self.ui_new_track_type) + self.ui_new_track_name = ""; self.ui_new_track_desc = "" + + def _render_mma_global_controls(self) -> None: + changed, self.mma_step_mode = imgui.checkbox("Step Mode (HITL)", self.mma_step_mode) + imgui.same_line(); imgui.text(f"Status: {self.mma_status.upper()}") + if self.controller and hasattr(self.controller, 'engine') and self.controller.engine and hasattr(self.controller.engine, '_pause_event'): + imgui.same_line() + is_paused = self.controller.engine._pause_event.is_set() + if imgui.button("Resume" if is_paused else "Pause"): + if is_paused: self.controller.engine.resume() + else: self.controller.engine.pause() + if self.active_tier: + imgui.same_line(); imgui.text_colored(C_VAL, f"| Active: {self.active_tier}") + any_pending = len(self._pending_mma_spawns) > 0 or len(self._pending_mma_approvals) > 0 or self._pending_ask_dialog + if any_pending: + alpha = abs(math.sin(time.time() * 5)) + c = vec4(255, 72, 64, alpha) if theme.is_nerv_active() else imgui.ImVec4(1, 0.3, 0.3, alpha) + imgui.same_line(); imgui.text_colored(c, " APPROVAL PENDING"); imgui.same_line() + if imgui.button("Go to Approval"): pass + + def _render_mma_usage_section(self) -> None: + imgui.text("Tier Usage (Tokens & Cost)") + if imgui.begin_table("mma_usage", 5, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg): + imgui.table_setup_column("Tier"); imgui.table_setup_column("Model"); imgui.table_setup_column("Input"); imgui.table_setup_column("Output"); imgui.table_setup_column("Est. Cost"); imgui.table_headers_row() + total_cost = 0.0 + for tier, stats in self.mma_tier_usage.items(): + imgui.table_next_row(); imgui.table_next_column(); imgui.text(tier); imgui.table_next_column(); model = stats.get('model', 'unknown'); imgui.text(model); imgui.table_next_column(); in_t = stats.get('input', 0); imgui.text(f"{in_t:,}"); imgui.table_next_column(); out_t = stats.get('output', 0); imgui.text(f"{out_t:,}"); imgui.table_next_column(); cost = cost_tracker.estimate_cost(model, in_t, out_t); total_cost += cost; imgui.text(f"${cost:,.4f}") + imgui.table_next_row(); imgui.table_set_bg_color(imgui.TableBgTarget_.row_bg0, imgui.get_color_u32(imgui.Col_.plot_lines_hovered)); imgui.table_next_column(); imgui.text("TOTAL"); imgui.table_next_column(); imgui.text(""); imgui.table_next_column(); imgui.text(""); imgui.table_next_column(); imgui.text(""); imgui.table_next_column(); imgui.text(f"${total_cost:,.4f}"); imgui.end_table() + if imgui.collapsing_header("Tier Model Config"): + for tier in self.mma_tier_usage.keys(): + imgui.text(f"{tier}:"); imgui.same_line(); curr_model, curr_prov = self.mma_tier_usage[tier].get("model", "unknown"), self.mma_tier_usage[tier].get("provider", "gemini") + with imscope.id(f"tier_cfg_{tier}"): + imgui.push_item_width(80) + if imgui.begin_combo("##prov", curr_prov): + for p in models.PROVIDERS: + if imgui.selectable(p, p == curr_prov)[0]: + self.mma_tier_usage[tier]["provider"] = p + models_list = self.controller.all_available_models.get(p, []) + if models_list: self.mma_tier_usage[tier]["model"] = models_list[0] + imgui.end_combo() + imgui.pop_item_width(); imgui.same_line(); imgui.push_item_width(150) + models_list = self.controller.all_available_models.get(curr_prov, []) + if imgui.begin_combo("##model", curr_model): + for m in models_list: + if imgui.selectable(m, curr_model == m)[0]: self.mma_tier_usage[tier]["model"] = m + imgui.end_combo() + imgui.pop_item_width(); imgui.same_line(); imgui.push_item_width(-1) + curr_preset = self.mma_tier_usage[tier].get("tool_preset") or "None" + p_names = ["None"] + sorted(self.controller.tool_presets.keys()) + if imgui.begin_combo("##preset", curr_preset): + for pn in p_names: + if imgui.selectable(pn, curr_preset == pn)[0]: self.mma_tier_usage[tier]["tool_preset"] = None if pn == "None" else pn + imgui.end_combo() + imgui.pop_item_width(); imgui.same_line(); imgui.push_item_width(150) + curr_pers = self.mma_tier_usage[tier].get("persona") or "None" + personas = getattr(self.controller, 'personas', {}) + pers_opts = ["None"] + sorted(personas.keys()) + if imgui.begin_combo("##persona", curr_pers): + for pern in pers_opts: + if imgui.selectable(pern, curr_pers == pern)[0]: self.mma_tier_usage[tier]["persona"] = None if pern == "None" else pern + imgui.end_combo() + imgui.pop_item_width() + + def _render_mma_ticket_editor(self) -> None: + imgui.separator(); imgui.text_colored(C_VAL, f"Editing: {self.ui_selected_ticket_id}") + ticket = next((t for t in self.active_tickets if str(t.get('id', '')) == self.ui_selected_ticket_id), None) + if ticket: + imgui.text(f"Status: {ticket.get('status', 'todo')}"); prio = ticket.get('priority', 'medium') + imgui.text("Priority:"); imgui.same_line() + if imgui.begin_combo(f"##edit_prio_{ticket.get('id')}", prio): + for p_opt in ['high', 'medium', 'low']: + if imgui.selectable(p_opt, p_opt == prio)[0]: ticket['priority'] = p_opt; self._push_mma_state_update() + imgui.end_combo() + imgui.text(f"Target: {ticket.get('target_file', '')}"); imgui.text(f"Depends on: {', '.join(ticket.get('depends_on', []))}") + personas = getattr(self.controller, 'personas', {}); curr_pers = ticket.get('persona_id', '') + imgui.text("Persona Override:"); imgui.same_line() + pers_opts = ["None"] + sorted(personas.keys()); curr_idx = pers_opts.index(curr_pers) + 1 if curr_pers in pers_opts else 0 + _, curr_idx = imgui.combo(f"##ticket_persona_{ticket.get('id')}", curr_idx, pers_opts) + ticket['persona_id'] = None if curr_idx == 0 or pers_opts[curr_idx] == "None" else pers_opts[curr_idx] + if imgui.button(f"Mark Complete##{self.ui_selected_ticket_id}"): ticket['status'] = 'done'; self._push_mma_state_update() + imgui.same_line() + if imgui.button(f"Delete##{self.ui_selected_ticket_id}"): self.active_tickets = [t for t in self.active_tickets if str(t.get('id', '')) != self.ui_selected_ticket_id]; self.ui_selected_ticket_id = None; self._push_mma_state_update() + + def _render_mma_agent_streams(self) -> None: + imgui.text("Agent Streams") + if imgui.begin_tab_bar("mma_streams_tabs"): + for tier, label, sep_flag_attr in [("Tier 1", "Tier 1", "ui_separate_tier1"), ("Tier 2", "Tier 2 (Tech Lead)", "ui_separate_tier2"), ("Tier 3", None, "ui_separate_tier3"), ("Tier 4", "Tier 4 (QA)", "ui_separate_tier4")]: + with imscope.tab_item(tier) as (exp, _): + if exp: + sep_val = getattr(self, sep_flag_attr); ch, new_val = imgui.checkbox(f"Pop Out {tier}", sep_val) + if ch: + setattr(self, sep_flag_attr, new_val) + self.show_windows[f"{tier}: Strategy" if tier == "Tier 1" else (f"{tier}: Tech Lead" if tier == "Tier 2" else (f"{tier}: Workers" if tier == "Tier 3" else f"{tier}: QA"))] = new_val + if not new_val: self._render_tier_stream_panel(tier, label) + else: imgui.text_disabled(f"{tier} stream is detached.") + if getattr(self, "ui_project_execution_mode", "native") == "beads": + with imscope.tab_item("Beads") as (exp, _): + if exp: self._render_beads_tab() + imgui.end_tab_bar() + + def _render_tier_stream_panel(self, tier_key: str, stream_key: str | None) -> None: + """ + [C: tests/test_mma_dashboard_streams.py:TestMMADashboardStreams.test_tier1_renders_stream_content, tests/test_mma_dashboard_streams.py:TestMMADashboardStreams.test_tier3_renders_worker_subheaders] + """ + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_tier_stream_panel") + if self.is_viewing_prior_session: + imgui.text_colored(vec4(255, 200, 100), "HISTORICAL VIEW - READ ONLY") + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_tier_stream_panel") + return + if stream_key is not None: + content = self.mma_streams.get(stream_key, "") + imgui.begin_child(f"##stream_content_{tier_key}", imgui.ImVec2(-1, -1)) + self._render_selectable_label(f'stream_{tier_key}', content, width=-1, multiline=True, height=0) + try: + if len(content) != self._tier_stream_last_len.get(stream_key, -1): + imgui.set_scroll_here_y(1.0) + self._tier_stream_last_len[stream_key] = len(content) + except (TypeError, AttributeError): + pass + imgui.end_child() + else: + tier3_keys = [k for k in self.mma_streams if "Tier 3" in k] + if not tier3_keys: + imgui.text_disabled("No worker output yet.") + else: + worker_status = getattr(self, '_worker_status', {}) + for key in tier3_keys: + ticket_id = key.split(": ", 1)[-1] if ": " in key else key + status = worker_status.get(key, "unknown") + if status == "running": + imgui.text_colored(imgui.ImVec4(1, 1, 0, 1), f"{ticket_id} [{status}]") + elif status == "completed": + imgui.text_colored(imgui.ImVec4(0, 1, 0, 1), f"{ticket_id} [{status}]") + elif status == "failed": + imgui.text_colored(imgui.ImVec4(1, 0, 0, 1), f"{ticket_id} [{status}]") + else: + imgui.text(f"{ticket_id} [{status}]") + imgui.begin_child(f"##tier3_{ticket_id}_scroll", imgui.ImVec2(-1, 150), True) + self._render_selectable_label(f'stream_t3_{ticket_id}', self.mma_streams[key], width=-1, multiline=True, height=0) + try: + if len(self.mma_streams[key]) != self._tier_stream_last_len.get(key, -1): + imgui.set_scroll_here_y(1.0) + self._tier_stream_last_len[key] = len(self.mma_streams[key]) + except (TypeError, AttributeError): + pass + imgui.end_child() + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_tier_stream_panel") + +#enregion: MMA + 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) @@ -2718,28 +3047,546 @@ 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 _save_paths(self): + 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: """ - [C: tests/test_gui_paths.py:test_save_paths] + Logic for capturing UI state for undo/redo. """ - self.config["paths"] = { - "logs_dir": self.ui_logs_dir, - "scripts_dir": self.ui_scripts_dir - } - cfg_path = paths.get_config_path() - if cfg_path.exists(): - shutil.copy(cfg_path, str(cfg_path) + ".bak") - models.save_config(self.config) - paths.reset_resolved() - self.init_state() - self.ai_status = 'paths applied and session reset' + 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_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() + + def _render_text_viewer_window(self) -> None: + """Renders the standalone text/code/markdown viewer window.""" + if not self.show_text_viewer: return + imgui.set_next_window_size(imgui.ImVec2(900, 700), imgui.Cond_.first_use_ever) + expanded, opened = imgui.begin(f"Text Viewer - {self.text_viewer_title}", self.show_text_viewer) + self.show_text_viewer = bool(opened) + if not opened: + self.ui_editing_slices_file = None + self._slice_sel_start = -1 + self._slice_sel_end = -1 + if expanded: + if self.ui_editing_slices_file is not None: + imgui.text_colored(C_IN, "Slice Management (Click-drag lines to select range)") + if imgui.button("Add Selection as Slice"): + if self._slice_sel_start != -1 and self._slice_sel_end != -1: + s_line = min(self._slice_sel_start, self._slice_sel_end) + e_line = max(self._slice_sel_start, self._slice_sel_end) + from src.fuzzy_anchor import FuzzyAnchor + slice_data = FuzzyAnchor.create_slice(self.text_viewer_content, s_line, e_line) + slice_data['tag'] = ""; slice_data['comment'] = "" + self.ui_editing_slices_file.custom_slices.append(slice_data) + self._slice_sel_start = -1; self._slice_sel_end = -1 + imgui.same_line() + if imgui.button("Clear Selection"): self._slice_sel_start = -1; self._slice_sel_end = -1 + to_remove = -1 + for idx, slc in enumerate(self.ui_editing_slices_file.custom_slices): + imgui.push_id(f"slc_row_{idx}"); imgui.text(f"Slice {idx+1}: {slc['start_line']}-{slc['end_line']}"); imgui.same_line() + imgui.set_next_item_width(100); changed_tag, new_tag = imgui.input_text("Tag", slc.get('tag', '')) + if changed_tag: slc['tag'] = new_tag + imgui.same_line(); imgui.set_next_item_width(200); changed_comm, new_comm = imgui.input_text("Comment", slc.get('comment', '')) + if changed_comm: slc['comment'] = new_comm + imgui.same_line() + if imgui.button("Remove"): to_remove = idx + imgui.pop_id() + if to_remove != -1: self.ui_editing_slices_file.custom_slices.pop(to_remove) + imgui.separator() + if imgui.button("Copy"): imgui.set_clipboard_text(self.text_viewer_content) + imgui.same_line(); _, self.text_viewer_wrap = imgui.checkbox("Word Wrap", self.text_viewer_wrap) + imgui.separator() + renderer = markdown_helper.get_renderer(); tv_type = getattr(self, "text_viewer_type", "text") + if tv_type == 'markdown': + with imscope.child("tv_md_scroll", -1, -1, True): markdown_helper.render(self.text_viewer_content, context_id='text_viewer') + elif self.ui_editing_slices_file is not None: + with imscope.child("slice_editor_content", -1, -1, True): + lines = self.text_viewer_content.splitlines(); draw_list = imgui.get_window_draw_list() + for i, line_text in enumerate(lines): + line_num = i + 1; pos = imgui.get_cursor_screen_pos(); line_height = imgui.get_text_line_height() + is_sliced = any(slc['start_line'] <= line_num <= slc['end_line'] for slc in self.ui_editing_slices_file.custom_slices) + if is_sliced: draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(255, 165, 0, 0.2))) + if self._slice_sel_start != -1 and self._slice_sel_end != -1: + s, e = min(self._slice_sel_start, self._slice_sel_end), max(self._slice_sel_start, self._slice_sel_end) + if s <= line_num <= e: draw_list.add_rect_filled(pos, imgui.ImVec2(pos.x + imgui.get_content_region_avail().x, pos.y + line_height), imgui.get_color_u32(vec4(100, 100, 255, 0.3))) + imgui.selectable(f"{line_num:4} | {line_text}##ln{line_num}", False) + if imgui.is_item_clicked(): self._slice_sel_start = line_num; self._slice_sel_end = line_num + if imgui.is_item_hovered() and imgui.is_mouse_down(0): self._slice_sel_end = line_num + elif tv_type in renderer._lang_map: + if self._text_viewer_editor is None: + self._text_viewer_editor = ced.TextEditor(); self._text_viewer_editor.set_read_only_enabled(True); self._text_viewer_editor.set_show_line_numbers_enabled(True) + try: + self._text_viewer_editor.set_text(self.text_viewer_content) + if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_text_viewer_ced") + self._text_viewer_editor.render(f"##ced_{self.text_viewer_title}", imgui.ImVec2(-1, -1)) + if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_text_viewer_ced") + except Exception as e: imgui.text_colored(vec4(255, 100, 100), f"CED Error: {e}"); imgui.text_unformatted(self.text_viewer_content) + else: + with imscope.child("tv_scroll", -1, -1, True): + if self.text_viewer_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) + imgui.text_unformatted(self.text_viewer_content) + if self.text_viewer_wrap: imgui.pop_text_wrap_pos() + imgui.end() + # Sync text and language + + #region: Inject File Modal + if getattr(self, "show_inject_modal", False): + imgui.open_popup("Inject File") + self.show_inject_modal = False + + if imgui.begin_popup_modal("Inject File", None, imgui.WindowFlags_.always_auto_resize)[0]: + files = self.project.get('files', {}).get('paths', []) + imgui.text("Select File to Inject:") + imgui.begin_child("inject_file_list", imgui.ImVec2(0, 200), True) + for f_path in files: + is_selected = (self._inject_file_path == f_path) + if imgui.selectable(f_path, is_selected)[0]: + self._inject_file_path = f_path + self.controller._update_inject_preview() + imgui.end_child() + imgui.separator() + if imgui.radio_button("Skeleton", self._inject_mode == "skeleton"): + self._inject_mode = "skeleton" + self.controller._update_inject_preview() + imgui.same_line() + if imgui.radio_button("Full", self._inject_mode == "full"): + self._inject_mode = "full" + self.controller._update_inject_preview() + imgui.separator() + imgui.text("Preview:") + imgui.begin_child("inject_preview_area", imgui.ImVec2(600, 300), True) + imgui.text_unformatted(self._inject_preview) + imgui.end_child() + imgui.separator() + if imgui.button("Inject", imgui.ImVec2(120, 0)): + formatted = f"## File: {self._inject_file_path}\n```python\n{self._inject_preview}\n```\n" + with self._disc_entries_lock: + self.disc_entries.append({ + "role": "Context", + "content": formatted, + "collapsed": True, + "ts": project_manager.now_ts() + }) + self._scroll_disc_to_bottom = True + imgui.close_current_popup() + imgui.same_line() + if imgui.button("Cancel", imgui.ImVec2(120, 0)): + imgui.close_current_popup() + imgui.end_popup() + #endregion: Inject File Modal + + self._render_ast_inspector_modal() + return + + def _render_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" + + 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_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 _set_external_editor_default(self, editor_name: str) -> None: from src import models - if "tools" not in self.config: - self.config["tools"] = {} - if "default_editor" not in self.config["tools"]: - self.config["tools"]["default_editor"] = {} + if "tools" not in self.config: self.config["tools"] = {} + if "default_editor" not in self.config["tools"]: self.config["tools"]["default_editor"] = {} self.config["tools"]["default_editor"]["default_editor"] = editor_name models.save_config(self.config) self.ai_status = f"Default editor set to: {editor_name}" @@ -2800,11 +3647,9 @@ class App: 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) + 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) + if imgui.button(f"Delete##{name}"): self.delete_context_preset(name) def _apply_pending_patch(self) -> None: if not self._pending_patch_text: @@ -2884,18 +3729,14 @@ class App: with imscope.window("Log Management", self.show_windows["Log Management"]) as (exp, opened): self.show_windows["Log Management"] = bool(opened) if exp: - if self._log_registry is None: - self._log_registry = log_registry.LogRegistry(str(paths.get_logs_dir() / "log_registry.toml")) + if self._log_registry is None: self._log_registry = log_registry.LogRegistry(str(paths.get_logs_dir() / "log_registry.toml")) else: - if imgui.button("Refresh Registry"): - self._log_registry = log_registry.LogRegistry(str(paths.get_logs_dir() / "log_registry.toml")) + if imgui.button("Refresh Registry"): self._log_registry = log_registry.LogRegistry(str(paths.get_logs_dir() / "log_registry.toml")) imgui.same_line() - if imgui.button("Load Log"): - self.cb_load_prior_log() + if imgui.button("Load Log"): self.cb_load_prior_log() imgui.same_line() - if imgui.button("Force Prune Logs"): - self.controller.event_queue.put("gui_task", {"action": "click", "item": "btn_prune_logs"}) - + if imgui.button("Force Prune Logs"): self.controller.event_queue.put("gui_task", {"action": "click", "item": "btn_prune_logs"}) + registry = self._log_registry sessions = registry.data if imgui.begin_table("sessions_table", 7, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable): @@ -2954,148 +3795,6 @@ class App: if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_log_management") - 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): - self.show_windows["Diagnostics"] = bool(opened) - if exp: - metrics = self.perf_monitor.get_metrics() - imgui.text("Performance Telemetry") - imgui.same_line() - _, self.perf_profiling_enabled = imgui.checkbox("Enable Profiling", self.perf_profiling_enabled) - imgui.separator() - - if imgui.begin_table("perf_table", 3, imgui.TableFlags_.borders_inner_h): - imgui.table_setup_column("Metric") - imgui.table_setup_column("Value") - imgui.table_setup_column("Graph") - imgui.table_headers_row() - - for label, key, format_str in [ - ("FPS", "fps", "%.1f"), - ("Frame Time (ms)", "frame_time_ms", "%.2f"), - ("CPU %", "cpu_percent", "%.1f"), - ("Input Lag (ms)", "input_lag_ms", "%.1f") - ]: - imgui.table_next_row() - imgui.table_next_column() - imgui.text(label) - imgui.table_next_column() - if key == "fps": - avg_val = imgui.get_io().framerate - else: - avg_val = metrics.get(f"{key}_avg", metrics.get(key, 0.0)) - imgui.text(format_str % avg_val) - imgui.table_next_column() - self.perf_show_graphs.setdefault(key, False) - _, self.perf_show_graphs[key] = imgui.checkbox(f"##g_{key}", self.perf_show_graphs[key]) - imgui.end_table() - - if self.perf_profiling_enabled: - imgui.separator() - imgui.text("Detailed Component Timings (Moving Average)") - if imgui.begin_table("comp_timings", 6, imgui.TableFlags_.borders): - imgui.table_setup_column("Component") - imgui.table_setup_column("Avg (ms)") - imgui.table_setup_column("Count") - imgui.table_setup_column("Max (ms)") - imgui.table_setup_column("Min (ms)") - imgui.table_setup_column("Graph") - imgui.table_headers_row() - for key, val in metrics.items(): - if key.startswith("time_") and key.endswith("_ms") and not key.endswith("_avg"): - comp_name = key[5:-3] - avg_val = metrics.get(f"{key}_avg", val) - count = int(metrics.get(f"count_{comp_name}", 0)) - max_val = metrics.get(f"max_{comp_name}_ms", 0.0) - min_val = metrics.get(f"min_{comp_name}_ms", 0.0) - imgui.table_next_row() - imgui.table_next_column() - imgui.text(comp_name) - imgui.table_next_column() - if avg_val > 10.0: - imgui.text_colored(imgui.ImVec4(1.0, 0.2, 0.2, 1.0), f"{avg_val:.2f}") - else: - imgui.text(f"{avg_val:.2f}") - imgui.table_next_column() - imgui.text(f"{count}") - imgui.table_next_column() - imgui.text(f"{max_val:.2f}") - imgui.table_next_column() - imgui.text(f"{min_val:.2f}") - imgui.table_next_column() - self.perf_show_graphs.setdefault(comp_name, False) - _, self.perf_show_graphs[comp_name] = imgui.checkbox(f"##g_{comp_name}", self.perf_show_graphs[comp_name]) - imgui.end_table() - - imgui.separator() - imgui.text("Performance Graphs") - for key, show in self.perf_show_graphs.items(): - if show: - imgui.text(f"History: {key}") - hist_data = self.perf_monitor.get_history(key) - if hist_data: - import numpy as np - imgui.plot_lines(f"##plot_{key}", np.array(hist_data, dtype=np.float32), graph_size=imgui.ImVec2(-1, 60)) - else: - imgui.text_disabled(f"(no history data for {key})") - - imgui.separator() - imgui.text("Diagnostic Log") - if imgui.begin_table("diag_log_table", 3, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable): - imgui.table_setup_column("Timestamp", imgui.TableColumnFlags_.width_fixed, 150) - imgui.table_setup_column("Type", imgui.TableColumnFlags_.width_fixed, 100) - imgui.table_setup_column("Message") - imgui.table_headers_row() - for entry in reversed(self.controller.diagnostic_log): - imgui.table_next_row() - imgui.table_next_column() - imgui.text(entry.get("ts", "")) - imgui.table_next_column() - imgui.text(entry.get("type", "")) - imgui.table_next_column() - imgui.text_wrapped(entry.get("message", "")) - imgui.end_table() - - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_diagnostics_panel") - - def _render_discussion_tab(self) -> None: - imgui.begin_child("HistoryChild", size=(0, -self.ui_discussion_split_h)) - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_discussion_panel") - self._render_discussion_panel() - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_discussion_panel") - imgui.end_child() - imgui.button("###discussion_splitter", imgui.ImVec2(-1, 4)) - if imgui.is_item_active(): - self.ui_discussion_split_h = max(150.0, min(imgui.get_window_height() - 150.0, self.ui_discussion_split_h - imgui.get_io().mouse_delta.y)) - imgui.push_style_var(imgui.StyleVar_.item_spacing, imgui.ImVec2(10, 4)) - ch1, self.ui_separate_message_panel = imgui.checkbox("Pop Out Message", self.ui_separate_message_panel) - imgui.same_line() - ch2, self.ui_separate_response_panel = imgui.checkbox("Pop Out Response", self.ui_separate_response_panel) - if ch1: self.show_windows["Message"] = self.ui_separate_message_panel - if ch2: self.show_windows["Response"] = self.ui_separate_response_panel - imgui.pop_style_var() - show_message_tab = not self.ui_separate_message_panel - show_response_tab = not self.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 self._autofocus_response_tab: - tab_flags = imgui.TabItemFlags_.set_selected - self._autofocus_response_tab = False - self.controller._autofocus_response_tab = False - if show_message_tab: - if imgui.begin_tab_item("Message", None)[0]: - self._render_message_panel() - imgui.end_tab_item() - if show_response_tab: - if imgui.begin_tab_item("Response", None, tab_flags)[0]: - self._render_response_panel() - imgui.end_tab_item() - imgui.end_tab_bar() - else: - imgui.text_disabled("Message & Response panels are detached.") - def _update_context_file_stats(self) -> tuple[int, int]: if not hasattr(self, '_file_stats_cache'): self._file_stats_cache = {} @@ -3451,153 +4150,40 @@ def hello(): """ markdown_helper.render(md) - def _render_files_panel(self, height_override: float = 0) -> None: - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_files_panel") - imgui.text("Paths") - imgui.same_line() - imgui.text("| Base Dir:") - imgui.same_line() - imgui.set_next_item_width(-100) - ch, self.ui_files_base_dir = imgui.input_text("##f_base", self.ui_files_base_dir) - imgui.same_line() - if imgui.button("Browse##fb"): - r = hide_tk_root() - d = filedialog.askdirectory() - r.destroy() - if d: self.ui_files_base_dir = d - imgui.separator() - # Calculate content-based height: use override if provided, else content-based - if height_override > 0: - child_h = height_override - else: - row_count = max(len(self.files), 1) - child_h = min(row_count * 28 + 40, 300) - # BEGIN f_paths child window - imgui.begin_child("f_paths", imgui.ImVec2(0, child_h), True) - if imgui.begin_table("files_table", 4, imgui.TableFlags_.resizable | imgui.TableFlags_.borders): - imgui.table_setup_column("Actions", imgui.TableColumnFlags_.width_fixed, 40) - imgui.table_setup_column("File Path", imgui.TableColumnFlags_.width_stretch) - imgui.table_setup_column("Flags", imgui.TableColumnFlags_.width_fixed, 150) - imgui.table_setup_column("Cache", imgui.TableColumnFlags_.width_fixed, 40) - imgui.table_headers_row() - - for i, f_item in enumerate(self.files): - imgui.table_next_row() - # Actions - imgui.table_set_column_index(0) - if imgui.button(f"x##f{i}"): - self.files.pop(i) - break - # File Path - imgui.table_set_column_index(1) - imgui.text(f_item.path if hasattr(f_item, "path") else str(f_item)) - # Flags - imgui.table_set_column_index(2) - if hasattr(f_item, "auto_aggregate"): - changed_agg, f_item.auto_aggregate = imgui.checkbox(f"Agg##a{i}", f_item.auto_aggregate) - imgui.same_line() - changed_full, f_item.force_full = imgui.checkbox(f"Full##f{i}", f_item.force_full) - # Cache - imgui.table_set_column_index(3) - path = f_item.path if hasattr(f_item, "path") else str(f_item) - is_cached = any(path in c for c in getattr(self, "_cached_files", [])) - if is_cached: - imgui.text_colored("●", imgui.ImVec4(0, 1, 0, 1)) # Green dot - else: - imgui.text_disabled("○") - imgui.end_table() - imgui.end_child() - if imgui.button("Add File(s)"): - r = hide_tk_root() - paths = filedialog.askopenfilenames() - r.destroy() - for p in paths: - if p not in [f.path if hasattr(f, "path") else f for f in self.files]: - self.files.append(models.FileItem(path=p)) - imgui.same_line() - if imgui.button("Add Wildcard"): - r = hide_tk_root() - d = filedialog.askdirectory() - r.destroy() - if d: self.files.append(models.FileItem(path=str(Path(d) / "**" / "*"))) - - imgui.separator() - from src import summarize - stats = summarize._summary_cache.get_stats() - imgui.text_disabled(f"Summary Cache: {stats['entries']} entries ({stats['size_bytes']} bytes)") - imgui.same_line() - if imgui.button("Clear Summary Cache##btn_clear_summary_cache"): - self.controller._cb_clear_summary_cache() - - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_files_panel") - def _render_screenshots_panel(self, height_override: float = 0) -> None: if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_screenshots_panel") - imgui.text("Paths") - imgui.same_line() - imgui.text("| Base Dir:") - imgui.same_line() + imgui.text("Paths"); imgui.same_line(); imgui.text("| Base Dir:"); imgui.same_line(); imgui.set_next_item_width(-100) ch, self.ui_shots_base_dir = imgui.input_text("##s_base", self.ui_shots_base_dir) imgui.same_line() if imgui.button("Browse##sb"): - r = hide_tk_root() - d = filedialog.askdirectory() - r.destroy() + r = hide_tk_root(); d = filedialog.askdirectory(); r.destroy() if d: self.ui_shots_base_dir = d imgui.separator() # Calculate content-based height: use override if provided, else content-based - if height_override > 0: - shot_h = height_override + if height_override > 0: shot_h = height_override else: shot_count = max(len(self.screenshots), 1) shot_h = min(shot_count * 28 + 40, 200) # BEGIN s_paths child window imgui.begin_child("s_paths", imgui.ImVec2(0, shot_h), True) for i, s in enumerate(self.screenshots): - if imgui.button(f"x##s{i}"): + if imgui.button(f"x##s{i}"): self.screenshots.pop(i) break - imgui.same_line() - imgui.text(s) + imgui.same_line(); imgui.text(s) imgui.end_child() if imgui.button("Add Screenshot(s)"): - r = hide_tk_root() + r = hide_tk_root() paths = filedialog.askopenfilenames( title="Select Screenshots", filetypes=[("Images", "*.png *.jpg *.jpeg *.gif *.bmp *.webp"), ("All", "*.*")], ) r.destroy() - for p in paths: + for p in paths: if p not in self.screenshots: self.screenshots.append(p) if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_screenshots_panel") - def _render_discussion_panel(self) -> 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_phase4.py:test_track_discussion_toggle, tests/test_gui_symbol_navigation.py:test_render_discussion_panel_symbol_lookup] - """ - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_discussion_panel") - self._render_thinking_indicator() - - if self.is_viewing_prior_session: - self._render_prior_session_view() - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_discussion_panel") - return - - self._render_discussion_selector() - - if not self.is_viewing_prior_session: - imgui.separator() - self._render_discussion_entry_controls() - - imgui.separator() - self._render_discussion_roles() - - imgui.separator() - self._render_discussion_entries() - - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_discussion_panel") - def _render_synthesis_panel(self) -> None: """ Renders a panel for synthesizing multiple discussion takes. @@ -3619,19 +4205,18 @@ def hello(): if len(selected) > 1: from src import synthesis_formatter discussions_dict = self.project.get('discussion', {}).get('discussions', {}) - takes_dict = {name: discussions_dict.get(name, {}).get('history', []) for name in selected} - diff_text = synthesis_formatter.format_takes_diff(takes_dict) - prompt = f"{self.ui_synthesis_prompt}\n\nHere are the variations:\n{diff_text}" + takes_dict = {name: discussions_dict.get(name, {}).get('history', []) for name in selected} + diff_text = synthesis_formatter.format_takes_diff(takes_dict) + prompt = f"{self.ui_synthesis_prompt}\n\nHere are the variations:\n{diff_text}" new_name = "synthesis_take" - counter = 1 + counter = 1 while new_name in discussions_dict: new_name = f"synthesis_take_{counter}" counter += 1 self._create_discussion(new_name) - with self._disc_entries_lock: - self.disc_entries.append({"role": "User", "content": prompt, "collapsed": False, "ts": project_manager.now_ts()}) + with self._disc_entries_lock: self.disc_entries.append({"role": "User", "content": prompt, "collapsed": False, "ts": project_manager.now_ts()}) self._handle_generate_send() def _render_persona_selector_panel(self) -> None: @@ -3768,17 +4353,13 @@ def hello(): 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" + 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() + 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() @@ -3786,8 +4367,7 @@ def hello(): r = hide_tk_root() p = filedialog.askopenfilename(title="Select gemini CLI binary") r.destroy() - if p: - self.ui_gemini_cli_path = p + 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 @@ -3898,40 +4478,6 @@ def hello(): imgui.text_disabled("Cache Usage: INACTIVE") if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_token_budget_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": - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_cache_panel") - return - imgui.text_colored(C_LBL, 'Cache Analytics') - stats = getattr(self.controller, '_cached_cache_stats', {}) - if not stats.get("cache_exists"): - imgui.text_disabled("No active cache") - if self.perf_profiling_enabled: self.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"): - self.controller.clear_cache() - self._cache_cleared_timestamp = time.time() - if hasattr(self, '_cache_cleared_timestamp') and time.time() - self._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 self.perf_profiling_enabled: self.perf_monitor.end_component("_render_cache_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') @@ -3987,17 +4533,6 @@ def hello(): 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_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_message_panel(self) -> None: if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_message_panel") # LIVE indicator @@ -4152,24 +4687,17 @@ def hello(): self._comms_log_dirty = True imgui.separator() - imgui.text_colored(C_OUT, "OUT") - imgui.same_line() - imgui.text_colored(C_REQ, "request") - imgui.same_line() - imgui.text_colored(C_TC, "tool_call") - imgui.same_line() - imgui.text(" ") - imgui.same_line() - imgui.text_colored(C_IN, "IN") - imgui.same_line() - imgui.text_colored(C_RES, "response") - imgui.same_line() + imgui.text_colored(C_OUT, "OUT"); imgui.same_line() + imgui.text_colored(C_REQ, "request"); imgui.same_line() + imgui.text_colored(C_TC, "tool_call"); imgui.same_line() + imgui.text(" "); imgui.same_line() + imgui.text_colored(C_IN, "IN"); imgui.same_line() + imgui.text_colored(C_RES, "response"); imgui.same_line() imgui.text_colored(C_TR, "tool_result") imgui.separator() # Use tinted background for prior session - if self.is_viewing_prior_session: - imgui.push_style_color(imgui.Col_.child_bg, vec4(40, 30, 20)) + if self.is_viewing_prior_session: imgui.push_style_color(imgui.Col_.child_bg, vec4(40, 30, 20)) imgui.begin_child("comms_scroll", imgui.ImVec2(0, 0), False, imgui.WindowFlags_.horizontal_scrollbar) @@ -4183,19 +4711,18 @@ def hello(): imgui.push_id(f"comms_entry_{i}") i_display = i + 1 - ts = entry.get("ts", "00:00:00") + ts = entry.get("ts", "00:00:00") direction = entry.get("direction", "??") - kind = entry.get("kind", entry.get("type", "??")) - provider = entry.get("provider", "?") - model = entry.get("model", "?") - tier = entry.get("source_tier", "main") - payload = entry.get("payload", {}) + kind = entry.get("kind", entry.get("type", "??")) + provider = entry.get("provider", "?") + model = entry.get("model", "?") + tier = entry.get("source_tier", "main") + payload = entry.get("payload", {}) if not payload and kind not in ("request", "response", "tool_call", "tool_result"): payload = entry # legacy # Row 1: #Idx TS DIR KIND Provider/Model [Tier] - imgui.text_colored(C_LBL, f"#{i_display}") - imgui.same_line() + imgui.text_colored(C_LBL, f"#{i_display}"); imgui.same_line() imgui.text_colored(vec4(160, 160, 160), ts) latency = entry.get("latency") or entry.get("metadata", {}).get("latency") @@ -4209,13 +4736,10 @@ def hello(): imgui.text_colored(vec4(255, 120, 120), f"[{ticket_id}]") imgui.same_line() d_col = DIR_COLORS.get(direction, C_VAL) - imgui.text_colored(d_col, direction) - imgui.same_line() + imgui.text_colored(d_col, direction); imgui.same_line() k_col = KIND_COLORS.get(kind, C_VAL) - imgui.text_colored(k_col, kind) - imgui.same_line() - imgui.text_colored(C_LBL, f"{provider}/{model}") - imgui.same_line() + imgui.text_colored(k_col, kind); imgui.same_line() + imgui.text_colored(C_LBL, f"{provider}/{model}"); imgui.same_line() imgui.text_colored(C_SUB, f"[{tier}]") # Optimized content rendering using _render_heavy_text logic @@ -4229,35 +4753,29 @@ def hello(): if payload.get("system"): self._render_heavy_text("system", payload.get("system", ""), idx_str) elif kind == "response": - r = payload.get("round", 0) - sr = payload.get("stop_reason", "STOP") - usage = payload.get("usage", {}) + r = payload.get("round", 0) + sr = payload.get("stop_reason", "STOP") + usage = payload.get("usage", {}) usage_str = "" if usage: - inp = usage.get("input_tokens", 0) - out = usage.get("output_tokens", 0) - cache = usage.get("cache_read_input_tokens", 0) + inp = usage.get("input_tokens", 0) + out = usage.get("output_tokens", 0) + cache = usage.get("cache_read_input_tokens", 0) usage_str = f" in:{inp} out:{out}" - if cache: - usage_str += f" cache:{cache}" + if cache: usage_str += f" cache:{cache}" imgui.text_colored(C_LBL, f"round: {r} stop_reason: {sr}{usage_str}") - text_content = payload.get("text", "") + text_content = payload.get("text", "") segments, parsed_response = thinking_parser.parse_thinking_trace(text_content) - if segments: - self._render_thinking_trace([{"content": s.content, "marker": s.marker} for s in segments], i, is_standalone=not bool(parsed_response.strip())) - if parsed_response: - self._render_heavy_text("text", parsed_response, idx_str) + if segments: self._render_thinking_trace([{"content": s.content, "marker": s.marker} for s in segments], i, is_standalone=not bool(parsed_response.strip())) + if parsed_response: self._render_heavy_text("text", parsed_response, idx_str) tcs = payload.get("tool_calls", []) - if tcs: - self._render_heavy_text("tool_calls", json.dumps(tcs, indent=1), idx_str) - elif kind == "tool_call": - self._render_heavy_text(payload.get("name", "call"), payload.get("script") or json.dumps(payload.get("args", {}), indent=1), idx_str) - elif kind == "tool_result": - self._render_heavy_text(payload.get("name", "result"), payload.get("output", ""), idx_str) - else: - self._render_heavy_text("data", str(payload), idx_str) + if tcs: self._render_heavy_text("tool_calls", json.dumps(tcs, indent=1), idx_str) + + elif kind == "tool_call": self._render_heavy_text(payload.get("name", "call"), payload.get("script") or json.dumps(payload.get("args", {}), indent=1), idx_str) + elif kind == "tool_result": self._render_heavy_text(payload.get("name", "result"), payload.get("output", ""), idx_str) + else: self._render_heavy_text("data", str(payload), idx_str) imgui.separator() imgui.pop_id() @@ -4370,8 +4888,8 @@ def hello(): def _cb_block_ticket(self, ticket_id: str) -> None: t = next((t for t in self.active_tickets if str(t.get('id', '')) == ticket_id), None) if t: - t['status'] = 'blocked' - t['manual_block'] = True + t['status'] = 'blocked' + t['manual_block'] = True t['blocked_reason'] = '[MANUAL] User blocked' changed = True while changed: @@ -4382,15 +4900,15 @@ def hello(): dep = next((x for x in self.active_tickets if str(x.get('id', '')) == dep_id), None) if dep and dep.get('status') == 'blocked': t['status'] = 'blocked' - changed = True + changed = True break self._push_mma_state_update() def _cb_unblock_ticket(self, ticket_id: str) -> None: t = next((t for t in self.active_tickets if str(t.get('id', '')) == ticket_id), None) if t and t.get('manual_block', False): - t['status'] = 'todo' - t['manual_block'] = False + t['status'] = 'todo' + t['manual_block'] = False t['blocked_reason'] = None changed = True while changed: @@ -4405,7 +4923,7 @@ def hello(): break if can_run: t['status'] = 'todo' - changed = True + changed = True self._push_mma_state_update() def cb_load_prior_log(self, path: Optional[str] = None) -> None: @@ -4416,518 +4934,21 @@ def hello(): if path: self.controller.cb_load_prior_log(path) - def _reorder_ticket(self, src_idx: int, dst_idx: int) -> None: + def _save_paths(self): """ - [C: tests/test_ticket_queue.py:TestReorder.test_reorder_ticket_invalid, tests/test_ticket_queue.py:TestReorder.test_reorder_ticket_valid] + [C: tests/test_gui_paths.py:test_save_paths] """ - if src_idx == dst_idx: return - new_tickets = list(self.active_tickets) - ticket = new_tickets.pop(src_idx) - new_tickets.insert(dst_idx, ticket) - # Validate dependencies: a ticket cannot be placed before any of its dependencies - id_to_idx = {str(t.get('id', '')): i for i, t in enumerate(new_tickets)} - valid = True - for i, t in enumerate(new_tickets): - deps = t.get('depends_on', []) - for d_id in deps: - if d_id in id_to_idx and id_to_idx[d_id] >= i: - valid = False - break - if not valid: break - if valid: - self.active_tickets = new_tickets - self._push_mma_state_update() - - def _render_ticket_queue(self) -> None: - """ - [C: tests/test_gui_kill_button.py:test_render_ticket_queue_table_columns] - """ - imgui.text("Ticket Queue Management") - if not self.active_track: - imgui.text_disabled("No active track.") - return - - # Select All / None - if imgui.button("Select All"): - self.ui_selected_tickets = {str(t.get('id', '')) for t in self.active_tickets} - imgui.same_line() - if imgui.button("Select None"): - self.ui_selected_tickets.clear() - - imgui.same_line() - imgui.spacing() - imgui.same_line() - - # Bulk Actions - if imgui.button("Bulk Execute"): - self.bulk_execute() - imgui.same_line() - if imgui.button("Bulk Skip"): - self.bulk_skip() - imgui.same_line() - if imgui.button("Bulk Block"): - self.bulk_block() - # Table - flags = imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable | imgui.TableFlags_.scroll_y - if imgui.begin_table("ticket_queue_table", 7, flags, imgui.ImVec2(0, 300)): - imgui.table_setup_column("Select", imgui.TableColumnFlags_.width_fixed, 40) - imgui.table_setup_column("ID", imgui.TableColumnFlags_.width_fixed, 80) - imgui.table_setup_column("Priority", imgui.TableColumnFlags_.width_fixed, 100) - imgui.table_setup_column("Model", imgui.TableColumnFlags_.width_fixed, 150) - imgui.table_setup_column("Status", imgui.TableColumnFlags_.width_fixed, 100) - imgui.table_setup_column("Description", imgui.TableColumnFlags_.width_stretch) - imgui.table_setup_column("Actions", imgui.TableColumnFlags_.width_fixed, 80) - imgui.table_headers_row() - - for i, t in enumerate(self.active_tickets): - tid = str(t.get('id', '')) - imgui.table_next_row() - - # Select - imgui.table_next_column() - is_sel = tid in self.ui_selected_tickets - changed, is_sel = imgui.checkbox(f"##sel_{tid}", is_sel) - if changed: - if is_sel: self.ui_selected_tickets.add(tid) - else: self.ui_selected_tickets.discard(tid) - - # ID - imgui.table_next_column() - is_selected = (tid == self.ui_selected_ticket_id) - opened, _ = imgui.selectable(f"{tid}##drag_{tid}", is_selected) - if opened: - self.ui_selected_ticket_id = tid - - if imgui.begin_drag_drop_source(): - imgui.set_drag_drop_payload("TICKET_REORDER", i) - imgui.text(f"Moving {tid}") - imgui.end_drag_drop_source() - - if imgui.begin_drag_drop_target(): - payload = imgui.accept_drag_drop_payload("TICKET_REORDER") - if payload: - src_idx = int(payload.data) - self._reorder_ticket(src_idx, i) - imgui.end_drag_drop_target() - - # Priority - - imgui.table_next_column() - prio = t.get('priority', 'medium') - p_col = vec4(180, 180, 180) # gray - if prio == 'high': p_col = vec4(255, 100, 100) # red - elif prio == 'medium': p_col = vec4(255, 255, 100) # yellow - - imgui.push_style_color(imgui.Col_.text, p_col) - if imgui.begin_combo(f"##prio_{tid}", prio, imgui.ComboFlags_.height_small): - for p_opt in ['high', 'medium', 'low']: - if imgui.selectable(p_opt, p_opt == prio)[0]: - t['priority'] = p_opt - self._push_mma_state_update() - imgui.end_combo() - imgui.pop_style_color() - - # Model - imgui.table_next_column() - model_override = t.get('model_override') - current_model = model_override if model_override else "Default" - if imgui.begin_combo(f"##model_{tid}", current_model, imgui.ComboFlags_.height_small): - if imgui.selectable("Default", model_override is None)[0]: - t['model_override'] = None - self._push_mma_state_update() - for model in ["gemini-2.5-flash-lite", "gemini-2.5-flash", "gemini-3-flash-preview", "gemini-3.1-pro-preview", "deepseek-v3"]: - if imgui.selectable(model, model_override == model)[0]: - t['model_override'] = model - self._push_mma_state_update() - imgui.end_combo() - - # Status - imgui.table_next_column() - status = t.get('status', 'todo') - if t.get('model_override'): - imgui.text_colored(imgui.ImVec4(1, 0.5, 0, 1), f"{status} [{t.get('model_override')}]") - else: - imgui.text(t.get('status', 'todo')) - - # Description - imgui.table_next_column() - imgui.text(t.get('description', '')) - - # Actions - Kill button for in_progress tickets - imgui.table_next_column() - status = t.get('status', 'todo') - if status == 'in_progress': - if imgui.button(f"Kill##{tid}"): - self._cb_kill_ticket(tid) - elif status == 'todo': - if imgui.button(f"Block##{tid}"): - self._cb_block_ticket(tid) - elif status == 'blocked' and t.get('manual_block', False): - if imgui.button(f"Unblock##{tid}"): - self._cb_unblock_ticket(tid) - - imgui.end_table() - - def _render_mma_dashboard(self) -> None: - """Main MMA dashboard interface.""" - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_mma_dashboard") - self._render_mma_focus_selector() - imgui.separator() - if self.is_viewing_prior_session: - c = vec4(255, 152, 48) if theme.is_nerv_active() else vec4(255, 200, 100) - imgui.text_colored(c, "HISTORICAL VIEW - READ ONLY") - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_mma_dashboard") - return - self._render_mma_track_summary() - imgui.separator() - self._render_mma_epic_planner() - imgui.separator() - if imgui.collapsing_header("Conductor Setup"): self._render_mma_conductor_setup() - imgui.separator() - self._render_mma_track_browser() - imgui.separator() - self._render_mma_global_controls() - imgui.separator() - self._render_mma_usage_section() - imgui.separator() - self._render_ticket_queue() - imgui.separator() - self._render_window_if_open("Task DAG", self._render_task_dag_panel, not self.ui_separate_task_dag) - if self.ui_selected_ticket_id: self._render_mma_ticket_editor() - imgui.separator() - self._render_mma_agent_streams() - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_mma_dashboard") - - def _render_task_dag_panel(self) -> None: # 4. Task DAG Visualizer - imgui.text("Task DAG") - if (self.active_track or self.active_tickets) and self.node_editor_ctx: - ed.set_current_editor(self.node_editor_ctx) - ed.begin('Visual DAG') - # Selection detection - selected = ed.get_selected_nodes() - if selected: - for node_id in selected: - node_val = node_id.id() - for t in self.active_tickets: - if abs(hash(str(t.get('id', '')))) == node_val: - self.ui_selected_ticket_id = str(t.get('id', '')) - break - break - for t in self.active_tickets: - tid = str(t.get('id', '??')) - int_id = abs(hash(tid)) - ed.begin_node(ed.NodeId(int_id)) - if getattr(self, "ui_project_execution_mode", "native") == "beads": - imgui.text_colored(imgui.ImVec4(0, 1, 1, 1), "[B] ") - imgui.same_line() - imgui.text_colored(C_KEY, f"Ticket: {tid}") - status = t.get('status', 'todo') - s_col = C_VAL - if status == 'done' or status == 'complete': s_col = C_IN - elif status == 'in_progress' or status == 'running': s_col = C_OUT - elif status == 'error': s_col = imgui.ImVec4(1, 0.4, 0.4, 1) - imgui.text("Status: ") - imgui.same_line() - imgui.text_colored(s_col, status) - imgui.text(f"Target: {t.get('target_file','')}") - ed.begin_pin(ed.PinId(abs(hash(tid + "_in"))), ed.PinKind.input) - imgui.text("->") - ed.end_pin() - imgui.same_line() - ed.begin_pin(ed.PinId(abs(hash(tid + "_out"))), ed.PinKind.output) - imgui.text("->") - ed.end_pin() - ed.end_node() - for t in self.active_tickets: - tid = str(t.get('id', '??')) - for dep in t.get('depends_on', []): - ed.link(ed.LinkId(abs(hash(dep + "_" + tid))), ed.PinId(abs(hash(dep + "_out"))), ed.PinId(abs(hash(tid + "_in")))) - - # Handle link creation - if ed.begin_create(): - start_pin = ed.PinId() - end_pin = ed.PinId() - if ed.query_new_link(start_pin, end_pin): - if ed.accept_new_item(): - s_id = start_pin.id() - e_id = end_pin.id() - source_tid = None - target_tid = None - for t in self.active_tickets: - tid = str(t.get('id', '')) - if abs(hash(tid + "_out")) == s_id: source_tid = tid - if abs(hash(tid + "_out")) == e_id: source_tid = tid - if abs(hash(tid + "_in")) == s_id: target_tid = tid - if abs(hash(tid + "_in")) == e_id: target_tid = tid - if source_tid and target_tid and source_tid != target_tid: - for t in self.active_tickets: - if str(t.get('id', '')) == target_tid: - if source_tid not in t.get('depends_on', []): - t.setdefault('depends_on', []).append(source_tid) - self._push_mma_state_update() - break - ed.end_create() - - # Handle link deletion - if ed.begin_delete(): - link_id = ed.LinkId() - while ed.query_deleted_link(link_id): - if ed.accept_deleted_item(): - lid_val = link_id.id() - for t in self.active_tickets: - tid = str(t.get('id', '')) - deps = t.get('depends_on', []) - if any(abs(hash(d + "_" + tid)) == lid_val for d in deps): - t['depends_on'] = [dep for dep in deps if abs(hash(dep + "_" + tid)) != lid_val] - self._push_mma_state_update() - break - ed.end_delete() - # Validate DAG after any changes - try: - from src.dag_engine import TrackDAG - ticket_dicts = [{'id': str(t.get('id', '')), 'depends_on': t.get('depends_on', [])} for t in self.active_tickets] - temp_dag = TrackDAG(ticket_dicts) - if temp_dag.has_cycle(): - imgui.open_popup("Cycle Detected!") - except Exception: - pass - ed.end() - # 5. Add Ticket Form - imgui.separator() - if imgui.button("Add Ticket"): - self._show_add_ticket_form = not self._show_add_ticket_form - if self._show_add_ticket_form: - # Default Ticket ID - max_id = 0 - for t in self.active_tickets: - tid = t.get('id', '') - if tid.startswith('T-'): - try: max_id = max(max_id, int(tid[2:])) - except: pass - self.ui_new_ticket_id = f"T-{max_id + 1:03d}" - self.ui_new_ticket_desc = "" - self.ui_new_ticket_target = "" - self.ui_new_ticket_deps = "" - if self._show_add_ticket_form: - imgui.begin_child("add_ticket_form", imgui.ImVec2(-1, 220), True) - imgui.text_colored(C_VAL, "New Ticket Details") - _, self.ui_new_ticket_id = imgui.input_text("ID##new_ticket", self.ui_new_ticket_id) - _, self.ui_new_ticket_desc = imgui.input_text_multiline("Description##new_ticket", self.ui_new_ticket_desc, imgui.ImVec2(-1, 60)) - _, self.ui_new_ticket_target = imgui.input_text("Target File##new_ticket", self.ui_new_ticket_target) - _, self.ui_new_ticket_deps = imgui.input_text("Depends On (IDs, comma-separated)##new_ticket", self.ui_new_ticket_deps) - imgui.text("Priority:") - imgui.same_line() - if imgui.begin_combo("##new_prio", self.ui_new_ticket_priority): - for p_opt in ['high', 'medium', 'low']: - if imgui.selectable(p_opt, p_opt == self.ui_new_ticket_priority)[0]: - self.ui_new_ticket_priority = p_opt - imgui.end_combo() - if imgui.button("Create"): - new_ticket = { - "id": self.ui_new_ticket_id, - "description": self.ui_new_ticket_desc, - "status": "todo", - "priority": self.ui_new_ticket_priority, - "assigned_to": "tier3-worker", - "target_file": self.ui_new_ticket_target, - "depends_on": [d.strip() for d in self.ui_new_ticket_deps.split(",") if d.strip()] - } - self.active_tickets.append(new_ticket) - self._show_add_ticket_form = False - self._push_mma_state_update() - imgui.same_line() - if imgui.button("Cancel"): - self._show_add_ticket_form = False - imgui.end_child() - else: - imgui.text_disabled("No active MMA track or tickets.") - - def _render_beads_tab(self) -> None: - imgui.text("Beads Graph (Dolt-backed)") - if imgui.button("Refresh Beads"): - pass - imgui.separator() - - # Check for dolt/bd dependencies - dolt_path = shutil.which("dolt") - bd_path = shutil.which("bd") - if not dolt_path or not bd_path: - missing = [] - if not dolt_path: missing.append("'dolt'") - if not bd_path: missing.append("'bd'") - imgui.text_colored(imgui.ImVec4(1, 0.5, 0, 1), f"Warning: {', '.join(missing)} not found in PATH.") - imgui.text_wrapped("Beads mode requires Dolt and the Beads (bd) CLI tools.") - - if getattr(self, "ui_project_execution_mode", "native") == "beads": - try: - from src import beads_client - bclient = beads_client.BeadsClient(Path(self.active_project_root)) - beads = bclient.list_beads() - if not beads: - imgui.text_disabled("No beads found.") - else: - if imgui.begin_table("beads_table", 3, imgui.TableFlags_.borders | imgui.TableFlags_.row_bg | imgui.TableFlags_.resizable): - imgui.table_setup_column("ID") - imgui.table_setup_column("Status") - imgui.table_setup_column("Title") - imgui.table_headers_row() - for b in beads: - imgui.table_next_row() - imgui.table_next_column() - imgui.text(str(b.id)) - imgui.table_next_column() - imgui.text(str(b.status)) - imgui.table_next_column() - imgui.text(str(b.title)) - imgui.end_table() - except Exception as e: - imgui.text_colored(imgui.ImVec4(1, 0, 0, 1), f"Error loading beads: {e}") - - def _render_tier_stream_panel(self, tier_key: str, stream_key: str | None) -> None: - """ - [C: tests/test_mma_dashboard_streams.py:TestMMADashboardStreams.test_tier1_renders_stream_content, tests/test_mma_dashboard_streams.py:TestMMADashboardStreams.test_tier3_renders_worker_subheaders] - """ - if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_tier_stream_panel") - if self.is_viewing_prior_session: - imgui.text_colored(vec4(255, 200, 100), "HISTORICAL VIEW - READ ONLY") - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_tier_stream_panel") - return - if stream_key is not None: - content = self.mma_streams.get(stream_key, "") - imgui.begin_child(f"##stream_content_{tier_key}", imgui.ImVec2(-1, -1)) - self._render_selectable_label(f'stream_{tier_key}', content, width=-1, multiline=True, height=0) - try: - if len(content) != self._tier_stream_last_len.get(stream_key, -1): - imgui.set_scroll_here_y(1.0) - self._tier_stream_last_len[stream_key] = len(content) - except (TypeError, AttributeError): - pass - imgui.end_child() - else: - tier3_keys = [k for k in self.mma_streams if "Tier 3" in k] - if not tier3_keys: - imgui.text_disabled("No worker output yet.") - else: - worker_status = getattr(self, '_worker_status', {}) - for key in tier3_keys: - ticket_id = key.split(": ", 1)[-1] if ": " in key else key - status = worker_status.get(key, "unknown") - if status == "running": - imgui.text_colored(imgui.ImVec4(1, 1, 0, 1), f"{ticket_id} [{status}]") - elif status == "completed": - imgui.text_colored(imgui.ImVec4(0, 1, 0, 1), f"{ticket_id} [{status}]") - elif status == "failed": - imgui.text_colored(imgui.ImVec4(1, 0, 0, 1), f"{ticket_id} [{status}]") - else: - imgui.text(f"{ticket_id} [{status}]") - imgui.begin_child(f"##tier3_{ticket_id}_scroll", imgui.ImVec2(-1, 150), True) - self._render_selectable_label(f'stream_t3_{ticket_id}', self.mma_streams[key], width=-1, multiline=True, height=0) - try: - if len(self.mma_streams[key]) != self._tier_stream_last_len.get(key, -1): - imgui.set_scroll_here_y(1.0) - self._tier_stream_last_len[key] = len(self.mma_streams[key]) - except (TypeError, AttributeError): - pass - imgui.end_child() - if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_tier_stream_panel") - - def _render_system_prompts_panel(self) -> None: - imgui.text("Global System Prompt (all projects)") - preset_names = sorted(self.controller.presets.keys()) - current_global = self.controller.ui_global_preset_name or "Select Preset..." - imgui.set_next_item_width(200) - if imgui.begin_combo("##global_preset", current_global): - for name in preset_names: - is_sel = (name == current_global) - if imgui.selectable(name, is_sel)[0]: - self.controller._apply_preset(name, "global") - if is_sel: - imgui.set_item_default_focus() - imgui.end_combo() - imgui.same_line(0, 8) - if imgui.button("Manage Presets##global"): - self.show_preset_manager_window = True - imgui.set_item_tooltip("Open preset management modal") - ch, self.ui_global_system_prompt = imgui.input_text_multiline("##gsp", self.ui_global_system_prompt, imgui.ImVec2(-1, 100)) - imgui.separator() - _, self.ui_use_default_base_prompt = imgui.checkbox("Use Default Base System Prompt", self.ui_use_default_base_prompt) - imgui.same_line() - if imgui.button("Reset to Default##btn_reset_base_prompt"): - self.controller._cb_reset_base_prompt() - imgui.same_line() - if imgui.button("Show Diff##btn_show_base_prompt_diff"): - self.controller._cb_show_base_prompt_diff() - imgui.set_item_tooltip("Compare current base prompt with the default.") - - imgui.same_line() - imgui.text_disabled("(?)") - imgui.set_item_tooltip("The Base System Prompt contains foundational instructions for the AI, including its role as a coding assistant and safety guidelines. You can override it here if needed.") - - header_flags = imgui.TreeNodeFlags_.default_open if not self.ui_use_default_base_prompt else 0 - if imgui.collapsing_header("Base System Prompt (foundational instructions)", header_flags): - if self.ui_use_default_base_prompt: - imgui.begin_disabled() - imgui.input_text_multiline("##base_prompt_def", ai_client._SYSTEM_PROMPT, imgui.ImVec2(-1, 100), imgui.InputTextFlags_.read_only) - imgui.end_disabled() - imgui.text_disabled(f"Characters: {len(ai_client._SYSTEM_PROMPT)}") - else: - ch, self.ui_base_system_prompt = imgui.input_text_multiline("##base_prompt", self.ui_base_system_prompt, imgui.ImVec2(-1, 150)) - imgui.text_disabled(f"Characters: {len(self.ui_base_system_prompt)}") - imgui.separator() - imgui.text("Project System Prompt") - current_project = self.controller.ui_project_preset_name or "Select Preset..." - imgui.set_next_item_width(200) - if imgui.begin_combo("##project_preset", current_project): - for name in preset_names: - is_sel = (name == current_project) - if imgui.selectable(name, is_sel)[0]: - self.controller._apply_preset(name, "project") - if is_sel: - imgui.set_item_default_focus() - imgui.end_combo() - imgui.same_line(0, 8) - if imgui.button("Manage Presets##project"): - self.show_preset_manager_window = True - imgui.set_item_tooltip("Open preset management modal") - ch, self.ui_project_system_prompt = imgui.input_text_multiline("##psp", self.ui_project_system_prompt, imgui.ImVec2(-1, 100)) - - def _render_rag_panel(self) -> None: - conf = self.controller.rag_config - if not conf: return - ch, conf.enabled = imgui.checkbox("Enable RAG", conf.enabled) - - imgui.text("Vector Store Provider") - providers = ['chroma', 'qdrant', 'mock'] - try: - idx = providers.index(conf.vector_store.provider) - except (ValueError, AttributeError): - idx = 0 - ch2, next_idx = imgui.combo("##rag_provider", idx, providers) - if ch2: - conf.vector_store.provider = providers[next_idx] - - imgui.text("Embedding Provider") - emb_providers = ['gemini', 'local'] - try: - idx_e = emb_providers.index(conf.embedding_provider) - except (ValueError, AttributeError): - idx_e = 0 - ch3, next_idx_e = imgui.combo("##rag_emb_provider", idx_e, emb_providers) - if ch3: - conf.embedding_provider = emb_providers[next_idx_e] - - imgui.text("Chunk Size") - imgui.set_next_item_width(150) - ch4, conf.chunk_size = imgui.input_int("##rag_chunk_size", conf.chunk_size) - imgui.text("Chunk Overlap") - imgui.set_next_item_width(150) - ch5, conf.chunk_overlap = imgui.input_int("##rag_chunk_overlap", conf.chunk_overlap) - - imgui.separator() - imgui.text(f"Status: {self.controller.rag_status}") - - if imgui.button("Rebuild Index"): - self.controller.event_queue.put('click', 'btn_rebuild_rag_index') + self.config["paths"] = { + "logs_dir": self.ui_logs_dir, + "scripts_dir": self.ui_scripts_dir + } + cfg_path = paths.get_config_path() + if cfg_path.exists(): + shutil.copy(cfg_path, str(cfg_path) + ".bak") + models.save_config(self.config) + paths.reset_resolved() + self.init_state() + self.ai_status = 'paths applied and session reset' def _render_external_editor_panel(self) -> None: from src.external_editor import get_default_launcher @@ -4982,89 +5003,6 @@ def hello(): except Exception as e: imgui.text_colored(C_TC, f"Error: {str(e)}") - def _render_agent_tools_panel(self) -> None: - if imgui.collapsing_header("Active Tool Presets & Biases", imgui.TreeNodeFlags_.default_open): - imgui.text("Tool Preset") - presets = self.controller.tool_presets - preset_names = [""] + sorted(list(presets.keys())) - - active = getattr(self, "ui_active_tool_preset", "") - if active is None: active = "" - try: - idx = preset_names.index(active) - except ValueError: - idx = 0 - - ch, new_idx = imgui.combo("##tool_preset_select", idx, preset_names) - if ch: - self.ui_active_tool_preset = preset_names[new_idx] - - imgui.same_line() - if imgui.button("Manage Presets##tools"): - self.show_tool_preset_manager_window = True - if imgui.is_item_hovered(): - imgui.set_tooltip("Configure tool availability and default modes.") - - imgui.dummy(imgui.ImVec2(0, 4)) - imgui.text("Bias Profile") - if imgui.begin_combo("##bias", getattr(self, 'ui_active_bias_profile', "") or "None"): - if imgui.selectable("None", not getattr(self, 'ui_active_bias_profile', ""))[0]: - self.ui_active_bias_profile = "" - from src import ai_client - ai_client.set_bias_profile(None) - for bname in sorted(self.controller.bias_profiles.keys()): - if not bname: - continue - if imgui.selectable(bname, bname == getattr(self, 'ui_active_bias_profile', ""))[0]: - self.ui_active_bias_profile = bname - from src import ai_client - ai_client.set_bias_profile(bname) - imgui.end_combo() - - imgui.dummy(imgui.ImVec2(0, 8)) - cat_options = ["All"] + sorted(list(models.DEFAULT_TOOL_CATEGORIES.keys())) - try: - f_idx = cat_options.index(self.ui_tool_filter_category) - except ValueError: - f_idx = 0 - imgui.set_next_item_width(200) - ch_cat, next_f_idx = imgui.combo("Filter Category##agent", f_idx, cat_options) - if ch_cat: - self.ui_tool_filter_category = cat_options[next_f_idx] - - imgui.dummy(imgui.ImVec2(0, 8)) - active_name = self.ui_active_tool_preset - if active_name and active_name in presets: - preset = presets[active_name] - for cat_name, tools in preset.categories.items(): - if self.ui_tool_filter_category != "All" and self.ui_tool_filter_category != cat_name: - continue - if imgui.tree_node(cat_name): - for tool in tools: - if tool.weight >= 5: - imgui.text_colored(vec4(255, 100, 100), "[HIGH]") - imgui.same_line() - elif tool.weight == 4: - imgui.text_colored(vec4(255, 255, 100), "[PREF]") - imgui.same_line() - elif tool.weight == 2: - imgui.text_colored(vec4(255, 150, 50), "[REJECT]") - imgui.same_line() - elif tool.weight <= 1: - imgui.text_colored(vec4(180, 180, 180), "[LOW]") - imgui.same_line() - - imgui.text(tool.name) - imgui.same_line(180) - - mode = tool.approval - if imgui.radio_button(f"Auto##{cat_name}_{tool.name}", mode == "auto"): - tool.approval = "auto" - imgui.same_line() - if imgui.radio_button(f"Ask##{cat_name}_{tool.name}", mode == "ask"): - tool.approval = "ask" - imgui.tree_pop() - def _render_theme_panel(self) -> None: if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_theme_panel") exp, opened = imgui.begin("Theme", self.show_windows["Theme"]) @@ -5081,11 +5019,11 @@ def hello(): imgui.end_combo() imgui.separator() - ch1, self.ui_separate_message_panel = imgui.checkbox("Separate Message Panel", self.ui_separate_message_panel) - ch2, self.ui_separate_response_panel = imgui.checkbox("Separate Response Panel", self.ui_separate_response_panel) + ch1, self.ui_separate_message_panel = imgui.checkbox("Separate Message Panel", self.ui_separate_message_panel) + ch2, self.ui_separate_response_panel = imgui.checkbox("Separate Response Panel", self.ui_separate_response_panel) ch3, self.ui_separate_tool_calls_panel = imgui.checkbox("Separate Tool Calls Panel", self.ui_separate_tool_calls_panel) - if ch1: self.show_windows["Message"] = self.ui_separate_message_panel - if ch2: self.show_windows["Response"] = self.ui_separate_response_panel + if ch1: self.show_windows["Message"] = self.ui_separate_message_panel + if ch2: self.show_windows["Response"] = self.ui_separate_response_panel if ch3: self.show_windows["Tool Calls"] = self.ui_separate_tool_calls_panel imgui.separator() imgui.text("Font")